Skip to content

Developer API

Read event data with API keys, or use public widget endpoints for zero-setup embeds.

5-minute quick start

  1. Create an API key in the dashboard under API keys (org admin only). The key is shown once — copy it immediately and store it in your secret manager.
  2. Make your first request with the Bearer token:

    curl

    curl -H "Authorization: Bearer gsk_YOUR_KEY" \
      https://your-domain.example/api/v1/events
  3. Parse the response — events are in data, pagination in pagination.
  4. Walk all pages using the pagination loop (see Pagination below).
  5. Need unauthenticated reads? Use the public widget API for browser-based integrations: GET /api/public/widgets/events?organizer=:slug.

Authentication

API keys

Create organization API keys in the dashboard under API keys. Authenticated API routes use a Bearer token in the Authorization header.

Keys are prefixed gsk_ and SHA-256 hashed in the database. The raw value is only shown once at creation time.

Request

GET /api/v1/events HTTP/1.1
Host: your-domain.example
Authorization: Bearer gsk_YOUR_KEY

Language examples

curl

curl -H "Authorization: Bearer gsk_YOUR_KEY" \
  https://your-domain.example/api/v1/events?limit=2

Limits and CORS

Rate limiting

Authenticated API routes are limited to 120 requests per minute per client IP. Public widget endpoints use a separate 240/min limit. The 429 response includes a Retry-After header.

Rate limiting is disabled in NODE_ENV=test.

Browser access (CORS)

For the authenticated API, set API_CORS_ORIGINS to a comma-separated list of allowed origins (or * to allow any).

