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
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /api/v1/events?includePast=true&includeUnlisted=true | events:read | Published events, soonest first. Drafts are never returned. |
| GET | /api/v1/events/:slug | events:read | One event, with live remaining count and its presale tier. |
| GET | /api/v1/events/:slug/availability | events:read | Capacity, sold, remaining, soldOut, maxPerOrder. Safe to call from a prerendered page on load. |
| POST | /api/v1/orders | orders:write | action: reserve | confirm | release. All idempotent on holdId — seats are held at reserve, not at payment. |
| GET | /api/v1/audience/eligibility | audience:read | ?slug=&email= — whether that person may buy right now, given the VIP and subscriber windows. |
| POST | /api/v1/audience/subscribe | audience:read | Join the mailing list from your own signup form. |
| POST | /api/v1/inquiries | inquiries:write | Your contact form writes a contact and a lead straight into the pipeline. |
| GET | /api/v1/inquiries?limit=100 | inquiries:read | Every open inquiry, oldest first — so you can show what's waiting wherever you already look. |
| POST | /api/v1/inquiries/answered | inquiries:write | Mark 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:read | An 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.
| Field | Type | Where the venue sets it |
|---|---|---|
| slug | string | Web address, artwork and Stripe |
| title | string | Event name |
| description | string | null | Description |
| startTime / endTime | number (ms) | Starts / Ends |
| timezone | string | Venue settings |
| status | on_sale | sold_out | cancelled | completed | Event page header |
| unlisted | boolean | Unlisted checkbox |
| priceCents | number | Ticket price |
| capacity | number | Capacity |
| maxPerOrder | number | Max per order |
| imageUrl | string | null | Web address, artwork and Stripe |
| stripeProductId | string | null | Web address, artwork and Stripe |
| taxCode | string | null | Web address, artwork and Stripe |
| collectDietary | boolean | Dietary requirements checkbox |
| taxBehavior | inclusive | exclusive | Tax |
| customFields[] | {key, label, type, options, required} | Ask at checkout |
| access.rungs[] | {listId, name, slug, opensAt, open} | Who gets in first |
| access.publicOpensAt | number | null | Who gets in first → Then everyone |
| access.nextOpensAt | number | null | Derived, for a countdown |
| salesCloseAt | number | Sales close |
| salesClosed | boolean | Derived — stop offering checkout |
| presale.* | legacy three-tier summary | Derived from the ladder |
| sold / remaining / soldOut | number / number / boolean | Live, from orders |
| ticketTypes[] | array | Tickets & 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