Core API
REST reference.
Plain REST and JSON over HTTPS. Two environments, one error envelope, cursor pagination and idempotent writes read this page once and the rest of the API holds no surprises.
Base URLs & environments#
Zatabox runs two completely isolated environments, and every path in this reference lives under /api/v1 on both.
- Live (production)
https://api.zatabox.comwithvt_live_keys. Real money, real tickets, real payouts. - Sandbox (test)
https://sandbox.zatabox.comwithvt_test_keys. A full mirror of the API same endpoints, same behaviour backed by its own isolated database. No real money moves and nothing you do here can ever touch production data.
Environment fencing is enforced server-side: a vt_test_ key sent to Live (or a vt_live_ key sent to Sandbox) is rejected with 403 WRONG_ENV rather than a quiet mistake. Switching environments is just the base URL and the key prefix nothing else in your integration changes.
The same host also serves the agent surface: the MCP server is a mount on this API at https://api.zatabox.com/mcp, not a separate service, so it shares this base URL, these credentials and the rate limits below.
LIVE · PRODUCTION API https://api.zatabox.com vt_live_ keys · real money, real tickets, real payouts SANDBOX · TEST API https://sandbox.zatabox.com vt_test_ keys · isolated data, no real money Portal https://tester.zatabox.com sandbox control portal (manage keys, logs, metrics) Every path lives under /api/v1 on BOTH API hosts. The two environments arefully isolated sandbox data is never your production data.Authentication#
All requests authenticate with a bearer token. First-party clients send the JWT issued at login; servers send an API key vt_live_ for production, vt_test_ for test mode. Buyers never set a password: buying needs only a full name and email (the account is created automatically), and logging back in is an emailed 6-digit code via /auth/token/request + /auth/token/exchange. Guest orders also return a per-order access token for reading that order without logging in.
# First-party (a user session)curl https://api.zatabox.com/api/v1/users/me \ -H "Authorization: Bearer eyJhbGciOi…" # Server-to-server (an API key)curl https://api.zatabox.com/api/v1/events \ -H "Authorization: Bearer vt_live_…" # Buyer login no passwords, an emailed 6-digit codecurl -X POST https://api.zatabox.com/api/v1/auth/token/exchange -d '{"email":"[email protected]","code":"482913"}'API keys are scoped. Grant the narrowest set that does the job: events:read · events:write · tickets:read · tickets:write · orders:read · orders:write · attendees:read · attendees:write · checkin:write · payouts:read · payouts:write · webhooks:manage · analytics:read. The wildcard * exists but is admin-only.
Errors#
Failures always arrive in the same envelope. Log meta.request_id quote it to support and we can find the exact request.
{ "error": { "code": "TICKET_SOLD_OUT", "message": "Not enough inventory left on tkt_8f2k.", "details": null }, "meta": { "request_id": "req_…" }}VALIDATION_ERRORthe body or query string failed validation.UNAUTHORIZEDmissing, expired or malformed credentials.FORBIDDENvalid credentials, insufficient scope.NOT_FOUNDno such resource, or not yours to see. Resource-specific variants exist:ORDER_NOT_FOUND,EVENT_NOT_FOUND,TICKET_TYPE_NOT_FOUND.TICKET_SOLD_OUTthe requested quantity is no longer available.EXCEEDS_MAX_PER_ORDER/EXCEEDS_MAX_PER_CUSTOMERover a ticket type's purchase caps.PRESALE_CODE_REQUIREDthe ticket type is gated behind a presale code.IDEMPOTENCY_KEY_REUSEDsame key, different body. Returned as a409. Its siblingIDEMPOTENCY_IN_FLIGHTmeans a concurrent duplicate is still running.RATE_LIMITEDover the limit; honorRetry-After.VELOCITY_LIMIT_EXCEEDEDpurchase velocity checks tripped.PROVIDER_NOT_CONFIGUREDthat payment provider has no credentials on this deployment.WRONG_ENVa key sent to a deployment pinned to the other environment. Returned as a403.
Pagination#
List endpoints paginate with cursors: pass ?limit=20&cursor=…, read nextCursor from the response, and feed it back until it comes back null. Cursors are opaque treat them as tokens, not offsets.
{ "data": { "items": [ { "id": "evt_…", "slug": "friday-salsa-night", "status": "published" } ], "nextCursor": "…" }}Idempotency#
Send an Idempotency-Key header any UUID on every POST, PUT, PATCH and DELETE. For 24 hours, replaying the same key returns the cached original response instead of running the write again, so a retried request can never double-charge or double-create.
POST /api/v1/orders HTTP/1.1Idempotency-Key: 6f2c1e8a-4b0d-4c5f-9a37-58e21cd9b144Rate limits#
- 120 requests/min public reads.
- 20 requests/min auth endpoints (login, register, code requests).
- 30 requests/min strict writes (orders, payments and the like).
- 120 requests/min check-in scanning.
- 300 requests/min inbound provider webhooks (the /payments/webhook/* receivers). Webhook management writes use the 30/min strict tier.
- 600 requests/min admin.
- MCP tool calls hit these same endpoints and inherit their limits there is no separate MCP quota.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 RATE_LIMITED adds Retry-After back off for that many seconds rather than guessing.
Product types#
Every event carries a read-only productType facet the server derives from category. category stays the single writable, type-defining field (set it on create/update); productType is a computed discriminator that appears on every event payload and as a filter on the list and search endpoints it is never accepted on a write.
eventhappens at a fixed time, attend-at-a-moment (music, sports, conference, webinar, …). The default bucket. Requires a realendDate.reservationper-date booking against a rolling window (resorts, restaurants, healthcare).category === "reservations". Dates are server-managed: the public payload reportsstartDate/endDateasnulland abookingOpenflag, and availability comes from a dedicated endpoint.digitalinstant-access on purchase, no fixed attend moment. Categoriesonline-course,digital-product,e-books,downloadable-filesandonline-service.endDateis optional omit it for an always-available product;capacitymay be null for unlimited stock.online-courseis the one flavor withindigitalthat has its own?course=truefilter and the course APIs below.
Filter any listing with ?productType=event|reservation|digital (and ?course=true to narrow digital to just courses) on GET /events, GET /search and GET /organizer/events.
Endpoints#
Path parameters appear in braces. Organizer routes require an organizer JWT or an API key with the matching write scope; buyer routes under /users/me require that buyer's token. Rows with a chevron expand to the full field reference fields are JSON body fields unless tagged query, path or header, and req marks the ones you must send.
Auth
POST/api/v1/auth/registerRegister an account
Returns 201 with the user plus an accessToken / refreshToken pair.
| Field | Description | |
|---|---|---|
emailreq | string | Account email unique per deployment. |
passwordreq | string | 8–128 characters. |
firstName | string | Optional, 1–100 characters. |
lastName | string | Optional, 1–100 characters. |
phone | string | Optional, up to 20 characters. |
organizationName | string | Optional. Defaults to “<First Last>’s Events” so every account can organize without an onboarding step. |
POST/api/v1/auth/loginLog in returns a JWT
Returns an accessToken / refreshToken pair. If the account has 2FA enabled it instead returns { requires2fa: true, challengeToken, email } complete the login with POST /auth/2fa-verify.
| Field | Description | |
|---|---|---|
emailreq | string | Account email. |
passwordreq | string | Account password. |
POST/api/v1/auth/2fa-verifyComplete a 2FA login challenge
Step 2 of a 2FA login. Returns the same accessToken / refreshToken pair as /auth/login.
| Field | Description | |
|---|---|---|
challengeTokenreq | string | The short-lived challenge token returned by /auth/login when 2FA is enabled. |
codereq | string | The 6-digit TOTP code from the authenticator app, or a one-time recovery code. |
POST/api/v1/auth/token/requestEmail a buyer a 6-digit login code
| Field | Description | |
|---|---|---|
emailreq | string | Where the 6-digit code is sent. |
name | string | Used to create the account if the email is new buyers get an account on first contact. |
POST/api/v1/auth/token/exchangeExchange email + code for a JWT
Returns the same JWT pair as /auth/login no password ever exists.
| Field | Description | |
|---|---|---|
emailreq | string | The email the code was sent to. |
codereq | string | The 6-digit code from the email. |
POST/api/v1/auth/refreshRefresh an expired access token
Mints a fresh accessToken (and a rotated refreshToken) when the access token expires no re-login needed.
| Field | Description | |
|---|---|---|
refreshTokenreq | string | The long-lived refresh token from a prior login/register. |
POST/api/v1/auth/logoutLog out
Revokes the refresh token so it can no longer mint new access tokens.
| Field | Description | |
|---|---|---|
refreshToken | string | The refresh token to invalidate. |
Organization
GET/api/v1/organizer/organizations/{id}Get organization details & wallets
Returns { organization, wallets }. organization carries id, publicId, name, slug, description, website, contactEmail, contactPhone, logoUrl, type, verified, status, socialLinks, pageTheme and createdAt. wallets is one entry per currency (USD/NGN/ZAR), each with currency, symbol, balance, pendingBalance, lifetimePayout and status the organization's live balances.
| Field | Description | |
|---|---|---|
idreq | string · path | Organization id (numeric). With an org-scoped API key it must match the key's organization. |
Events public
GET/api/v1/eventsList and search public events
Returns published, public events as discovery cards. Every card carries the read-only productType facet alongside category. The same q / category / productType / course / city / date filters work on GET /search.
| Field | Description | |
|---|---|---|
q | string · query | Free-text search across title and short description. |
category | string · query | Category slug, matched exactly. Open set any organizer slug, e.g. music, conference, workshop, sports, tech, digital-product, online-course, webinar, reservations. category stays the writable type-defining field. |
productType | enum · query | Filter by the read-only product-type facet: event, reservation or digital. Server-derived from category (see “Product types” above) it is not a writable field. Combine with category for finer scoping. |
course | bool · query | course=true narrows the digital listing to just online courses (category online-course) the one flavor within the digital product type that gets its own filter. |
city | string · query | Case-insensitive contains match on venue city. |
country | string · query | ISO 3166-1 alpha-2, e.g. NG, US. |
venue | string · query | Case-insensitive contains match on venue name. |
date_from | datetime · query | ISO 8601. Without date filters, only upcoming events are returned reservations and open-ended digital products/courses stay listed regardless (they have no fixed date). |
date_to | datetime · query | ISO 8601 upper bound on start date. |
sort | enum · query | date (default). popularity and relevance are accepted but currently fall back to date ordering. |
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | 1–100, default 20. |
GET/api/v1/events/{slug}Event detail
Carries category plus the read-only productType facet (event | reservation | digital), organizer info, schedule, gallery, highlightVideoUrl, returnPolicy and active ticket types (each with transferable / refundable flags and a digitalAccess boolean). branding is whitelisted to a public-safe subset courseType, certificateTemplate and requireIdCheck only plus, for online-course events, a public syllabus (per lesson: order, title, description, duration, skillLevel, format only). Paid lesson internals (video/PDF links, materials, writeups, PINs) and the exam are NEVER on this payload. Reservation events return a reservation config block (mode, slots, maxAdvanceDays) and null start/end dates; digital/course products may also have null endDate (always-available). Buyer-only delivery links (a ticket’s accessUrl, and onlineLink for digital-product / e-books / downloadable-files / online-course / online-service categories) are never returned here. Private events 404 to anonymous callers but stay visible to their own organization authenticate with an organizer JWT or the org’s API key.
| Field | Description | |
|---|---|---|
slugreq | string · path | Event slug from a list response. |
GET/api/v1/public/events/by-id/{eventId}/availabilityReservation availability calendar
Reservations category only. Per-date remaining counts, clamped to the bookable window [tomorrow, today+maxAdvanceDays] (hard cap 90 days) in the event timezone. Returns { mode, windowStart, windowEnd, days: [{ date, remaining, soldOut, slots?: [{ slotId, label, startTime, endTime, remaining }] }] } in date_time mode each day also lists per-slot remaining. Feeds the checkout calendar before the ticket-quantity step. 400 NOT_A_RESERVATION_EVENT for a non-reservation event.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
from | string · query | Optional range start, YYYY-MM-DD clamped into the bookable window. |
to | string · query | Optional range end, YYYY-MM-DD clamped into the window. |
Venue seating public
GET/api/v1/public/events/{eventId}/seatmapGet the seat map + per-section pricing
The seat map for an event with interactive seating: the canvas size, every section (with its ticketTypeId + per-seat price), every seat (id, label, x/y coordinates, kind incl. wheelchair/companion) and decorative elements (stage, text labels). Render it as a clickable SVG and resolve a clicked seat to its section for the price + ticket type. Events without seating return 404 NO_SEATING. Venue maps are built by Zatabox staff and are not part of this API.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
GET/api/v1/public/events/{eventId}/seatsLive seat availability snapshot
Live availability snapshot: { sold, blocked, held } arrays of seat ids. sold = purchased or held in a live order; blocked = withheld by the organizer; held = temporarily held by other buyers mid-selection. Any seat id not in these lists is available. For a continuous feed use the SSE stream below.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
SSE/api/v1/public/events/{eventId}/seats/liveLive availability stream (SSE)
Server-Sent Events. On connect it emits a snapshot event with the full availability, then seat_update events as seats change ({ sold | held | released | blocked | unblocked: [seatId,…] }), plus a periodic snapshot refresh. Use the browser EventSource API; reconnection is automatic.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
POST/api/v1/public/events/{eventId}/seats/holdHold (replace) the buyer's seat selection
Places (or replaces) temporary holds while the buyer selects. Returns { held: [...], failed: [...] } a seat already sold/blocked/held by someone else is skipped into failed. Holds expire automatically (default 5 minutes). Holds are a UX convenience: the authoritative double-booking guard is at order creation, so always handle a 409 SEAT_TAKEN from POST /orders.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
holderreq | string | An opaque token you generate once per checkout session (8–80 chars) and reuse on every hold call. |
seatIdsreq | string[] | The buyer's full current seat selection (up to 50). This is a REPLACE operation: seats you previously held that are absent here are released. |
DELETE/api/v1/public/events/{eventId}/seats/holdRelease seat holds
Releases a holder's seat holds. Returns { released: <count> }.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event public UUID or numeric id. |
holderreq | string | The same opaque token used to acquire the holds. |
seatIds | string[] | Specific seats to release. Omit to release all of this holder's seats (e.g. when the buyer leaves the picker). |
Venue seating organizer
GET/api/v1/organizer/venuesBrowse attachable venue maps
Browse the published, reusable venue seat maps you can attach to an event. Each venue lists its published map versions with seat counts. (Venue maps themselves are built by Zatabox staff and aren't part of this API.)
| Field | Description | |
|---|---|---|
search | string · query | Filter venues by name (contains match). |
GET/api/v1/organizer/events/{id}/seatingGet the event's seating setup
The event's seating setup: the attached map (sections, seats, elements), the section→ticket-type pricing, and the blocked + sold seat ids. Returns { enabled: false } when no map is attached.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
PUT/api/v1/organizer/events/{id}/seatingAttach a venue map to the event
Attaches a published venue map so buyers pick exact seats. Only PUBLISHED maps are accepted (409 MAP_NOT_PUBLISHED). Switching to a different map after any seat has sold is rejected (409 SEATS_SOLD). Then call the pricing endpoint to put sections on sale.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
venueMapIdreq | string | Id of a PUBLISHED venue map (from GET /organizer/venues). |
mode | enum | reserved (every seat selectable, default) or ga_mixed (reserved seats + standing zones). |
holdTtlSeconds | int | How long a buyer's seat hold lasts during checkout, 60–3600 (default 300). |
DELETE/api/v1/organizer/events/{id}/seatingRemove interactive seating
Removes interactive seating from the event and clears its pricing + blocks. Rejected once any seat has sold (409 SEATS_SOLD).
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
PUT/api/v1/organizer/events/{id}/seating/pricingPrice each section against a ticket type
Maps each venue section to a ticket type. Mapped tiers are flagged seated and their inventory is sized to the seats they cover. Replaces the full pricing set for the event. 400 if a section or ticket type doesn't belong to this map/event; 409 NO_SEATING if no map is attached.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
mappingsreq | array | One entry per section you want on sale (up to 500). A section omitted here is not sellable. |
mappings[].venueSectionIdreq | string | A section id from the attached map (GET …/seating). |
mappings[].ticketTypeIdreq | string | One of the event's ticket types; its price applies to every seat in the section. |
POST/api/v1/organizer/events/{id}/seating/blocksBlock or unblock seats
Blocks or unblocks seats. Sold seats can never be blocked (409 SEAT_SOLD). Changes broadcast instantly to buyers on the live seat map.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
block | string[] | Seat ids to withhold from sale (production holds, broken seats…), up to 5000. |
unblock | string[] | Previously-blocked seat ids to release back on sale. |
Events organizer
GET/api/v1/organizer/eventsList your events
Lists the events your organization owns (any status), newest first. Use the public GET /events to browse published events.
| Field | Description | |
|---|---|---|
orgId | string · query | Scope to one organization. Required for API-key auth (the key is already org-scoped); optional for a multi-org portal session. |
q | string · query | Free-text search across title and description. |
category | string · query | Filter by category slug. |
productType | enum · query | Filter by the read-only product-type facet (event | reservation | digital). Scope your list to just events, reservations or digital products/courses without enumerating categories. Add course=true to narrow digital to online courses. |
course | bool · query | course=true narrows a digital listing to online courses (category online-course). |
city | string · query | Venue city (contains match). |
country | string · query | ISO 3166-1 alpha-2 venue country. |
venue | string · query | Venue name (contains match). |
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | 1–100, default 20. |
POST/api/v1/organizer/eventsCreate a draft
Required: title, category, startDate, timezone and venueType. endDate is required only for event-bucket categories (digital/course omit it for always-available; reservations manage it). capacity is optional (null = unlimited). The event is created in draft status; add ticket types and publish it separately. online-course events additionally enforce course-completeness on publish (see “Courses authoring”).
| Field | Description | |
|---|---|---|
titlereq | string | 3–200 characters. |
categoryreq | string | Category slug. Free-form (any string is stored); the organizer picker offers music, concerts, festival, conference, workshop, sports, theater, comedy, business, arts, food, tech, community, wedding, wellness, education, webinar, online-course, digital-product, online-service, religion, fashion, film, gaming, kids and other. |
startDatereq | datetime | ISO 8601; must be in the future. |
endDate | datetime | ISO 8601; after startDate and within 5 years of it. REQUIRED for event-bucket categories; optional/nullable for digital & online-course categories (omit → an always-available product), and server-managed for reservations. |
timezonereq | string | IANA timezone, e.g. Africa/Lagos. |
venueTypereq | enum | physical, online or hybrid. |
capacity | int | Total capacity, 1–1,000,000 (larger values are clamped). Nullable omit for unlimited (digital / online-course classes). Required in practice for physical events. |
subcategory | string | Optional finer classification, up to 100 characters. |
shortDesc | string | Up to 130 characters shown on discovery cards and social previews. |
description | string | Long-form description, 50–50,000 characters. |
tags | string[] | Up to 10 free-text tags, each ≤ 50 characters. |
visibility | enum | public (default), unlisted or private. |
coverImage | uri | Cover image URL (≤ 2048 chars). |
coverVideo | uri | Cover video URL (≤ 2048 chars). |
gallery | uri[] | Up to 20 gallery image URLs. |
doorOpenTime | datetime | When doors / access open, on or before startDate. |
venueName | string | Venue name (physical / hybrid events). |
venueAddress | string | Street address. |
venueCity | string | City powers the city search filter. |
venueCountry | string | ISO 3166-1 alpha-2. |
venueLat | number | Latitude (-90…90) feeds the map and the nearby search. |
venueLng | number | Longitude (-180…180). |
onlineLink | uri | Default delivery link for online / digital events the join, download or access URL used by ticket tiers that don’t set their own. Host a file with POST /media/upload-file and pass the returned /media/<id> (numeric id) straight through, or give an absolute URL. Stored root-relative, served absolute in buyer-facing payloads. Delivered to buyers; for digital-product, e-books, downloadable-files, online-course and online-service categories it is NOT returned on the public event API. |
onlineLinkNote | string | Access password / PIN / unlock note that accompanies the default onlineLink (e.g. a zip password). ≤ 2000 chars. Delivered to buyers, never shown publicly. |
returnPolicy | string | Return / refund policy shown publicly on the event detail page. ≤ 5000 chars. |
highlightVideoUrl | uri | YouTube (or Vimeo) highlight video embedded near the top of the public event page. ≤ 2048 chars. |
currency | enum | USD (default), NGN or ZAR the event’s settlement currency. |
absorbFees | bool | When true the organizer absorbs the platform fee (deducted from payout); when false (default) the buyer pays it on top of the ticket price. |
branding | object | Free-form branding overrides (e.g. accent color, invite code). |
seoData | object | SEO overrides (meta title, description, social image). |
entities | array | Up to 80 lineup blocks. Each: kind (artist, dj, speaker, host, guest, sponsor, partner, team or feature), name, plus optional role, imageUrl, bio, setTime and link. Ignored for the reservations category, which never carries or returns entities. |
reservation | object | Reservations category only (ignored otherwise). Booking rules: mode (date_only default | date_time), dailyCapacity (-1 unlimited), maxPerCustomerPerDay (default 10), maxAdvanceDays (rolling horizon, hard cap 90), allowGroups, allowMultiDay, closedDates[] (YYYY-MM-DD blackouts), closedWeekdays[] (0=Sun…6=Sat), and slots[] ({ label, startTime, endTime, capacity?, sortOrder?, status active|paused }) slots are required for date_time mode. Reservation events are perpetual: their start/end are auto-managed. |
courseType | enum | online-course category only. unified (one learning material behind a single unlock) or syllabus (an ordered lesson list). Defaults to unified. See the “Courses authoring” group below for the full course field set. |
PUT/api/v1/organizer/events/{id}Update an event
Partial update accepts every field from create, all optional; omitted fields keep their value. Date or venue changes to a published event notify ticket holders.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
POST/api/v1/organizer/events/{id}/publishPublish a draft
No body. Fails if required fields are missing or the event has no ticket types yet.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
POST/api/v1/organizer/events/{id}/unpublishUnpublish an event
No body. Removes a published event from public listings and discovery and returns it to draft tickets and data are preserved, and it can be re-published later. Does not cancel the event or make tickets refund-eligible.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
DELETE/api/v1/organizer/events/{id}Cancel an event
Cancels the event. Drafts simply disappear; published events make issued tickets eligible for refunds.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
reason | string | Up to 2,000 characters quoted in the notification to ticket holders. |
Ticket types
PUT/api/v1/organizer/events/{id}/tickets/{ticketTypeId}Update a ticket type
Partial update accepts the same body fields as create, all optional. Quantity can't drop below the number already sold; price and type are immutable once a ticket has sold. accessUrl, accessNote, transferable and refundable can be changed anytime.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
ticketTypeIdreq | string · path | Ticket type to update. |
POST/api/v1/organizer/events/{id}/ticketsCreate a ticket type
| Field | Description | |
|---|---|---|
idreq | string · path | The event to attach the type to. |
namereq | string | 1–200 characters, e.g. “General Admission”. |
typereq | enum | general, reserved, vip, early_bird, group, free, multi_day, season, at_door or upgrade. |
saleStartreq | datetime | ISO 8601 when sales open. |
saleEndreq | datetime | After saleStart, at or before event end. |
price | number | Unit price excluding fees, 0–1,000,000, default 0. type=free requires price=0. |
currency | string | ISO 4217 3-letter code (any), e.g. USD, NGN, ZAR. Defaults to the event currency. |
quantityTotal | int | -1 for unlimited (default); otherwise the finite stock. |
maxPerOrder | int | Purchase cap per order, 1–1000, default 10. |
maxPerCustomer | int | Purchase cap per buyer, 1–1000, default 10. |
transferable | bool | Default true holders can pass tickets on. |
refundable | bool | Default false. When true, refundDeadline is required. |
refundDeadline | datetime | Last moment a refund request is accepted; at or before event start. |
presaleCode | string | 3–50 characters gates purchase behind a code (PRESALE_CODE_REQUIRED otherwise). |
waitlistEnabled | bool | Opens the waitlist when this type sells out. |
description | string | Up to 2,000 characters, shown under the type. |
accessUrl | uri | Per-tier digital delivery link (download / join / enrolment) for online & digital-product tickets. Host the deliverable with POST /media/upload-file and pass the returned /media/<id> (numeric id), or use any absolute external link. Stored root-relative, served absolute in buyer-facing payloads. Delivered to the buyer with their ticket never shown on the public page. ≤ 2048 chars, nullable. |
accessNote | string | Short note delivered alongside accessUrl (e.g. access password / PIN or instructions). ≤ 500 chars, nullable. |
sectionId | string | Reserved-seating section this type maps to, for events with a seating map. |
sortOrder | int | Display order among the event’s ticket types, default 0. |
GET/api/v1/events/{id}/ticketsList ticket types
Returns each type with live availability available is null when quantityTotal is -1 (unlimited).
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
Media upload & hosting
POST/api/v1/media/uploadUpload an image
multipart/form-data, not JSON. Resizes the image into web/thumbnail/OG variants and returns { mediaId, primaryUrl, variants }. Use primaryUrl as coverImage, gallery entries, etc. Requires an authenticated organizer JWT or API key.
| Field | Description | |
|---|---|---|
filereq | file · multipart | The image. JPEG, PNG, WebP or AVIF, up to 50 MB. Sent as multipart/form-data. |
kind | string · multipart | Asset role, e.g. event-cover or event-gallery. Defaults to event-cover. |
POST/api/v1/media/upload-fileHost a digital-product or course file
multipart/form-data, not JSON. Hosts a raw file on Zatabox and returns { mediaId, primaryUrl, filename, bytes, mime } mediaId is a numeric asset id (e.g. 501) and primaryUrl is the stable root-relative path /media/501. Drop primaryUrl into a ticket’s accessUrl, the event onlineLink, or a course lesson’s lessonPdf/lessonVideo/materials (see the media-URL callout below). 413 FILE_TOO_LARGE over the cap; 415 UNSUPPORTED_FILE_TYPE for a disallowed type; 400 EMPTY_FILE for a 0-byte file. Requires an authenticated organizer JWT or API key.
| Field | Description | |
|---|---|---|
filereq | file · multipart | The deliverable to host. Generic uploads: up to 45 MB. Allowed types: documents (pdf, doc(x), xls(x), ppt(x), odt/ods/odp, rtf, txt, csv, md), e-books (epub, mobi, azw3, fb2), archives (zip, rar, 7z, tar, gz), audio (mp3, wav, ogg, m4a, flac, aac), video (mp4, webm, mov, m4v) and images (png, jpg, webp, gif). Executable / script types (exe, js, html, svg, …) are rejected. |
field | enum · multipart | Optional type restriction used by the course wizard: pdf (application/pdf only, 45 MB cap a course “Lesson PDF” / unified PDF) or video (MP4/WebM/MOV/M4V only a “Lesson video”). Validated on BOTH extension and mime. A big upload only clears 45 MB when you send field=video: that path’s cap is 500 MB by default and is tunable per deployment. Absent → the generic 45 MB raw allowlist above. |
private | bool · multipart | The multipart form field private=1 (the literal string “1”, not a query parameter) marks the file as paid content. Use it for every deliverable you sell. Private assets are served only behind a short-lived signed URL (see the callout below), and the platform mints those links for you at read time: the learner course API signs lesson media, and a digital product’s buyer signs one download at a time via GET /users/me/tickets/{ticketId}/download. Absent / any other value → public. |
Event schedule sessions
GET/api/v1/organizer/events/{id}/scheduleList sessions
Returns { sessions: [...] } the event's running order (talks, sets, acts), each with its day, time, speaker and location, ordered by day then sortOrder.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
POST/api/v1/organizer/events/{id}/scheduleAdd a session
Adds a session to the event's public running order. Returns the created session.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
sessionTitlereq | string | 1–200 characters, e.g. “Opening keynote”. |
startTimereq | datetime | ISO 8601 when the session starts. |
endTimereq | datetime | ISO 8601 after startTime. |
dayNumber | int | Day of a multi-day event, 1–99 (default 1). |
sessionDesc | string | Up to 5,000 characters. |
speakerName | string | Up to 200 characters. |
speakerBio | string | Up to 2,000 characters. |
speakerAvatar | uri | Speaker image URL (≤ 2048 chars). |
locationDetail | string | Stage / room, up to 200 characters. |
sortOrder | int | Display order within the day, default 0. |
PUT/api/v1/organizer/events/{id}/schedule/{sessionId}Update a session
Partial update accepts every field from create, all optional; omitted fields keep their value.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
sessionIdreq | string · path | Session id. |
DELETE/api/v1/organizer/events/{id}/schedule/{sessionId}Delete a session
No body. Removes the session from the running order. Returns { deleted: true }.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
sessionIdreq | string · path | Session id. |
Event sections seating
GET/api/v1/organizer/events/{id}/sectionsList sections
Returns { sections: [...] } the named capacity blocks for the venue (GA floor, VIP deck, Balcony), ordered by sortOrder. Ticket types map to a section via their sectionId.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
POST/api/v1/organizer/events/{id}/sectionsAdd a section
Creates a seating section. Reference it from a ticket type's sectionId to sell into it. Returns the created section.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
namereq | string | 1–100 characters, e.g. “VIP deck”. |
capacityreq | int | Section capacity, 1–1,000,000. |
description | string | Up to 2,000 characters, shown in the public venue notes. |
sortOrder | int | Display order among sections, default 0. |
mapCoordinates | object | Optional overlay coordinates for a clickable seat map. |
PUT/api/v1/organizer/events/{id}/sections/{sectionId}Update a section
Partial update accepts every field from create, all optional; omitted fields keep their value.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
sectionIdreq | string · path | Section id. |
DELETE/api/v1/organizer/events/{id}/sections/{sectionId}Delete a section
No body. Fails with a 409 if any ticket type still references the section reassign those types first. Returns { deleted: true }.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
sectionIdreq | string · path | Section id. |
Event page customization & FAQ
GET/api/v1/organizer/event-customization/{id}Get page customization
Returns { customization, defaults }. customization.faqs is the “Good to know” list ([{ question, answer }]) shown on the public event page; the object also carries the page theme, colors, layout, feature toggles, social links and SEO. A row is created with defaults on first read.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
PUT/api/v1/organizer/event-customization/{id}Update page customization (incl. FAQ)
Partial update every field is optional and only the keys you send change. Returns the full { customization }. This is the endpoint that powers the “Good to know” tab in the organizer portal.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
faqs | array | The “Good to know” list up to 40 items, each { question (≤ 280 chars), answer (≤ 4000 chars) }. Send the full array; it replaces the stored one. Pass [] to clear. |
layoutPattern | enum | classic, split, gallery, minimal, magazine or festival. |
heroStyle | enum | image, video, gradient, pattern or solid. |
primaryColor | hex | Brand color, e.g. #6C5CE7 (also secondary/accent/background/text colors). |
ctaLabel | string | Override for the buy-button label, ≤ 80 chars. |
showOrganizer | bool | Feature toggles: showOrganizer, showSchedule, showVenueMap, showCountdown, showSocialShare, showPoweredBy. |
socialLinks | object | Map of platform → URL. |
seoTitle | string | SEO overrides: seoTitle, seoDescription, seoImage. |
Promo codes
GET/api/v1/organizer/promo-codesList promo codes
Returns { promoCodes: [...] }, newest first. Scoped to your organization (an API key is pinned to its own org).
| Field | Description | |
|---|---|---|
eventId | string · query | Filter to one event (public id or numeric id). Omit to list every code in your organization. |
POST/api/v1/organizer/promo-codesCreate a promo code
Returns 201 with the created code. 409 CODE_ALREADY_EXISTS if the value is taken for that event; 404 EVENT_NOT_FOUND if eventId isn’t in your org; 409 EVENT_CANCELLED for a cancelled event.
| Field | Description | |
|---|---|---|
codereq | string | 3–50 chars, letters/numbers/dash/underscore. Stored upper-cased; unique per event (or org-wide when eventId is omitted). |
discountTypereq | enum | percentage or flat. |
discountValuereq | number | > 0. For percentage, 0–100; for flat, the amount off in the order currency (≤ 100,000). |
validFromreq | datetime | ISO 8601 when the code becomes usable. |
validUntilreq | datetime | ISO 8601 must be after validFrom. |
eventId | string | Scope the code to one event (public id or numeric id; must belong to your org). Omit for an org-wide code that works on any of your events. |
description | string | Up to 200 characters, shown to your team. |
maxUses | int | Total redemption cap, 1–1,000,000. Unlimited when omitted. |
minOrderValue | number | Minimum order subtotal before the code applies. Default 0. |
applicableTypes | array | Ticket-type ids the code is limited to. Omit to allow every type. |
PUT/api/v1/organizer/promo-codes/{id}Update a promo code
Partial update send only the fields you want to change. Returns the updated code.
| Field | Description | |
|---|---|---|
idreq | string · path | Promo code id. |
code | string | New value. Cannot be changed once the code has been redeemed (409 CODE_LOCKED). |
discountType | enum | percentage or flat. |
discountValue | number | > 0; ≤ 100 for percentage. |
description | string | Up to 200 characters. |
maxUses | int | Total redemption cap, or omit/null for unlimited. |
minOrderValue | number | Minimum order subtotal. |
validFrom | datetime | ISO 8601. |
validUntil | datetime | ISO 8601 must stay after validFrom. |
applicableTypes | array | Ticket-type ids to limit the code to. |
DELETE/api/v1/organizer/promo-codes/{id}Delete or disable a promo code
Unused codes are deleted ({ deleted: true }). A code that has already been redeemed is disabled instead of deleted ({ disabled: true, promoCode }) so historical orders stay intact.
| Field | Description | |
|---|---|---|
idreq | string · path | Promo code id. |
POST/api/v1/tickets/promo/validateValidate a promo code
Read-only preview used at checkout does not consume a use. Returns { valid, discount, reason? }; reasons include CODE_NOT_FOUND, NOT_YET_ACTIVE, EXPIRED, MAX_USES_REACHED, BELOW_MIN_ORDER and NOT_APPLICABLE_TO_THESE_TICKETS. Pass the same code as promoCode when creating the order to apply it.
| Field | Description | |
|---|---|---|
codereq | string | The promo code to check. |
eventId | string | Event public id, slug or numeric id matches event-scoped and org-wide codes. |
ticketTypeIds | array | Ticket-type ids in the cart, for codes restricted via applicableTypes. |
subtotal | number | Cart subtotal, used to compute the discount and enforce minOrderValue. |
Orders & payments
POST/api/v1/ordersCreate an order guest checkout needs only name + email
Returns 201 with the order plus a per-order accessToken for guest reads. Free orders complete instantly tickets are issued at creation. For seated events, pass items[].venueSeatIds (see the Venue seating section). Reservation lines require items[].reservationDate (and reservationSlotId in date_time mode). When a paid order settles, the buyer is emailed their tickets; digital-product and online-course purchases additionally receive an invoice email with the delivery/enrolment link a behavioral note, not a separate API call.
| Field | Description | |
|---|---|---|
Idempotency-Key | uuid · header | Any UUID makes retries safe (see Idempotency above). |
itemsreq | array | 1–20 line items, one per ticket type. |
items[].ticketTypeIdreq | string | The ticket type to buy. |
items[].quantityreq | int | 1–100, within the type’s purchase caps. |
items[].venueSeatIds | string[] | Interactive seating only. The exact seat ids being bought on this line its length must equal quantity, and every seat must belong to a section priced to this ticketTypeId (see GET /public/events/{eventId}/seatmap). If a seat was taken between selection and checkout the order fails 409 SEAT_TAKEN re-fetch availability and retry. Ignored for non-seated ticket types. |
items[].reservationDate | string | Reservations category only. Chosen day, YYYY-MM-DD, within the rolling booking window. |
items[].reservationSlotId | string | Reservations only required when the event's reservation mode is date_time. |
items[].attendeeName | string | Per-ticket holder name when it differs from the buyer. |
items[].attendeeEmail | string | Per-ticket holder email. |
guestEmail | string | Guest checkout where tickets and the receipt go. Creates the account on first contact. |
guestName | string | Name on the order, up to 200 characters. |
promoCode | string | Applied before totals, up to 50 characters. |
presaleCode | string | Unlocks presale-gated ticket types. |
GET/api/v1/orders/{id}Get an order
| Field | Description | |
|---|---|---|
idreq | string · path | Order id. |
token | string · query | Per-order access token from create guest reads without a session. |
POST/api/v1/orders/{id}/payPay provider: nowpayments (crypto), paystack or flutterwave
nowpayments returns the generated crypto deposit details (payAddress, payAmount, payCurrency, network, payinExtraId); the redirect providers return an authorizationUrl to open. 409s: ALREADY_PAID, ORDER_NOT_PAYABLE, and NOTHING_TO_PAY for free orders.
| Field | Description | |
|---|---|---|
idreq | string · path | Order id. |
provider | enum | nowpayments (crypto, default), paystack or flutterwave. |
payCurrency | string | For nowpayments only the crypto coin ticker to generate a deposit for (e.g. btc, eth, sol, usdttrc20, usdcbsc). Defaults to btc. Call GET /payments/crypto/currencies for the live list. |
token | string | Guest access token when the order was created without a session. |
POST/api/v1/payments/verifyActively verify a payment no inbound webhook needed
Confirms the charge with the provider server-side and issues tickets. Idempotent and poll-safe call it after the buyer returns from checkout, or on an interval until status is completed.
| Field | Description | |
|---|---|---|
orderIdreq | string | The order to verify. |
token | string | Guest access token, if applicable. |
GET/api/v1/payments/{orderId}Read payment / order status
Returns the order status, total, currency and the list of payment attempts (provider, status, amount). Read-only use /payments/verify to actively confirm and issue tickets.
| Field | Description | |
|---|---|---|
orderIdreq | string · path | The order to read payment status for. |
token | string · query | Guest access token when the order has no session owner. |
GET/api/v1/payments/crypto/currenciesList supported crypto coins
Returns the live list of crypto coins NOWPayments can generate a deposit for (ticker, label, symbol) the source for the payCurrency value on /orders/{id}/pay. Cached ~10 minutes.
POST/api/v1/orders/{id}/cancelCancel an unpaid order
Pending / processing orders only releases held inventory. Paid orders go through the refund flow instead.
| Field | Description | |
|---|---|---|
idreq | string · path | Order id. |
token | string | Guest access token, if applicable. |
POST/api/v1/events/{eventId}/issueIssue tickets you sold elsewhere (developer-handled payment)
Fee: a 3% platform fee on the ticket face value is deducted from your org wallet for every NON-FREE (paid) ticket free tickets (price 0) incur NO fee and never touch the wallet. This is the reduced developer rate; paid orders bought through Zatabox checkout pay 5%. Flow: you collected the payment yourself, so we mint the tickets and a completed (externally-paid) order, then debit the 3% in the ticket currency fund the wallet first (Wallet → Fund). The whole issuance fails atomically with 402 INSUFFICIENT_FUNDS if the wallet can't cover the fee (nothing is minted). Honours the Idempotency-Key header so a retry never double-issues. Returns 201 with the order, the minted tickets and the exact fee charged.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | The event's public id. The API key must belong to its organization. |
itemsreq | array | [{ ticketTypeId, quantity, attendeeName?, attendeeEmail? }] the ticket types and counts to issue. |
buyer | object | { email?, name? } the recipient. With an email we create a passwordless account so the tickets land in their wallet. |
reference | string | Your own payment/order id, stored and echoed back for reconciliation. |
sendEmail | boolean | Email the buyer their tickets. Defaults to true when an email is supplied. |
Check-in
POST/api/v1/checkin/event/{id}/manualManual check-in
Admits a ticket by code without scanning a QR same validation and idempotency as /checkin/scan.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
ticketCodereq | string | Full ticket code, or the 6-char short code printed on the ticket PDF for no-camera manual entry. 6–50 chars. |
gateName | string | Gate/door this admission happened at, for attribution. |
deviceId | string | Optional device identifier. |
GET/api/v1/checkin/event/{id}/gate/{gate}Gate check-in stats
Live check-in stats (scanned, admitted, denied) for a single gate.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
gatereq | string · path | Gate/door name to scope the stats to. |
POST/api/v1/checkin/scanValidate a QR, barcode or door code
Denials are 200s, not errors: status comes back success or denied_duplicate, denied_cancelled, denied_expired, denied_wrong_event, with a deniedReason field.
| Field | Description | |
|---|---|---|
qrDatareq | string | The rotating HMAC-signed QR payload or a typed 6-character door code through the same field. |
eventIdreq | string | The event being scanned UUID or numeric id. |
gateName | string | Which gate, for per-gate stats. |
deviceId | string | Scanning device identifier. |
method | enum | qr_scan (default), barcode, manual or nfc. |
geoLat / geoLng | number | Optional scan location. |
GET/api/v1/checkin/event/{id}/manifestHashed guest-list manifest for offline scanning
Ticket hashes + statuses a gate device caches locally, so it keeps admitting with zero connectivity.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
since | datetime · query | ISO 8601 returns only tickets changed since then, for delta sync. |
POST/api/v1/checkin/batchSync queued offline scans
| Field | Description | |
|---|---|---|
eventIdreq | string | The event the scans belong to. |
scansreq | array | Up to 500 queued offline scans. |
scans[].qrData | string | One of qrData, ticketCode or shortCode is required per scan. |
scans[].ticketCode | string | Full ticket code, 6–50 characters. |
scans[].shortCode | string | The 6-character door code. |
scans[].scannedAtreq | datetime | Capture time with offset each scan is re-validated against it. |
scans[].gateName | string | Gate at capture time. |
scans[].deviceId | string | Device that captured the scan. |
GET/api/v1/checkin/event/{id}/statsCheck-in totals
Totals, capacity %, entry rate and per-gate breakdown. Per-gate slice: GET /checkin/event/{id}/gate/{gate}.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
SSE/api/v1/checkin/event/{id}/liveLive check-in stream (SSE)
Server-Sent Events a stats snapshot every 2 seconds, keep-alive comments every 30. Point an EventSource at it.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
Community
POST/api/v1/community/reviews/{id}/replyReply to a review
Posts the organizer's public reply to an attendee review.
| Field | Description | |
|---|---|---|
idreq | string · path | Review id. |
bodyreq | string | The organizer's public reply, 1–2000 characters. |
GET/api/v1/community/events/{eventId}/waitlistList the waitlist
Lists the waitlist entries for an event (organizer view).
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event id. |
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | 1–100, default 20. |
POST/api/v1/community/events/{eventId}/waitlist/offerOffer waitlist spots
Offers freed-up inventory to the next people on the waitlist each gets a time-limited claim link by email.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event id. |
countreq | int | How many of the oldest waiting entries to send a purchase offer to. |
POST/api/v1/community/reviewsReview an event checked-in ticket holders only
Only checked-in tickets can review the pair ticketCode + email is the proof, no login needed.
| Field | Description | |
|---|---|---|
ticketCodereq | string | The code on the ticket proves attendance passwordlessly. |
emailreq | string | Must match the ticket holder’s email. |
ratingreq | int | 1–5 stars. |
bodyreq | string | 10–2,000 characters. |
authorName | string | Display name shown next to the review, up to 120 characters. |
POST/api/v1/community/orgs/{orgId}/followFollow an organizer
Re-subscribes a prior opt-out; every announcement email carries a one-click unsubscribe link.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | Organization UUID, numeric id or slug. |
emailreq | string | Where new-event announcements go. |
name | string | Subscriber name, up to 120 characters. |
GET/api/v1/community/orgs/{orgId}/followersList subscribers organizer
Organizer auth (portal session or org-scoped API key). Lists the org's subscribers with id, email, name, status and createdAt.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | Organization UUID, numeric id or slug. |
cursor | string · query | Opaque pagination cursor from the previous page. |
limit | int · query | 1–100, default 20. |
DELETE/api/v1/community/orgs/{orgId}/followers/{followerId}Remove a subscriber organizer
Organizer auth, write-level access. Hard-deletes the subscriber the compliant response to a removal request: their email/name are purged (not just marked unsubscribed), and the slot frees so they may follow again later. Returns { removed: true }.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | Organization UUID, numeric id or slug. |
followerIdreq | string · path | Subscriber id from the list endpoint. |
POST/api/v1/community/events/{eventId}/waitlistJoin a waitlist offers fire on cancellations
No payment at join time. When inventory frees up, the next entries get a time-limited purchase link.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event UUID, numeric id or slug. |
emailreq | string | Where the offer email goes. |
namereq | string | 1–120 characters. |
ticketTypeId | string | Wait for a specific ticket type instead of any. |
Growth organizer
DELETE/api/v1/organizer/growth/tagsRemove an attendee tag
Removes one tag from one attendee. Create/apply tags with POST /organizer/growth/tags.
| Field | Description | |
|---|---|---|
orgIdreq | string | Organization id (omit for API-key auth the key is already org-scoped). |
ticketIdreq | string | The ticket/attendee to untag. |
tagreq | string | The tag to remove. |
GET/api/v1/organizer/events/{id}/analyticsEvent analytics
Sales, revenue, check-in and conversion analytics for one event. Requires the analytics:read scope.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
GET/api/v1/organizer/events/{id}/attendeesList event attendees
The attendee list (holder name/email, ticket type, check-in status) for an event. Requires the attendees:read scope.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | 1–100, default 20. |
POST/api/v1/organizer/growth/events/{eventId}/compsBulk-mint and email comp tickets
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event UUID or numeric id. |
ticketTypeIdreq | string | The type to mint from comps draw down its remaining quantity. |
recipientsreq | array | 1–200 entries; each gets a real ticket by email at no charge. |
recipients[].emailreq | string | Recipient email. |
recipients[].name | string | Recipient name. |
note | string | Internal note on the batch, e.g. “press list”. Up to 500 characters. |
POST/api/v1/organizer/growth/events/{eventId}/comps/import-csvImport attendees from CSV
Returns imported and skipped counts rows with invalid emails are skipped, not fatal.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event UUID or numeric id. |
ticketTypeIdreq | string | The type each imported attendee receives. |
csvreq | string | Raw CSV text header row must contain name and email columns (any order), up to 500 data rows. |
POST/api/v1/organizer/growth/events/{eventId}/broadcastEmail a broadcast with reply threads
Sends real email to every matching attendee; replies thread back to the organizer inbox.
| Field | Description | |
|---|---|---|
eventIdreq | string · path | Event UUID or numeric id. |
subjectreq | string | Up to 200 characters. |
bodyreq | string | Plain text or simple HTML, up to 5,000 characters. |
tagFilter | string | Only send to attendees whose ticket carries this tag. |
POST/api/v1/organizer/growth/tagsTag attendees
Additive existing tags stay. Tags power broadcast tagFilter and CRM segments. DELETE the same path removes one.
| Field | Description | |
|---|---|---|
orgIdreq | string | Organization UUID or numeric id. |
ticketIdsreq | array | 1–500 ticket ids (from the attendee list). |
tagreq | string | Short lowercase label up to 60 characters, e.g. “vip”, “press”. |
Customers CRM
The customers API aggregates the distinct buyers behind an organization's completed orders folded across events, reservations and digital products and keyed on the buyer's normalized email. It is the non-event counterpart to the attendee list: event buyers surface as attendees, everyone else as customers. Auth is a portal session or an org-scoped API key. The {key} is an opaque base64url token (the encoded email no raw address ever appears in a path or log); always pass the value the list endpoint gave you.
Customers CRM (organizer)
GET/api/v1/organizer/customersList customers (buyer CRM)
Returns { customers: [...], pagination: { cursor, has_more }, meta: { total, scanned, truncated } }. Each customer carries key, email, name, orderCount, lifetimeSpend (per currency, net of refunds), productTypeCounts, first/lastPurchaseAt and tags. The aggregation windows the org's most recent revenue orders meta.truncated true means the tail may be omitted (raise the window by paging). 400 if orgId is missing; 403 if the caller may not access the org.
| Field | Description | |
|---|---|---|
orgIdreq | string · query | Organization public UUID or numeric id. An org-scoped API key must match it. |
q | string · query | Filter by email OR name (case-insensitive substring). |
cursor | string · query | Opaque cursor from the prior page's pagination.cursor. |
limit | int · query | 1–100, default 25. |
GET/api/v1/organizer/customers/{key}Get a customer's CRM profile
The customer summary plus full purchase history: orders[] (with items + tickets and a per-line productType), refunds[], ticketCount and private notes[]. 404 CUSTOMER_NOT_FOUND if the key has no completed purchases in the org.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque base64url customer key from the list endpoint. |
orgIdreq | string · query | Organization id. |
GET/api/v1/organizer/customers/{key}/notesList private notes
Returns { notes: [...] } private, org-internal notes on the customer, newest first. Never shown to the buyer.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
orgIdreq | string · query | Organization id. |
POST/api/v1/organizer/customers/{key}/notesAdd a private note
Creates a private note (201). Standard session write rate limit.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
bodyreq | string | Note text, 1–5000 chars (private, org-internal). |
orgId | string | Organization id as a query param or in the body. |
PUT/api/v1/organizer/customers/{key}/notes/{noteId}Edit a private note
Edits a note. Scoped by org + customer key so it can never be re-pointed across orgs/customers. 404 NOTE_NOT_FOUND if absent.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
noteIdreq | string · path | Numeric note id. |
bodyreq | string | New note text, 1–5000 chars. |
orgId | string | Query param or body. |
DELETE/api/v1/organizer/customers/{key}/notes/{noteId}Delete a private note
Deletes a note. Returns { deleted: true }. 404 NOTE_NOT_FOUND if absent.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
noteIdreq | string · path | Numeric note id. |
orgId | string | Query param or body. |
POST/api/v1/organizer/customers/{key}/tagsTag a customer
Applies the tag to ALL of the customer's active tickets in the org the same AttendeeTag namespace that segmented broadcasts filter on. 201. 409 NO_TICKETS if the customer has no active tickets to tag.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
tagreq | string | Label, 1–60 chars normalized to trimmed lowercase. |
orgId | string | Query param or body. |
DELETE/api/v1/organizer/customers/{key}/tagsRemove a customer tag
Removes the tag from every one of the customer's tickets in the org. Returns { tag, removed }.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
tagreq | string | The tag to remove. |
orgId | string | Query param or body. |
POST/api/v1/organizer/customers/{key}/contactEmail a customer (white-label)
Sends ONE real, white-label email to the customer, branded as the organizer (org header/footer + From name). Tight strict rate limit (30/min → 429). Returns { to, subject, sent, dev }; 502 EMAIL_SEND_FAILED if delivery fails.
| Field | Description | |
|---|---|---|
keyreq | string · path | Opaque customer key. |
subjectreq | string | 1–200 chars. |
bodyreq | string | 1–5000 chars (plain text; newlines become line breaks). |
orgId | string | Query param or body. |
Digital products
A digital product is an event whose category is one of digital-product, e-books, downloadable-files or online-service all four resolve to productType: digital (the online-course flavor is the fifth digital category and gets its own authoring/learner groups below). You author one through the same POST / PUT /organizer/events endpoints as any event, with venueType: "online". Two traits make it open-ended: endDate is optional omit it (or send null) for an always-available product with no sale cutoff and capacity may be null for unlimited stock, with the real inventory sized on each ticket tier (quantityTotal: -1 = unlimited). Set a returnPolicy (shown publicly on the product page) to spell out your refund terms. The product is created in draft; add at least one ticket type, then publish.
Delivery model. Each purchased ticket resolves to a delivery link + note (PIN) with a fixed fallback order a tier's own accessUrl wins, otherwise the event's default onlineLink covers it; a tier's own accessNote wins, otherwise the default link's companion onlineLinkNote (its access PIN) applies only when the default link was used. So you can host one deliverable at the event level and let every tier inherit it, or give a tier its own file. Host the deliverable with POST /media/upload-file using private=1 and drop the returned /media/<id> URL numeric id, root-relative into either place exactly as it came back; an absolute URL is equally valid.
Digital products delivery (buyer)
GET/api/v1/users/me/tickets/{ticketId}/downloadDownload a purchased file
Authorizes ONE download. Needs the buyer’s own JWT not an API key, not an order access token and re-checks ownership on every call, so call it per click instead of caching what it returns. Answers { url, note, kind }. kind: 'file' is a freshly-signed, short-lived URL to a file Zatabox hosts, served as an attachment; kind: 'external' is the organizer’s own off-platform link, handed back verbatim for you to open. note carries the access PIN (accessNote) or null. Every failure is a 404, deliberately indistinguishable from a wrong id: TICKET_NOT_FOUND (not yours, or cancelled / refunded), NOT_DOWNLOADABLE (not a digital product an event, a reservation, or a course ticket, which delivers through the learner API instead), NO_FILE (the seller has attached no deliverable yet).
| Field | Description | |
|---|---|---|
ticketIdreq | string · path | The buyer’s own ticket id, taken from GET /users/me/tickets. Only rows flagged downloadable: true resolve to a hosted file. |
Digital products participate in the same productType facet as everything else: filter any listing with ?productType=digital on GET /events, GET /search and GET /organizer/events (add ?course=true to narrow to just online courses). See Product types above for the full facet contract.
Digital products authoring (organizer)
POST/api/v1/organizer/eventsCreate a digital product (draft)
Digital-flow subset of the create-event body see the “Events organizer → Create a draft” endpoint above for the full shared field set (tags ≤ 10, coverImage / coverVideo, gallery ≤ 20, visibility public|unlisted|private, subcategory, branding, seoData, highlightVideoUrl), all optional and documented once there. Creates the product in status: draft with productType: digital; add at least one ticket tier, then publish. Upload the deliverable first with POST /media/upload-file (below) using private=1, so onlineLink / a tier’s accessUrl can point at the returned /media/<id>.
| Field | Description | |
|---|---|---|
orgId | string | Organization to create under. Required for API-key auth (already org-scoped it may be omitted); a multi-org portal session must name the org. |
titlereq | string | 3–200 characters. |
categoryreq | string | One of the four digital-product slugs: digital-product, e-books, downloadable-files or online-service all resolve to productType: digital. (The fifth digital slug, online-course, has its own “Courses authoring” group below.) |
venueTypereq | enum | Use online for a digital product (physical / hybrid are for on-site events). A non-physical venueType is what lets the event-level onlineLink flow through to buyers. |
startDatereq | datetime | ISO 8601, must be in the future the “on sale from” moment. The product isn’t purchasable before it. |
endDate | datetime | ISO 8601, after startDate. Optional/nullable for digital omit it or send null for an always-available product with no sale cutoff (the service persists a far-future sentinel so discovery keeps showing it). |
timezonereq | string | IANA timezone, e.g. Africa/Lagos. |
capacity | int | Optional/nullable omit or null for unlimited stock (the real inventory is sized per ticket tier via quantityTotal). A positive value is clamped to 1,000,000. |
shortDesc | string | ≤ 130 characters the card / social-preview blurb. |
description | string | Long-form product description, 50–50,000 characters. |
currency | enum | USD (default), NGN or ZAR the settlement currency. |
absorbFees | bool | true → you absorb the platform fee (deducted from payout); false (default) → the buyer pays it on top of the price. |
onlineLink | uri | Default deliverable the file every tier inherits unless it sets its own accessUrl. Upload it first with POST /media/upload-file (private=1) and pass the returned /media/<id> (numeric id) here verbatim; an absolute URL is equally valid. ≤ 2048 chars. Like accessUrl this is an AUTHORING INPUT: it is stored root-relative, never returned on the public event API for a digital category, and buyers reach the bytes only through GET /users/me/tickets/{ticketId}/download. |
onlineLinkNote | string | Access PIN / password / unlock note that rides with the default onlineLink (e.g. a zip password). ≤ 2000 chars. Delivered to buyers, never public. Only applied when a tier falls back to the default link (a tier’s own accessNote overrides it). |
returnPolicy | string | Return / refund policy shown publicly on the product page. ≤ 5000 chars. |
PUT/api/v1/organizer/events/{id}Update a digital product
Partial update accepts every field from create, all optional; omitted fields keep their value. Send endDate: null to turn a dated product into an always-available one; set onlineLink / onlineLinkNote to swap the default deliverable or its PIN. A published-product change to date or delivery notifies existing buyers.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id (UUID or numeric). |
POST/api/v1/organizer/events/{id}/ticketsCreate the sellable tier
The sellable licence for the product; at least one tier is required before you can publish. Delivery resolves per buyer as: tier accessUrl → else event onlineLink; tier accessNote → else (only when the default link was used) event onlineLinkNote. That resolution happens server-side inside the download endpoint you set the inputs, you never hand the resolved link to a buyer yourself. The public event API suppresses accessUrl / accessNote and exposes only a boolean digitalAccess per active tier. saleEnd after event end → 400 INVALID_SALE_WINDOW; refundDeadline after event start → 400 INVALID_REFUND_DEADLINE.
| Field | Description | |
|---|---|---|
idreq | string · path | The digital product to attach the tier to. |
namereq | string | 1–200 characters, e.g. “Full edition”. |
typereq | enum | general, reserved, vip, early_bird, group, free, multi_day, season, at_door or upgrade general (or free for a free download) fits most digital products. |
saleStartreq | datetime | ISO 8601 when this tier goes on sale. |
saleEndreq | datetime | After saleStart, at or before the event end. For an always-available product, use the far-future endDate the service assigned. |
price | number | Unit price excluding fees, 0–1,000,000, default 0. type=free requires price=0. |
currency | string | ISO 4217 3-letter code; defaults to the event currency. |
quantityTotal | int | -1 = unlimited stock (default) the norm for a digital product; a positive value caps licences sold. |
maxPerOrder | int | Cap per order, 1–1000, default 10. |
maxPerCustomer | int | Cap per buyer, 1–1000, default 10. |
refundable | bool | Default false. When true, refundDeadline is required. |
refundDeadline | datetime | Last moment a refund is accepted; at or before event start. |
transferable | bool | Default true the buyer can pass the entitlement on. |
accessUrl | uri | Per-tier delivery override this tier’s own file. Wins over the event’s default onlineLink for buyers of this tier. Host the file with POST /media/upload-file (private=1) and pass the returned /media/<id> (numeric id) verbatim, or any absolute URL. ≤ 2048 chars, nullable. This is an AUTHORING INPUT, not a buyer-facing link: a hosted file is private at rest and the buyer’s only path to it is GET /users/me/tickets/{ticketId}/download. Never on the public payload, and null on the buyer’s wallet row for a hosted file. |
accessNote | string | Access PIN / note delivered alongside this tier’s accessUrl. ≤ 500 chars, nullable. A tier’s own accessNote always wins; the event onlineLinkNote only fills in when the tier fell back to the default link. |
status | enum | active (default) or hidden a hidden tier isn’t sold. |
POST/api/v1/media/upload-fileHost the deliverable (cross-reference)
Cross-reference full field set, size caps and error codes live in the “Media upload & hosting” group above. multipart/form-data. Returns { mediaId, primaryUrl, filename, bytes, mime }; drop primaryUrl (the root-relative /media/<id>, numeric id) into the event onlineLink or a tier’s accessUrl exactly as returned both fields also accept an absolute URL, and both come back absolute on buyer-facing payloads.
| Field | Description | |
|---|---|---|
field | enum · multipart | Optional type restriction: pdf (application/pdf, 45 MB) or video (MP4/WebM/MOV/M4V, 500 MB by default and tunable per deployment). Only field=video raises the cap past 45 MB. Absent → the generic 45 MB raw allowlist (documents, e-books, archives, audio, video, images). |
private | bool · multipart | The multipart form field private=1 marks the file as paid content served only behind a short-lived signed URL. Use it for the deliverable so the raw /media/<id> isn’t publicly fetchable. |
PUT/api/v1/organizer/events/{id}/statusPublish (go live)
Transitions the product’s status (the same publish gate as POST /organizer/events/{id}/publish above a draft with no ticket tier can’t go live). Once published the product appears on the public page and in discovery with instant-access framing: each active tier shows a “delivered on purchase” hint, and a completed purchase emails the buyer the delivery link and PIN.
| Field | Description | |
|---|---|---|
idreq | string · path | Event id. |
statusreq | enum | Target status: draft, review, published, on_sale, sold_out, in_progress, completed or cancelled. Send published to take a ready draft live. |
Example. Host a PDF privately, create the always-available product (endDate: null, capacity: null), then add one unlimited tier that overrides delivery with its own accessUrl. Note that both requests send primaryUrl exactly as the upload returned it the root-relative /media/501, no rewriting required.
# 1 · Host the deliverable (multipart, private)curl https://api.zatabox.com/api/v1/media/upload-file \ -H "Authorization: Bearer vt_live_…" \ -F field=pdf -F private=1 \ -F file=@/path/to/growth-playbook.pdf# → { "mediaId": "501", "primaryUrl": "/media/501", "bytes": 4210233, "mime": "application/pdf" }# Media ids are numeric. Pass primaryUrl through as-is root-relative is what the API stores. # 2 · Create the draft digital productcurl https://api.zatabox.com/api/v1/organizer/events \ -H "Authorization: Bearer vt_live_…" \ -d '{ "orgId": "org_7Qk…", "title": "The SaaS Growth Playbook (PDF)", "category": "e-books", "venueType": "online", "startDate": "2026-08-01T00:00:00Z", "timezone": "Africa/Lagos", "endDate": null, "capacity": null, "currency": "USD", "absorbFees": true, "shortDesc": "58 pages of B2B growth tactics.", "onlineLink": "/media/501", "onlineLinkNote": "Unzip password: GROWTH-2026", "returnPolicy": "Digital goods are non-refundable once the download link is delivered." }'{ "data": { "id": "evt_5e6f7a8b", "title": "The SaaS Growth Playbook (PDF)", "category": "e-books", "productType": "digital", "venueType": "online", "status": "draft", "endDate": "2031-08-01T00:00:00Z", "onlineLink": "/media/501", "currency": "USD" }}curl https://api.zatabox.com/api/v1/organizer/events/evt_5e6f7a8b/tickets \ -H "Authorization: Bearer vt_live_…" \ -d '{ "name": "Playbook full edition", "type": "general", "price": 29, "quantityTotal": -1, "saleStart": "2026-08-01T00:00:00Z", "saleEnd": "2031-08-01T00:00:00Z", "refundable": false, "transferable": true, "accessUrl": "/media/501", "accessNote": "Unzip password: GROWTH-2026" }'{ "data": { "id": "tkt_3a1b", "name": "Playbook full edition", "type": "general", "price": "29.00", "quantityTotal": -1, "available": null, "accessUrl": "/media/501", "accessNote": "Unzip password: GROWTH-2026", "status": "active" }}Both responses are authoring reads, so they echo the stored, root-relative /media/501 the same value you sent. That is the last place this URL appears in a readable form: the buyer's wallet row will carry accessUrl: null with downloadable: true, and the actual bytes come from GET /users/me/tickets/{ticketId}/download one signed, short-lived URL at a time.
Authoring an online course
An online course is just an event with category: "online-course" (productType: digital). You author it through the same POST / PUT /organizer/events endpoints the course-specific fields below live under the event body and are persisted in the event's branding. Pick a courseType: unified (one learning material behind a single unlock) or syllabus (an ordered lesson list). Upload every PDF, video and material with POST /media/upload-file using the multipart field private=1 and pass the returned /media/<id> URL (numeric id) through unchanged; an absolute URL is accepted just as well, and both are normalized to the root-relative /media/<id> in storage. Authoring reads give you that stored form back the learner API is what hands enrolled buyers short-lived signed links.
Courses authoring (organizer)
POST/api/v1/organizer/eventsCreate a course (draft)
Creates the course in status: draft. Every course field from the PUT card below is accepted here too this table is just the minimum that gets a syllabus course through. Completeness is checked up front: an incomplete body is 400 VALIDATION_ERROR with every problem in details.issues[]. A course carries NO price create at least one ticket tier with POST /organizer/events/{id}/tickets (that tier is the enrolment), then publish. Copy-pasteable example below.
| Field | Description | |
|---|---|---|
orgId | string | Organization to create under. Optional for an org-scoped API key; a multi-org portal session must name it. |
titlereq | string | 3–200 characters. |
categoryreq | string | online-course the slug that makes this a course (productType: digital) and switches on the course field set. |
venueTypereq | enum | online for a course. |
startDatereq | datetime | ISO 8601 in UTC (trailing Z, no numeric offset) and in the future the “enrolment opens” moment. |
timezonereq | string | IANA timezone, e.g. Africa/Lagos. |
courseTypereq | enum | syllabus for an ordered lesson list, unified for one material behind a single unlock. Omitting it defaults to unified and then the unified material rule applies, which is the usual first-attempt 400. |
syllabusreq | array | syllabus courses: at least one lesson, each complete (duration + skillLevel + format + lessonPdf or lessonVideo). Full per-lesson reference on the PUT card below. |
endDate | datetime | Omit or send null for an open-ended course; the service persists a sentinel five years out so discovery keeps showing it. |
capacity | int | Omit or null for an unlimited class. |
description | string | Long-form description. If you send it at all it must be 50–50,000 characters. |
certificateTemplate | enum | cert-1 … cert-8, or null for no certificate. |
PUT/api/v1/organizer/events/{id}Author / edit a course (fields on the event body)
The course fields ride on the normal event create/update body (the full set is listed here on PUT; POST takes exactly the same fields see the create card above). They are persisted in branding; the public event API exposes only the curriculum shell (title/description/duration/skillLevel/format per lesson) + courseType + certificateTemplate. Media URLs accept /media/<id> (numeric) or an absolute URL and are normalized to /media/<id> for storage. Partial update: omitted fields keep their value, but the completeness check runs against the MERGED result whenever the edit touches courseType, syllabus, unified*, exam or onlineLink 400 COURSE_INCOMPLETE (first problem only) if the merge is incomplete. An edit that touches no course field skips the check.
| Field | Description | |
|---|---|---|
courseType | enum | unified or syllabus. Defaults to unified. Determines which field set below applies. |
unifiedPdf | uri | unified only. The course PDF, as a /media/<id> URL (upload with private=1). Nullable. |
unifiedVideo | uri | unified only. The course video, /media/<id>. At least one of unifiedPdf / unifiedVideo is required on every create and on any update that touches a course field. |
unifiedMaterials | array | unified only. Up to 10 extra downloads, each { name (1–200), url (/media/<id> or absolute) }. |
syllabus | array | syllabus only. Up to 200 ordered lessons see the per-lesson fields below. At least one lesson is required: an empty or omitted syllabus is the first thing the completeness check rejects. |
syllabus[].titlereq | string | 1–200 chars. |
syllabus[].duration | enum | Required on every lesson. One of: “1-4 hours”, “8-12 hours”, “24 hours”, “3 days”, “A week”, “2 Weeks”, “4 Weeks”, “3 months”, “6 months”. |
syllabus[].skillLevel | enum | Required on every lesson. Beginner, Intermediate, Advanced or “All levels”. |
syllabus[].format | enum | Required on every lesson. Self-paced or Live. |
syllabus[].lessonPdf | uri | Lesson PDF, /media/<id> or absolute. At least one of lessonPdf / lessonVideo is required per lesson. |
syllabus[].lessonVideo | uri | Lesson video, /media/<id> or absolute. Upload it with field=video to clear the 45 MB generic cap. |
syllabus[].materials | array | Up to 10 downloads, each { name, url }. |
syllabus[].description | string | ≤ 2000 chars. |
syllabus[].streamingPlatform | string | LEGACY ≤ 100 chars. A free-text “streamed on Zoom / YouTube Live” label from the pre-upload wizard. Still accepted on write and echoed on authoring and learner reads so old lessons round-trip, but no current authoring surface writes it and it is never public. Author lessonPdf / lessonVideo instead. |
syllabus[].writeup | string | ≤ 2000 chars. Paid lesson notes delivered only to enrolled buyers, never public. |
syllabus[].lessonPin | string | ≤ 500 chars. Access PIN delivered to buyers. |
certificateTemplate | enum | Completion-certificate design: cert-1 … cert-8, or null for no certificate. The chosen template id is public; the certificate PDF is issued to learners who complete. |
exam | object | Optional final exam: { enabled (bool), passMarkPct (1–100, default 70), questions[] }. |
exam.questions[] | array | Up to 50 questions, each { prompt (1–1000), options (2–6 strings), correctIndex (int, valid option index), explanation? (≤1000) }. correctIndex and explanation are stored but stripped from every learner/public payload. |
onlineLinkNote | string | Doubles as the unified-course access PIN (≤ 2000). Delivered to buyers, never public. |
Example. Host the lesson PDF, then create a complete one-lesson syllabus course in a single call. The lesson carries all four required pieces a duration, a skillLevel, a format and a lessonPdf so the completeness check passes on the first attempt. Drop any one of them and the create comes back 400 VALIDATION_ERROR instead.
# 1 · Host the lesson PDF (multipart paid course material)curl https://api.zatabox.com/api/v1/media/upload-file \ -H "Authorization: Bearer vt_live_…" \ -F field=pdf -F private=1 \ -F file=@/path/to/lesson-1.pdf# → { "mediaId": "612", "primaryUrl": "/media/612", "bytes": 1841022, "mime": "application/pdf" } # 2 · Create the course. A COMPLETE syllabus passes on the first call# anything missing comes straight back as 400 VALIDATION_ERROR.curl https://api.zatabox.com/api/v1/organizer/events \ -H "Authorization: Bearer vt_live_…" \ -d '{ "orgId": "org_7Qk…", "title": "Ship Your First API", "category": "online-course", "courseType": "syllabus", "venueType": "online", "startDate": "2026-09-01T00:00:00Z", "timezone": "Africa/Lagos", "endDate": null, "capacity": null, "currency": "USD", "shortDesc": "Design, build and ship a production REST API.", "description": "A hands-on course that takes you from an empty repository to a deployed, documented REST API with authentication, tests and webhooks.", "certificateTemplate": "cert-3", "syllabus": [ { "title": "Getting started", "duration": "1-4 hours", "skillLevel": "Beginner", "format": "Self-paced", "lessonPdf": "/media/612" } ] }' # 3 · Price it. A course has NO price field enrolment is a ticket tier:# POST /api/v1/organizer/events/{id}/tickets{ "data": { "id": "evt_9b3d7c1e", "title": "Ship Your First API", "category": "online-course", "productType": "digital", "venueType": "online", "status": "draft", "endDate": "2031-09-01T00:00:00Z", "courseType": "syllabus", "certificateTemplate": "cert-3", "syllabus": [ { "title": "Getting started", "description": null, "duration": "1-4 hours", "skillLevel": "Beginner", "format": "Self-paced", "streamingPlatform": null, "writeup": null, "lessonPdf": "/media/612", "lessonVideo": null, "materials": null, "lessonLink": null, "lessonPin": null } ], "unifiedPdf": null, "unifiedVideo": null, "exam": null }}The response is the organizer authoring read, so every lesson comes back in its full stored shape the keys you omitted are echoed as null, and lessonPdf keeps the root-relative /media/612 you sent. The draft is not sellable yet: add an enrolment tier with POST /organizer/events/{id}/tickets, then publish.
Taking a course (learner API)
Once a buyer owns a ticket to an online-course event, these endpoints drive the learning experience: the enrolled course list, the full lesson payload (with freshly-signed media links), lesson-completion tracking, the final exam and the completion certificate. All require the buyer's own JWT and re-verify ownership server-side a non-owner gets 403 NOT_A_BUYER on every call. The {eventId} is the numeric event id; a non-numeric segment is a clean 404 COURSE_NOT_FOUND, and a non-course event is 400 NOT_A_COURSE.
Courses learner (buyer)
GET/api/v1/courses/mineList my enrolled courses
Returns { courses: [...] } every online-course the caller has purchased, each with course meta (id, title, courseType, certificateTemplate, …) and a progress summary (totalLessons, completedLessons[], percentComplete, isComplete, certificateId, and an exam status block). Started courses sort first. No lesson content here fetch a single course for that.
GET/api/v1/courses/{eventId}Get a course (full content, signed links)
The full learner payload for one course (buyer-verified). Course meta + holderName + progress, plus the lessons. For a syllabus course, lessons[] carries each lesson with a completed flag and SIGNED, short-lived media links (lessonPdf / lessonVideo / materials) plus writeup and lessonPin these paid fields are delivered here to enrolled buyers only. For a unified course, lessons is [] and a unified block carries the signed pdf/video/materials + pin. 403 NOT_A_BUYER if the caller doesn't own a ticket.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric event id of the course. |
POST/api/v1/courses/{eventId}/lessons/{index}/completeMark a lesson complete
Syllabus courses only (400 NOT_A_SYLLABUS_COURSE otherwise). Marks the lesson complete (idempotent) and returns the updated completion state: { completed, justCompleted, examRequired, certificate, progress }. When the last requirement is met the course completes and, if a certificateTemplate is set and any exam is passed, a certificate is issued. 400 INVALID_LESSON_INDEX for an out-of-range index.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
indexreq | int · path | Zero-based lesson index (0 … totalLessons-1). |
DELETE/api/v1/courses/{eventId}/lessons/{index}/completeUn-complete a lesson
Un-marks a lesson (syllabus courses). Returns the same completion-state shape. Does not revoke an already-issued certificate.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
indexreq | int · path | Zero-based lesson index. |
POST/api/v1/courses/{eventId}/completeComplete a unified course
Unified courses only (400 NOT_A_UNIFIED_COURSE otherwise). Marks the single learning unit complete and returns { completed, justCompleted, examRequired, certificate, progress }.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
GET/api/v1/courses/{eventId}/examGet the exam (no answer key)
Returns the exam to take: { eventId, passMarkPct, total, questions: [{ id, prompt, options }], status }. Correct answers and explanations are stripped never sent to the learner. status carries attemptsUsed, bestScorePct, passed (sticky), latestScorePct etc. 404 NO_EXAM when the course has no live exam.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
POST/api/v1/courses/{eventId}/exam/submitSubmit the exam scored server-side
Grades server-side and records the attempt. Returns { scorePct, passed, passMarkPct, correctCount, total, perQuestion, status, completed, justCompleted, certificate, progress }. scorePct/passed are THIS attempt; status.bestScorePct and status.passed are best-ever and STICKY (a later failed retake never revokes a prior pass). perQuestion lists { questionId, correct } only the correct option index is NEVER revealed; a per-question explanation is included only on a passing attempt. A pass drives completion + certificate issuance when material requirements are also met. 404 NO_EXAM if there's no exam.
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
answersreq | array | [{ questionId, selectedIndex }] graded by questionId (order-independent). Not an array → 400 INVALID_ANSWERS. |
GET/api/v1/courses/{eventId}/certificateDownload my certificate (PDF)
Downloads the completion certificate as a PDF (Content-Type application/pdf, attachment). 404 CERTIFICATE_NOT_ISSUED until the course is completed (and, where an exam is required, passed).
| Field | Description | |
|---|---|---|
eventIdreq | int · path | Numeric course event id. |
Buyers
GET/api/v1/users/me/refundsList my refunds
Lists the signed-in buyer's refund requests and their statuses. Request a refund with POST /users/me/refunds.
GET/api/v1/users/me/messagesList my messages
Lists the signed-in buyer's message threads with organizers.
POST/api/v1/tickets/{id}/transferTransfer a ticket
Transfers a ticket to another person; the recipient claims it via the emailed link. Fails on non-transferable ticket types. The initiator can revoke before it's claimed.
| Field | Description | |
|---|---|---|
idreq | string · path | Ticket id to transfer. |
toEmailreq | string | Recipient email receives a claim link. |
toName | string | Recipient name (optional). |
token | string | Order access token proving ownership when not signed in as the holder. |
/api/v1/users/meProfileGET/api/v1/users/me/ticketsTicket wallet
Every live ticket the caller owns, each with a productType discriminator (event | reservation | digital), its event and tier, and for reservations the booked date/slot. Digital delivery: downloadable: true means Zatabox hosts the file, and accessUrl is null on that row by design; fetch the file with the download endpoint below. An external link rides along in accessUrl (absolutized) with downloadable: false. accessNote is the PIN either way. Course tickets are never downloadable here they open from the learner API.
| Field | Description | |
|---|---|---|
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | Page size. |
GET/api/v1/users/me/tickets/{ticketId}/downloadDownload a purchased file
Cross-reference documented in full under “Digital products → delivery (buyer)”. Re-checks ownership per call and returns { url, note, kind } with a freshly-signed, short-lived url. 404 TICKET_NOT_FOUND / NOT_DOWNLOADABLE / NO_FILE.
| Field | Description | |
|---|---|---|
ticketIdreq | string · path | A ticket id from the wallet above, flagged downloadable: true. |
GET/api/v1/users/me/exportGDPR data export
No parameters. Returns one JSON download of everything on the account: profile, orders + items, tickets, refund requests, reports and message threads.
POST/api/v1/users/me/refundsRequest a refund
Eligibility = the ticket type’s refundable flag plus its deadline; the organizer approves or denies. 409 when not eligible.
| Field | Description | |
|---|---|---|
ticketIdreq | string | The ticket to refund. |
reasonreq | string | 10–2,000 characters specific reasons fare better with organizers. |
message | string | Optional message to the organizer, up to 2,000 characters. |
evidenceUrls | array | Up to 5 supporting URLs. |
POST/api/v1/users/me/reportsFile a report
harassment and fraud route straight to platform admins; the rest go to the organizer first.
| Field | Description | |
|---|---|---|
categoryreq | enum | misleading_info, did_not_happen, harassment, fraud, accessibility or other. |
descriptionreq | string | 20–5,000 characters. |
eventId | string | One of eventId / organizationId is required. |
organizationId | string | Report an organizer rather than a single event. |
evidenceUrls | array | Up to 10 supporting URLs. |
POST/api/v1/users/me/tickets/{ticketId}/messageMessage an organizer about a ticket
Rate-limited to 10 messages per hour per organizer. Replies land in GET /users/me/messages.
| Field | Description | |
|---|---|---|
ticketIdreq | string · path | The ticket whose organizer you’re messaging. |
bodyreq | string | 1–5,000 characters. |
attachmentUrls | array | Up to 5 attachment URLs. |
Wallets
GET/api/v1/organizer/walletsList wallet balances
Lists your organization's per-currency wallet balances (balance, pending, lifetime payout).
| Field | Description | |
|---|---|---|
orgId | string · query | Organization id. Required for API-key auth; optional for a multi-org portal session. |
GET/api/v1/organizer/wallets/org/{orgId}Org wallet balances
The per-currency wallet balances for one organization (org pinned in the path).
| Field | Description | |
|---|---|---|
orgIdreq | string · path | Organization id. |
API keys
POST/api/v1/organizer/integrations/org/{orgId}/api-keysCreate an API key
The response returns the full plaintext secret EXACTLY ONCE only its prefix is stored afterwards. Send it as a Bearer token (Authorization: Bearer vt_live_…). Requires an organizer JWT (owner / admin).
| Field | Description | |
|---|---|---|
orgIdreq | string · path | The organization the key belongs to. |
namereq | string | Label for the key, 2–120 characters. |
environment | enum | live (default) → vt_live_ prefix, or test → vt_test_. A vt_test_ key is rejected on a live deployment and vice-versa. |
scopes | string[] | Up to 40 scope strings, e.g. events:write, orders:read, payouts:read, webhooks:manage. Omit for a full-access key. |
ipAllowlist | string[] | Up to 40 IPs/CIDRs; when set, requests from other addresses are rejected. |
expiresAt | datetime | Optional ISO 8601 expiry; null / omitted never expires. |
GET/api/v1/organizer/integrations/org/{orgId}/api-keysList API keys
Returns each key’s prefix, masked display, scopes, environment, status, last-used and expiry never the secret.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | The organization to list keys for. |
PUT/api/v1/organizer/integrations/org/{orgId}/api-keys/{keyId}Update a key (rename, pause, re-scope)
| Field | Description | |
|---|---|---|
orgIdreq | string · path | The organization. |
keyIdreq | string · path | The key to update. |
name | string | New label, 2–120 characters. |
status | enum | active, paused or revoked. |
scopes | string[] | Replaces the scope list. |
expiresAt | datetime | New expiry; null clears it. |
POST/api/v1/organizer/integrations/org/{orgId}/api-keys/{keyId}/rotateRotate a key’s secret
No body. Returns a new plaintext secret exactly once and invalidates the old one immediately.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | The organization. |
keyIdreq | string · path | The key to rotate. |
DELETE/api/v1/organizer/integrations/org/{orgId}/api-keys/{keyId}Revoke a key
Revokes the key it stops authenticating immediately.
| Field | Description | |
|---|---|---|
orgIdreq | string · path | The organization. |
keyIdreq | string · path | The key to revoke. |
Webhooks
POST/api/v1/webhooksCreate an endpoint
The response contains the full whsec_ signing secret exactly once it is masked on every later read. Store it immediately.
| Field | Description | |
|---|---|---|
urlreq | uri | HTTPS endpoint that receives deliveries private-network targets are rejected. |
eventsreq | array | 1–64 event types from the catalog, or ["*"] for everything. |
name | string | Friendly label, up to 120 characters. |
orgId | string | Required with a user JWT (query or body); an API key implies its own org. |
GET/api/v1/webhooksList endpoints
| Field | Description | |
|---|---|---|
orgId | string · query | Required with a user JWT; implied by an API key. |
PUT/api/v1/webhooks/{id}Update an endpoint
| Field | Description | |
|---|---|---|
idreq | string · path | Webhook endpoint id. |
url | uri | New delivery URL. |
events | array | Replaces the subscribed event list. |
name | string | New label; null clears it. |
status | enum | active or disabled pause without deleting. |
DELETE/api/v1/webhooks/{id}Delete an endpoint
Deliveries stop immediately and the signing secret is invalidated recreating issues a new one.
| Field | Description | |
|---|---|---|
idreq | string · path | Webhook endpoint id. |
POST/api/v1/webhooks/{id}/testSend a test event
No body fires a signed test event at the URL so you can verify your handler end to end.
| Field | Description | |
|---|---|---|
idreq | string · path | Webhook endpoint id. |
POST/api/v1/webhooks/{id}/rotate-secretRotate the signing secret
No body. Issues a new whsec_ signing secret and returns it exactly once (the old one stops verifying immediately). Store it right away it is masked on every later read.
| Field | Description | |
|---|---|---|
idreq | string · path | Webhook endpoint id. |
GET/api/v1/webhooks/{id}/deliveriesDelivery history
Each delivery shows event type, response status, latency, retry count and the error detail on failures.
| Field | Description | |
|---|---|---|
idreq | string · path | Webhook endpoint id. |
cursor | string · query | Opaque cursor from the previous page. |
limit | int · query | Page size. |
POST/api/v1/webhooks/deliveries/{id}/replayReplay a delivery
| Field | Description | |
|---|---|---|
idreq | string · path | Delivery id from the delivery history. |
GET/api/v1/webhooks/catalogAll 27 event types
No auth, no parameters every event type the platform can emit, for building subscription UIs.
Worked examples#
Create an order
An order holds one or more ticket types and starts life pending. Pay it with POST /api/v1/orders/{id}/pay, then confirm with POST /api/v1/payments/verify verification is an active call, so no inbound webhook is required to complete a purchase. Free orders complete instantly. A successful create returns 201 plus a per-order access token for guest reads.
curl https://api.zatabox.com/api/v1/orders \ -H "Authorization: Bearer vt_test_…" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "items": [{ "ticketTypeId": "tkt_8f2k", "quantity": 2 }], "guestName": "Alice Johnson" }'{ "data": { "id": "ord_31xq", "orderNumber": "ORD-001", "status": "pending", "total": "42.00", "currency": "USD", "items": [ { "ticketTypeId": "tkt_8f2k", "quantity": 2 } ] }}| Field | Description | |
|---|---|---|
itemsreq | array | Line items one entry per ticket type. |
items[].ticketTypeIdreq | string | The ticket type to buy. |
items[].quantityreq | int | How many of that type. |
guestEmail | string | Where tickets and the receipt are sent. |
guestName | string | Name on the order. |
promoCode | string | Optional promo code, applied before totals. |
Scan a ticket at the gate
qrData carries whatever the gate captured the rotating signed QR payload, or a typed 6-character door code through the same field. The call answers 200 with a status of success, denied_duplicate, denied_wrong_event, denied_cancelled or denied_expired a denied scan is data, not an error. Full field reference under Endpoints → Check-in.
curl https://api.zatabox.com/api/v1/checkin/scan \ -H "Authorization: Bearer vt_test_…" \ -d '{ "qrData": "VTA-9F2K….1781119200.a1b2c3d4e5f6a7b8", "eventId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "gateName": "Gate A" }'# qrData is the rotating QR value; a 6-char door code works too.# Manual entry of a typed code uses POST /checkin/event/{id}/manual.{ "data": { "status": "success", "ticketCode": "VTA-9F2K…", "holderName": "A. Bello", "idCheck": false, "checkedInAt": "2026-06-10T18:42:09Z" }}