Public widget endpoints (/api/public/widgets/*) always emit Access-Control-Allow-Origin: * and never use credentials. Safe for any browser context.

Authenticated endpoints

All authenticated routes require a valid Bearer token and return only published events for the key's organization.

GET/api/v1/events

List published events for the API key organization. Supports `page` (default 1) and `limit` (default 50, max 100) query params.

curl -H "Authorization: Bearer gsk_YOUR_KEY" \
  https://your-domain.example/api/v1/events?limit=2
GET/api/v1/events/:slug

Fetch event detail and ticket type data for one published event in the API key organization.

curl -H "Authorization: Bearer gsk_YOUR_KEY" \
  https://your-domain.example/api/v1/events/friday-sessions

Public widget endpoints

No authentication required. Returns only active-organization, published-event data. CORS: *.

GET/api/public/widgets/events?organizer=:slug&limit=6

Public listings data for published events. Used by the Listings widget. Safe for browser calls without credentials.

GET/api/public/widgets/events/:publicId

Public event detail for the Buy button widget. Public event IDs are generated by the dashboard in the format `evt_XXXX`.

Try it: live playground

Hit the public widget endpoints right from this page. The browser sends the request from your machine — useful for testing without setting up a key.

Request URL

Response

Ready

Response examples

Real-world examples for the authenticated endpoints.

List: GET /api/v1/events

Response (200 OK)

{
  "data": [
    {
      "id": "3b8e1f4a-7c2d-4e9a-8b5f-1d2e3f4a5b6c",
      "name": "Friday Sessions",
      "slug": "friday-sessions",
      "start_time": "2026-06-12T19:00:00.000Z",
      "end_time": "2026-06-12T22:00:00.000Z",
      "description": "Live music every Friday.",
      "category": "music",
      "age_restriction": "all_ages",
      "image_url": "https://your-domain.example/uploads/events/friday-sessions.jpg",
      "refund_policy": "Refunds available up to 24 hours before the event.",
      "venue": {
        "name": "Demo Hall",
        "address": "123 High Street",
        "city": "London"
      },
      "ticket_types": [
        {
          "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "name": "General Admission",
          "price": 1600,
          "currency": "GBP",
          "quantity_available": 120,
          "sale_start": "2026-05-01T09:00:00.000Z",
          "sale_end": "2026-06-12T18:00:00.000Z"
        }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 18,
    "has_more": false
  }
}

Detail: GET /api/v1/events/:slug

Response (200 OK)

{
  "id": "3b8e1f4a-7c2d-4e9a-8b5f-1d2e3f4a5b6c",
  "name": "Friday Sessions",
  "slug": "friday-sessions",
  "start_time": "2026-06-12T19:00:00.000Z",
  "end_time": "2026-06-12T22:00:00.000Z",
  "description": "Live music every Friday.",
  "category": "music",
  "age_restriction": "all_ages",
  "image_url": "https://your-domain.example/uploads/events/friday-sessions.jpg",
  "refund_policy": "Refunds available up to 24 hours before the event.",
  "venue": {
    "name": "Demo Hall",
    "address": "123 High Street",
    "city": "London"
  },
  "ticket_types": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "General Admission",
      "price": 1600,
      "currency": "GBP",
      "quantity_available": 120,
      "sale_start": "2026-05-01T09:00:00.000Z",
      "sale_end": "2026-06-12T18:00:00.000Z"
    }
  ]
}

Public widget event detail: GET /api/public/widgets/events/evt_DEMO

Response (200 OK)

{
  "event": {
    "public_id": "evt_DEMO",
    "name": "Demo Night Live",
    "slug": "demo-night-live",
    "summary": "A rolling demo event used to preview the hosted event button.",
    "start_time": "2026-06-12T19:00:00.000Z",
    "end_time": "2026-06-12T22:00:00.000Z",
    "image_url": "https://your-domain.example/seed/widgets/demo/friday-sessions.svg",
    "category": "music",
    "age_restriction": "all_ages",
    "event_format": "in_person",
    "venue_name": "Demo Hall",
    "venue_city": "London",
    "url": "https://your-domain.example/tickets/events/demo-night-live",
    "min_price": 1800,
    "currency": "GBP",
    "availability_status": "on_sale"
  },
  "brand": {
    "primary_color": "#F2CC5D",
    "font_family": "Inter"
  }
}

Pagination

The list endpoint is paginated. Use has_more to walk all pages.

Walk all events (JavaScript)

let page = 1
while (true) {
  const res = await fetch(
    `https://your-domain.example/api/v1/events?page=${page}&limit=100`,
    { headers: { Authorization: `Bearer ${process.env.API_KEY}` } },
  )
  const json = await res.json()
  await processEvents(json.data)
  if (!json.pagination.has_more) break
  page++
}

Walk all events (curl loop)

page=1
while true; do
  curl -H "Authorization: Bearer $KEY" \
    "https://your-domain.example/api/v1/events?page=$page&limit=100"
  # stop when has_more is false
  page=$((page + 1))
done

Pagination object

FieldTypeDescription
pagerequiredinteger

Current page (1-indexed).

limitrequiredinteger

Items per page (max 100).

totalrequiredinteger

Total items across all pages.

has_morerequiredboolean

`true` if more pages exist.

Response field reference

Event object

FieldTypeDescription
idrequiredstring

Internal event UUID.

namerequiredstring

Event name.

slugrequiredstring

URL slug, used for `/api/v1/events/:slug`.

start_timerequiredISO 8601 string

Event start (UTC).

end_timerequiredISO 8601 string

Event end (UTC).

descriptionstring | null

Long description.

categorystring | null

e.g. `music`, `comedy`, `theatre`.

age_restrictionstring | null

e.g. `all_ages`, `18_plus`.

image_urlstring | null

Absolute URL of the event image.

refund_policystring | null

Free-text refund policy.

venueVenue object | null

See Venue object below.

ticket_typesrequiredTicketType[]

See TicketType object below.

Venue object

FieldTypeDescription
namerequiredstring

Venue name.

addressstring | null

Street address.

citystring | null

City.

Ticket type object

FieldTypeDescription
idrequiredstring

Ticket type UUID.

namerequiredstring

e.g. `General Admission`.

pricerequiredinteger

Price in the smallest currency unit (pence, cents).

currencyrequiredstring

ISO 4217 currency code, e.g. `GBP`.

quantity_availablerequiredinteger

Remaining inventory.

sale_startISO 8601 string | null

On-sale start.

sale_endISO 8601 string | null

Sale end.

Price formatting

The API returns prices as integers in the smallest currency unit (pence for GBP, cents for USD). Format with Intl.NumberFormat:

const formatted = new Intl.NumberFormat("en-GB", {
  style: "currency",
  currency: event.ticket_types[0].currency,
}).format(event.ticket_types[0].price / 100)
// "£16.00"

Error responses

All errors return JSON with a single error field, except 429 (plain text with a Retry-After header).

401Unauthorized

Missing or invalid `Authorization` header on `/api/v1/*`. Verify the key (no extra whitespace, no missing `Bearer` prefix) and that you are hitting the right domain.

Response body

{ "error": "Unauthorized" }

Quick reference

FieldTypeDescription
400object

`{ "error": "Missing organizer" }` — public listings API called without `organizer` query param.

401object

`{ "error": "Unauthorized" }` — missing or invalid API key on `/api/v1/*`.

404object

`{ "error": "Event not found" }` or `"{ "error": "Organizer not found" }`. The resource does not exist or is not visible to your key.

429text

`Too Many Requests` — rate limit exceeded; check the `Retry-After` header.

Versioning

The current API is v1, mounted at /api/v1/. Backward-compatible additions (new fields, new endpoints) ship without a version bump. Breaking changes will ship as /api/v2/ with at least 6 months of overlap.

Next steps

Quick start

Two-track 5-minute walkthrough: embed widgets OR use the API. Great for first-time integrators.

Quick start →

Widget docs

Embed ticket widgets on any website with a small async script. Includes a snippet builder and CMS-specific guides.

Widget docs →