Sign in to see your venue's slug and publishable key in every sample.
Your account has no venue yet, so the samples keep their placeholders. Sign out
Signed in as · . Create a publishable key in the console and reload to see it here. Sign out
Signed in as · . The samples show your venue's publishable key. Sign out
Developer docs
Events
List a venue's published events, read one with its registration window, register for a free event with named tickets, and the keyless ticket and cancel routes.
A venue on Pro or higher publishes its events — a darts league night, a match screening, a weekly quiz — and guests register for them through the same API the widgets use. A registration produces one named ticket per attendee; each ticket has a code and a page with the code the venue checks in at the door. The list, detail and registration routes follow the same key and CORS rules as the rest of /api/venues/**. The last two routes on this page need no key at all: the ticket code and the cancel token in the URL are the credentials a guest received by e-mail, exactly like a printed QR token on QR codes and table sessions.
Events are created, published and cancelled in the console at app.bookdineplay.com; there is no public write route for them. The JavaScript SDK renders the whole flow as a widget or hands you the calls for your own markup; the .NET SDK wraps the same five calls.
Plans and visibility
| Situation | List | Detail | Registration |
|---|---|---|---|
| Published event, plan includes Events | listed | 200 |
201, or 409 when the window is shut |
| Cancelled event | not listed | 200 with status: "Cancelled" and registration.open: false |
409 registration-closed, reason: "not-open" |
| Draft, or unknown slug | not listed | 404 |
404 |
| Venue's plan does not include Events (Starter) | 200 with an empty events array |
404 |
403 feature-not-in-plan with requiredPlan: "Pro" |
| Unknown venue | 404 |
404 |
404 |
A widget on a Starter venue therefore shows an empty state rather than an error the guest cannot act on. The ticket and cancel routes are not plan-gated: a guest who registered while the plan allowed it keeps their ticket page and cancel link after a downgrade.
The registration window
Every event detail carries a registration object that says whether a registration would be accepted right now, and why not otherwise:
| Field | Type | Meaning |
|---|---|---|
mode |
string | None (informational, nothing to register), Free (named tickets, no payment) or Paid (tickets with a price — Premium) |
price, currency |
decimal, string | Ticket price of a paid event, e.g. 12.5 and EUR; both null otherwise |
capacity |
integer or null | Maximum number of tickets; null when unlimited |
remaining |
integer or null | Tickets still available; null when unlimited. Counts down as registrations arrive and back up when one is cancelled |
maxPerRegistration |
integer | Tickets one registration may hold (the venue's setting, default 6). Ask for min(maxPerRegistration, remaining) at most |
closesAt |
ISO 8601 instant or null | When the venue closes registration early; null when it stays open until the event starts |
open |
boolean | true when a registration sent now would be accepted |
reason |
string or null | Why open is false: closed (past closesAt), sold-out (remaining is 0), past (the event has started), not-open (mode None, or the event was cancelled). Null while open |
The same four reasons come back in the registration-closed problem when a registration is refused — the window can shut between the detail read and the registration, so handle both.
Endpoints
GET /api/venues/{venueSlug}/events
Published events in listing order: dated events ascending by date and start, ongoing events (no date) after them. Drafts and cancelled events are never listed. A venue whose plan does not include events returns an empty list, not an error.
Query — upcoming (true by default) drops dated events whose day has passed in the venue's time zone; upcoming=false returns every published event, past ones included.
Response 200 OK — events[], each:
| Field | Type | Meaning |
|---|---|---|
slug, title, excerpt |
string | Identity and teaser (excerpt nullable) |
imageUrl, imageAlt |
string or null | The event picture and its alt text |
status |
string | Published here; Cancelled on a detail read |
date |
string or null | Venue-local yyyy-MM-dd; null for an ongoing event |
start, end |
string or null | Venue-local HH:mm |
timeLabel |
string or null | Free text the venue shows next to, or instead of, the times (Doors 19:30, Every Thursday from 20:00) |
locationLabel |
string | Where it happens — the venue's label, or its name and address |
registrationMode |
string | None, Free or Paid |
language |
string | en or de — the language the event was written in |
{
"events": [
{
"slug": "darts-league-night",
"title": "Darts league night",
"excerpt": "Eight boards, one winner — sign up your team.",
"imageUrl": null,
"imageAlt": null,
"status": "Published",
"date": "2027-10-16",
"start": "20:00",
"end": "23:00",
"timeLabel": "Doors 19:30",
"locationLabel": "The Neon Tap, Boxhagener Straße 42, 10245 Berlin",
"registrationMode": "Free",
"language": "en"
},
{
"slug": "summer-cup-final",
"title": "Summer cup final",
"excerpt": "Watch the final on the big screens.",
"imageUrl": null,
"imageAlt": null,
"status": "Published",
"date": "2027-10-23",
"start": "18:00",
"end": null,
"timeLabel": null,
"locationLabel": "The Neon Tap, Boxhagener Straße 42, 10245 Berlin",
"registrationMode": "Free",
"language": "en"
},
{
"slug": "pub-quiz",
"title": "Pub quiz",
"excerpt": "Six rounds, free entry, prizes for the top three tables.",
"imageUrl": null,
"imageAlt": null,
"status": "Published",
"date": null,
"start": null,
"end": null,
"timeLabel": "Every Thursday from 20:00",
"locationLabel": "The Neon Tap, Boxhagener Straße 42, 10245 Berlin",
"registrationMode": "None",
"language": "en"
}
]
}Errors — 404 no venue with that slug.
curl
curl "https://api.bookdineplay.com/api/venues/your-venue/events" \
-H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
-H "Origin: https://www.your-venue.example"JavaScript
const client = BookDinePlay.createClient({
apiBaseUrl: 'https://api.bookdineplay.com', venueSlug: 'your-venue', publishableKey: 'bdp_pk_your_publishable_key'
});
const events = await client.events(); // the array, upcoming only
const all = await client.events({ upcoming: false }); // past dated events tooC#
var events = await client.GetEventsAsync("your-venue", cancellationToken: cancellationToken);
var all = await client.GetEventsAsync("your-venue", upcoming: false, cancellationToken);GET /api/venues/{venueSlug}/events/{eventSlug}
One event with its description and registration window. A cancelled event still answers 200 with status: "Cancelled" so a bookmarked link can explain itself; a draft or unknown slug is 404.
Response 200 OK — the list fields plus venueSlug, venueName, descriptionHtml and registration (The registration window). descriptionHtml is rendered from the venue's Markdown and filtered on the server against a strict allowlist — paragraphs, headings, emphasis, lists, quotes, code, links (rel="noopener nofollow") and https images. Allowlisted inline HTML (<strong>, <a> …) the venue wrote in its Markdown survives the filter; nothing outside that allowlist survives — no scripts, styles, event handlers or raw HTML beyond it — so it is safe to insert into your page as HTML.
{
"venueSlug": "demo-sportsbar",
"venueName": "The Neon Tap",
"slug": "darts-league-night",
"title": "Darts league night",
"excerpt": "Eight boards, one winner — sign up your team.",
"descriptionHtml": "<p>Bring your <strong>own</strong> darts or borrow a set at the bar. Registration closes at the door.</p>\n",
"imageUrl": null,
"imageAlt": null,
"status": "Published",
"date": "2027-10-16",
"start": "20:00",
"end": "23:00",
"timeLabel": "Doors 19:30",
"locationLabel": "The Neon Tap, Boxhagener Straße 42, 10245 Berlin",
"language": "en",
"registration": {
"mode": "Free",
"price": null,
"currency": null,
"capacity": 64,
"remaining": 64,
"maxPerRegistration": 6,
"closesAt": null,
"open": true,
"reason": null
}
}Errors — 404 no such venue, or no published event with that slug (a draft, an unknown slug, or a venue whose plan lacks Events).
curl
curl "https://api.bookdineplay.com/api/venues/your-venue/events/darts-league-night" \
-H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
-H "Origin: https://www.your-venue.example"JavaScript
const event = await client.event('darts-league-night'); // rejects with status 404 when unknown
if (!event.registration.open) console.log(event.registration.reason);C#
var detail = await client.GetEventAsync("your-venue", "darts-league-night", cancellationToken); // null when unknownPOST /api/venues/{venueSlug}/events/{eventSlug}/registrations
Register for a free event. quantity tickets are minted, one per attendee name in attendees (the first may be blank — it defaults to the buyer). The guest receives a confirmation e-mail with the tickets and a cancel link (What the guest receives).
Request body (Content-Type: application/json)
| Field | Required | Meaning |
|---|---|---|
buyerName |
yes | 1–120 characters — the person registering; also the first attendee when attendees[0] is blank |
email |
yes | A valid e-mail address — the tickets go there |
phone |
no | Up to 40 characters; a blank value counts as not supplied |
notes |
no | Up to 1000 characters, shown to the venue |
quantity |
yes | 1–100, and at most registration.maxPerRegistration and registration.remaining |
attendees |
no | One name per ticket, in ticket order, each up to 120 characters. Entry 0 may be blank (the buyer's name is used); every further ticket needs a name. More names than quantity is an error |
Response 201 Created with a Location header of the event detail URL.
| Field | Type | Meaning |
|---|---|---|
reference |
string | BDP-E-XXXX — what the guest quotes |
status |
string | Confirmed; Cancelled after the guest or the venue cancels |
payment |
string | NotRequired for a free event (Pending, Paid, Refunded are reserved for paid tickets) |
eventSlug, eventTitle |
string | Which event |
buyerName, email, quantity |
What was sent, normalized | |
tickets[] |
object[] | One per attendee: code (12 characters), attendeeName, ticketUrl — the ticket page with the QR code |
cancelUrl |
string | The guest's cancel page; the last path segment is the cancel token |
{
"reference": "BDP-E-ESJA",
"status": "Confirmed",
"payment": "NotRequired",
"eventSlug": "darts-league-night",
"eventTitle": "Darts league night",
"buyerName": "Ada Lovelace",
"email": "ada@example.com",
"quantity": 2,
"tickets": [
{
"code": "X7QZUP5AC3GH",
"attendeeName": "Ada Lovelace",
"ticketUrl": "https://app.bookdineplay.com/ticket/X7QZUP5AC3GH"
},
{
"code": "DR9HN4GDX9ZH",
"attendeeName": "Grace Hopper",
"ticketUrl": "https://app.bookdineplay.com/ticket/DR9HN4GDX9ZH"
}
],
"cancelUrl": "https://app.bookdineplay.com/e/dqN_AejenL2r7qXPdZuOxnLY9_BfVkTl"
}Errors
| Status | When |
|---|---|
400 |
A field fails validation, or a registration rule does. errors maps the field name (Email, BuyerName, Quantity, …) to its messages; a broken rule — more tickets than maxPerRegistration or than are left, a missing attendee name, more names than tickets — sits under errors.registration and detail repeats it as one sentence |
403 |
The venue's plan does not include Events — feature-not-in-plan with requiredPlan: "Pro" |
404 |
No such venue, or no published event with that slug |
409 |
registration-closed: the window is shut. reason says why (closed, sold-out, past, not-open); remaining: 0 is added when sold out — including when the last seats went to someone else between the detail read and this call |
409 |
ticket-sales-not-enabled: the event is Paid and online ticket sales are not available yet |
A rule failure looks like this — the same errors dictionary as a field failure, so a client handles one shape on 400:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"detail": "You can register at most 6 tickets at once.",
"errors": {
"registration": [
"You can register at most 6 tickets at once."
]
},
"traceId": "0HNOK3A31C71S"
}Rule messages are localized by Accept-Language; the confirmation e-mail is sent in that language too, falling back to the event's own language.
curl
curl -X POST "https://api.bookdineplay.com/api/venues/your-venue/events/darts-league-night/registrations" \
-H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
-H "Origin: https://www.your-venue.example" \
-H "Content-Type: application/json" \
-d '{ "buyerName": "Ada Lovelace", "email": "ada@example.com", "quantity": 2, "attendees": ["", "Grace Hopper"] }'JavaScript
try {
const registration = await client.registerForEvent('darts-league-night', {
buyerName: 'Ada Lovelace', email: 'ada@example.com', quantity: 2, attendees: ['', 'Grace Hopper']
});
console.log(registration.reference, registration.tickets.map((t) => t.code), registration.cancelUrl);
} catch (err) {
if (err.name === 'BookDinePlayError' && err.status === 409) console.log(err.reason); // 'sold-out', ...
if (err.name === 'BookDinePlayError' && err.status === 400) console.log(err.messages()); // the errors, flattened
}C#
try
{
var registration = await client.RegisterForEventAsync("your-venue", "darts-league-night",
new EventRegistrationRequest { BuyerName = "Ada Lovelace", Email = "ada@example.com", Quantity = 2, Attendees = ["", "Grace Hopper"] },
cancellationToken);
}
catch (BookDinePlayApiException ex) when (ex.Type == BookDinePlayApiException.RegistrationClosedType)
{
Console.WriteLine(ex.Reason); // "closed", "sold-out", "past" or "not-open"; ex.Remaining is 0 when sold out
}
catch (BookDinePlayApiException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
{
Console.WriteLine(string.Join(" ", ex.Messages)); // ex.Errors: field (or "registration") -> messages
}POST /api/venues/{venueSlug}/events/{eventSlug}/tickets/checkout
Reserved for paid events. Until online ticket sales are enabled for the venue it answers 409 with type ticket-sales-not-enabled (403 feature-not-in-plan and 404 apply as above). There is no request body yet, and the SDKs have no method for it until the route does something.
{
"type": "https://bookdineplay.com/docs/api/errors/ticket-sales-not-enabled",
"title": "Ticket sales not enabled",
"status": 409,
"detail": "Online ticket sales are not enabled for this venue yet.",
"traceId": "0HNOK3A31C71U"
}curl
curl -X POST "https://api.bookdineplay.com/api/venues/your-venue/events/gala-night/tickets/checkout" \
-H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
-H "Origin: https://www.your-venue.example"JavaScript
const response = await fetch('https://api.bookdineplay.com/api/venues/your-venue/events/gala-night/tickets/checkout', {
method: 'POST', headers: { 'X-BookDinePlay-Key': 'bdp_pk_your_publishable_key' }
});C#
using var response = await http.PostAsync("api/venues/your-venue/events/gala-night/tickets/checkout", null, cancellationToken);GET /api/tickets/{code}
Everything the ticket page shows, by the 12-character code. No key: the code is the credential. Input is normalized — abcd-efgh-jklm resolves like ABCDEFGHJKLM. Not plan-gated.
Response 200 OK
| Field | Type | Meaning |
|---|---|---|
code, attendeeName |
string | This ticket |
status |
string | Valid, CheckedIn (scanned at the door) or Cancelled |
checkedInAt |
ISO 8601 instant or null | When it was scanned |
reference, registrationStatus |
string | The registration it belongs to and its status (Confirmed, Cancelled) |
venueSlug, venueName |
string | The venue |
event |
object | The list shape of the event, status included — a ticket for a cancelled event still reads Valid, so check event.status too |
ticketUrl |
string | The ticket page on app.bookdineplay.com |
qrSvg |
string | An inline SVG of a QR code that encodes ticketUrl; insert it as HTML |
{
"code": "X7QZUP5AC3GH",
"attendeeName": "Ada Lovelace",
"status": "Valid",
"checkedInAt": null,
"reference": "BDP-E-ESJA",
"registrationStatus": "Confirmed",
"venueSlug": "demo-sportsbar",
"venueName": "The Neon Tap",
"event": {
"slug": "darts-league-night",
"title": "Darts league night",
"excerpt": "Eight boards, one winner — sign up your team.",
"imageUrl": null,
"imageAlt": null,
"status": "Published",
"date": "2027-10-16",
"start": "20:00",
"end": "23:00",
"timeLabel": "Doors 19:30",
"locationLabel": "The Neon Tap, Boxhagener Straße 42, 10245 Berlin",
"registrationMode": "Free",
"language": "en"
},
"ticketUrl": "https://app.bookdineplay.com/ticket/X7QZUP5AC3GH",
"qrSvg": "<svg …>…</svg>"
}(attributes shortened)
Errors — 404 for an unknown or malformed code.
curl
curl "https://api.bookdineplay.com/api/tickets/ABCDEFGHJKLM"JavaScript
const ticket = await client.ticket('ABCDEFGHJKLM'); // sends no key
document.querySelector('#qr').innerHTML = ticket.qrSvg;C#
var ticket = await client.GetTicketAsync("ABCDEFGHJKLM", cancellationToken); // sends no key; null when unknownPOST /api/event-registrations/{cancelToken}/cancel
Cancels the registration behind a cancel link and frees its seats; the guest receives a confirmation e-mail. No key: the token is the credential. Idempotent — a second call answers 200 again. Not plan-gated. No request body.
Response 200 OK — reference, status (Cancelled), eventSlug, eventTitle.
{
"reference": "BDP-E-ESJA",
"status": "Cancelled",
"eventSlug": "darts-league-night",
"eventTitle": "Darts league night"
}Errors — 404 for an unknown token; 409 once a ticket of the registration has been checked in (only the venue can cancel then).
curl
curl -X POST "https://api.bookdineplay.com/api/event-registrations/<cancelToken>/cancel"JavaScript
const token = location.pathname.split('/').pop();
const result = await client.cancelEventRegistration(token); // sends no keyC#
var result = await client.CancelEventRegistrationAsync(cancelToken, cancellationToken); // sends no key; null when unknownWhy two routes take no key
Every /api/venues/** route needs a key because a page on the venue's site makes the call, and that page carries the venue's publishable key. A guest who opens a ticket link or a cancel link from their e-mail has no such page: they land on app.bookdineplay.com, or on a page of yours that only knows the code or token from the URL. Requiring a key there would either force a credential into an e-mailed URL or break the link. The ticket code and the cancel token are already per-registration credentials — the same reasoning as the printed QR token behind QR codes and table sessions — so these two routes hang off the API root, outside the venue prefix, and both SDKs deliberately send no key on them.
What the guest receives
- On registration — one e-mail with every ticket (code, QR code, a link to its ticket page) and the cancel link. Sent in the language of the request (
Accept-Language), falling back to the event'slanguage. - On cancellation — a confirmation e-mail, whether the guest cancelled through the link or the venue cancelled the registration in the console.
- When the venue cancels the event — every confirmed registrant is told by e-mail.
The two pages behind those links live on app.bookdineplay.com and need nothing from your site:
| Page | URL | Shows |
|---|---|---|
| Ticket | https://app.bookdineplay.com/ticket/{code} |
The ticket with its QR code, the attendee, the event and the venue — what the venue checks in at the door |
| Cancel | https://app.bookdineplay.com/e/{cancelToken} |
The registration and a button that cancels it |
A site with its own event pages can render both itself from the two keyless routes above; ticketUrl and cancelUrl in the registration response keep pointing at the BookDinePlay pages, so the e-mails work either way.
Next steps
- JavaScript SDK: Events — the events widget, and the client calls for your own markup.
- Errors —
registration-closedandticket-sales-not-enabledin detail. - .NET SDK — the same five calls from a server.