Developers

The API your website actually calls.

Events are managed in bestvenue and rendered on your own site. Tickets are sold on your own Stripe. This is the seam between the two.

Authentication

One bearer key per site, scoped to what that site needs.

Create keys in Settings → API keys. They are shown once, stored only as a hash, and revocable. Send them as Authorization: Bearer <key>.

An unknown key, a revoked key, and a key missing the right scope all fail the same way, and a key reaching another venue's record gets a plain 404 — so a key can never be used to find out what exists somewhere else.

Where each key belongs

Read keys may live in a browser. The GET endpoints send CORS headers, so an event page can fetch its own availability on load. Those keys expose nothing a visitor could not already see.

Write keys must stay on a server. The POST endpoints deliberately send no CORS headers, so a browser cannot call them at all — a key that can hold seats or write leads is not something to ship to the public.

Rate limits are 240 reads and 60 writes a minute per key. Over that you get a 429 with Retry-After.

Endpoints

MethodPathScopeWhat it does
GET/api/v1/events?includePast=true&includeUnlisted=trueevents:readPublished events, soonest first. Drafts are never returned.
GET/api/v1/events/:slugevents:readOne event, with live remaining count and its presale tier.
GET/api/v1/events/:slug/availabilityevents:readCapacity, sold, remaining, soldOut, maxPerOrder. Safe to call from a prerendered page on load.
POST/api/v1/ordersorders:writeaction: reserve | confirm | release. All idempotent on holdId — seats are held at reserve, not at payment.
GET/api/v1/audience/eligibilityaudience:read?slug=&email= — whether that person may buy right now, given the VIP and subscriber windows.
POST/api/v1/audience/subscribeaudience:readJoin the mailing list from your own signup form.
POST/api/v1/inquiriesinquiries:writeYour contact form writes a contact and a lead straight into the pipeline.
GET/api/v1/inquiries?limit=100inquiries:readEvery open inquiry, oldest first — so you can show what's waiting wherever you already look.
POST/api/v1/inquiries/answeredinquiries:writeMark one answered, by email or by id. Wire it to a mail rule, a helpdesk, or whatever you use.
GET/api/v1/calendar?key=…events:readAn iCal feed of everything holding a room. Calendar clients cannot send headers, so the key goes in the query string.

The event payload

Everything your site renders, set by the venue.

Nothing here is hard-coded on our side. Each field has a box a venue fills in, so a change on the event page is a change on your site.

FieldTypeWhere the venue sets it
slugstringWeb address, artwork and Stripe
titlestringEvent name
descriptionstring | nullDescription
startTime / endTimenumber (ms)Starts / Ends
timezonestringVenue settings
statuson_sale | sold_out | cancelled | completedEvent page header
unlistedbooleanUnlisted checkbox
priceCentsnumberTicket price
capacitynumberCapacity
maxPerOrdernumberMax per order
imageUrlstring | nullWeb address, artwork and Stripe
stripeProductIdstring | nullWeb address, artwork and Stripe
taxCodestring | nullWeb address, artwork and Stripe
collectDietarybooleanDietary requirements checkbox
taxBehaviorinclusive | exclusiveTax
customFields[]{key, label, type, options, required}Ask at checkout
access.rungs[]{listId, name, slug, opensAt, open}Who gets in first
access.publicOpensAtnumber | nullWho gets in first → Then everyone
access.nextOpensAtnumber | nullDerived, for a countdown
salesCloseAtnumberSales close
salesClosedbooleanDerived — stop offering checkout
presale.*legacy three-tier summaryDerived from the ladder
sold / remaining / soldOutnumber / number / booleanLive, from orders
ticketTypes[]arrayTickets & inventory tab

Selling a ticket

Reserve, charge, confirm.

Seats are consumed the moment a checkout starts, so two buyers cannot take the last pair at once. An abandoned checkout expires on its own after thirty minutes.

Every step is idempotent on holdId, because payment webhooks retry.

// 1. Hold the seats before sending the buyer to checkout
const held = await fetch("https://bestvenue.tech/api/v1/orders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BESTVENUE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    action: "reserve",
    slug: "marisol-trio",
    holdId: crypto.randomUUID(),   // reuse this on retry
    quantity: 2,
    customerEmail: buyerEmail,     // checked against the presale window
  }),
}).then((response) => response.json());

// 2. Take the money on your own Stripe account, however you already do.

// 3. Confirm, so the seats stop being a hold and start being a sale.
await fetch("https://bestvenue.tech/api/v1/orders", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    action: "confirm",
    holdId: held.holdId,
    externalPaymentId: session.id,
  }),
});

Webhooks

Your site rebuilds when something changes.

Add an endpoint in Settings and we POST to it on event.published, event.updated, event.cancelled, event.sold_out, and booking.confirmed.

Each delivery carries X-Bestvenue-Signature, an HMAC-SHA256 of the body using the endpoint secret shown when you created it. Verify it before acting on the payload.

Wire your site to one source of truth.

14-day trial · no card required