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.

Queryupcoming (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 OKevents[], 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"
    }
  ]
}

Errors404 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 too

C#

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
  }
}

Errors404 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 unknown

POST /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)

Errors404 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 unknown

POST /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 OKreference, status (Cancelled), eventSlug, eventTitle.

{
  "reference": "BDP-E-ESJA",
  "status": "Cancelled",
  "eventSlug": "darts-league-night",
  "eventTitle": "Darts league night"
}

Errors404 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 key

C#

var result = await client.CancelEventRegistrationAsync(cancelToken, cancellationToken); // sends no key; null when unknown

Why 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's language.
  • 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.
  • Errorsregistration-closed and ticket-sales-not-enabled in detail.
  • .NET SDK — the same five calls from a server.