Developer API
Read event data with API keys, or use public widget endpoints for zero-setup embeds.
5-minute quick start
- 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.
- Make your first request with the Bearer token:
curl
curl -H "Authorization: Bearer gsk_YOUR_KEY" \ https://your-domain.example/api/v1/events - Parse the response — events are in
data, pagination inpagination. - Walk all pages using the pagination loop (see Pagination below).
- 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_KEYLanguage examples
curl
curl -H "Authorization: Bearer gsk_YOUR_KEY" \
https://your-domain.example/api/v1/events?limit=2Limits 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.
/api/v1/eventsList 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/api/v1/events/:slugFetch 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-sessionsPublic widget endpoints
No authentication required. Returns only active-organization, published-event data. CORS: *.
/api/public/widgets/events?organizer=:slug&limit=6Public listings data for published events. Used by the Listings widget. Safe for browser calls without credentials.
/api/public/widgets/events/:publicIdPublic 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
ReadyResponse 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))
donePagination object
| Field | Type | Description |
|---|---|---|
pagerequired | integer | Current page (1-indexed). |
limitrequired | integer | Items per page (max 100). |
totalrequired | integer | Total items across all pages. |
has_morerequired | boolean | `true` if more pages exist. |
Response field reference
Event object
| Field | Type | Description |
|---|---|---|
idrequired | string | Internal event UUID. |
namerequired | string | Event name. |
slugrequired | string | URL slug, used for `/api/v1/events/:slug`. |
start_timerequired | ISO 8601 string | Event start (UTC). |
end_timerequired | ISO 8601 string | Event end (UTC). |
description | string | null | Long description. |
category | string | null | e.g. `music`, `comedy`, `theatre`. |
age_restriction | string | null | e.g. `all_ages`, `18_plus`. |
image_url | string | null | Absolute URL of the event image. |
refund_policy | string | null | Free-text refund policy. |
venue | Venue object | null | See Venue object below. |
ticket_typesrequired | TicketType[] | See TicketType object below. |
Venue object
| Field | Type | Description |
|---|---|---|
namerequired | string | Venue name. |
address | string | null | Street address. |
city | string | null | City. |
Ticket type object
| Field | Type | Description |
|---|---|---|
idrequired | string | Ticket type UUID. |
namerequired | string | e.g. `General Admission`. |
pricerequired | integer | Price in the smallest currency unit (pence, cents). |
currencyrequired | string | ISO 4217 currency code, e.g. `GBP`. |
quantity_availablerequired | integer | Remaining inventory. |
sale_start | ISO 8601 string | null | On-sale start. |
sale_end | ISO 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
| Field | Type | Description |
|---|---|---|
400 | object | `{ "error": "Missing organizer" }` — public listings API called without `organizer` query param. |
401 | object | `{ "error": "Unauthorized" }` — missing or invalid API key on `/api/v1/*`. |
404 | object | `{ "error": "Event not found" }` or `"{ "error": "Organizer not found" }`. The resource does not exist or is not visible to your key. |
429 | text | `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 →