REST API

Base path /api/v1. Authenticate with Authorization: Bearer <key> or X-Api-Key; the organiser session cookie also works for same-origin calls. Responses are { data, meta? } or { error: { code, message, details? } }. Event ids accept the slug. Machine-readable spec: /api/v1/openapi.json.

KEY=tsr_live_demo_9f3b1c7e2a4d6f8b0c1d2e3f4a5b6c7d
# Bulk upsert booths from a CSV export of your CAD tool
curl -X POST -H "Authorization: Bearer $KEY" -F file=@booths.csv http://localhost:3000/api/v1/events/grip-connect-2026/booths/import
# Put a booth on hold for 2 hours
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"status":"held","holdMinutes":120}' \
  http://localhost:3000/api/v1/events/grip-connect-2026/booths/A101/status
# Route between two booths (public)
curl "http://localhost:3000/api/v1/events/grip-connect-2026/route?from=booth:A101&to=booth:T05&accessible=1"

Auth

GET/api/v1/me

Who am I

GET/api/v1/api-keys

List API keys

POST/api/v1/api-keys

Create key (organiser session only; key returned once)

Body: { name, scopes? }

DELETE/api/v1/api-keys/{id}

Revoke key

Events

GET/api/v1/events

List events

POST/api/v1/events

Create event

Body: EventInput

GET/api/v1/events/{event}

Get event

PATCH/api/v1/events/{event}

Update event (settings deep-merge)

Body: Partial<EventInput>

DELETE/api/v1/events/{event}

Delete event

POST/api/v1/events/{event}/publish

Publish a new floor plan version

Body: { note? }

POST/api/v1/events/{event}/unpublish

Set event back to draft

POST/api/v1/events/{event}/duplicate

Copy the map into a new draft event

Body: { name, slug?, includeExhibitors? }

GET/api/v1/events/{event}/versions

Published versions

GET/api/v1/events/{event}/bundle

Full plan bundle (live, or ?published=1)

Query: published (1 for the last published snapshot)

GET/api/v1/events/{event}/export/{format}

Export: expofp | geojson | booths.csv | exhibitors.csv | bundle | offline

Levels

GET/api/v1/events/{event}/levels

List levels

POST/api/v1/events/{event}/levels

Create level

Body: LevelInput

GET/api/v1/events/{event}/levels/{id}

Get level

PATCH/api/v1/events/{event}/levels/{id}

Update level

Body: Partial<LevelInput>

DELETE/api/v1/events/{event}/levels/{id}

Delete level

POST/api/v1/events/{event}/levels/reorder

Reorder levels

Body: { ids: string[] }

Booths

GET/api/v1/events/{event}/booths

List booths

Query: levelIdstatus (available|held|reserved|sold|unavailable)q (search label/externalId)exhibitorIdlimitoffset

POST/api/v1/events/{event}/booths

Create booth (object) or bulk upsert (array, matched by externalId then label)

Body: BoothInput | BoothInput[]

GET/api/v1/events/{event}/booths/{id}

Get booth by id, label or externalId

PATCH/api/v1/events/{event}/booths/{id}

Update booth

Body: Partial<BoothInput>

DELETE/api/v1/events/{event}/booths/{id}

Delete booth

POST/api/v1/events/{event}/booths/{id}/assign

Assign an exhibitor

Body: { exhibitorId, markSold? }

PUT/api/v1/events/{event}/booths/{id}/assign

Replace exhibitors

Body: { exhibitorIds }

DELETE/api/v1/events/{event}/booths/{id}/assign

Unassign an exhibitor

Body: { exhibitorId }

POST/api/v1/events/{event}/booths/{id}/status

Set status (held takes holdMinutes)

Body: { status: available|held|reserved|sold|unavailable, holdMinutes? }

POST/api/v1/events/{event}/booths/merge

Merge adjacent booths

Body: { boothIds, label? }

POST/api/v1/events/{event}/booths/import

CSV import (label, level, x, y, width, height, type, status, price, external_id, notes)

Body: { csv } | multipart file | text/csv

Sales

GET/api/v1/events/{event}/booths/{id}/quotepublic

Public price quote

GET/api/v1/events/{event}/exhibitors/{id}/extras

Extras held

POST/api/v1/events/{event}/exhibitors/{id}/extras

Assign extra

Body: { extraId, quantity?, boothId? }

DELETE/api/v1/events/{event}/exhibitors/{id}/extras

Remove extra

Body: { extraId }

POST/api/v1/events/{event}/reservepublic

Public: reserve/buy a booth

Body: { boothId, company, contactName, contactEmail, exhibitorId?, notes?, extras? }

GET/api/v1/events/{event}/orders

List orders

Query: statusboothIdexhibitorId

GET/api/v1/events/{event}/orders/summary

Sales summary

POST/api/v1/events/{event}/orders/expire

Expire timed-out holds now

GET/api/v1/events/{event}/orders/{id}

Get order

PATCH/api/v1/events/{event}/orders/{id}

Update notes/contact/amount

POST/api/v1/events/{event}/orders/{id}/pay

Mark paid (booth → sold)

Body: { providerRef? }

POST/api/v1/events/{event}/orders/{id}/invoice

Mark invoiced (booth → reserved)

POST/api/v1/events/{event}/orders/{id}/cancel

Cancel (booth → available)

Body: { reason? }

POST/api/v1/events/{event}/orders/{id}/refund

Refund (booth → available)

GET/api/v1/events/{event}/orders/{id}/checkoutpublic

Public: redirect to Stripe Checkout

POST/api/v1/events/{event}/orders/{id}/mock-paypublic

Public (mock provider): simulate payment

GET/api/v1/events/{event}/pricing-rules

List pricing rules

POST/api/v1/events/{event}/pricing-rules

Create rule

Body: { name, boothType?: standard|corner|peninsula|island|table|shell|sponsor|custom, minAreaM2?, maxAreaM2?, priceCents? | pricePerM2Cents?, currency? }

PATCH/api/v1/events/{event}/pricing-rules/{id}

Update

DELETE/api/v1/events/{event}/pricing-rules/{id}

Delete

GET/api/v1/events/{event}/extras

Sponsorships & booth extras

POST/api/v1/events/{event}/extras

Create extra

Body: { kind, name, description?, priceCents?, limitPerEvent?, limitPerExhibitor?, reserveOrBuyAllowed? }

PATCH/api/v1/events/{event}/extras/{id}

Update

DELETE/api/v1/events/{event}/extras/{id}

Delete

Exhibitors

GET/api/v1/events/{event}/exhibitors

List exhibitors

Query: qcategoryIdlevelIdfeatured (1|0)unassigned (1)limitoffset

POST/api/v1/events/{event}/exhibitors

Create (object) or bulk upsert (array; externalId → gripId → name)

Body: ExhibitorInput | ExhibitorInput[]

GET/api/v1/events/{event}/exhibitors/{id}

Get by id, slug or externalId

PATCH/api/v1/events/{event}/exhibitors/{id}

Update exhibitor

Body: Partial<ExhibitorInput>

DELETE/api/v1/events/{event}/exhibitors/{id}

Delete exhibitor

GET/api/v1/events/{event}/exhibitors/{id}/portal-link

Self-service magic link

POST/api/v1/events/{event}/exhibitors/{id}/portal-link

Rotate magic link

POST/api/v1/events/{event}/exhibitors/import

CSV import (ExpoFP template compatible)

Body: { csv } | multipart file

Categories

GET/api/v1/events/{event}/categories

List categories (with counts)

POST/api/v1/events/{event}/categories

Create category

Body: { name, color?, parentId?, sortIndex? }

GET/api/v1/events/{event}/categories/{id}

Get category

PATCH/api/v1/events/{event}/categories/{id}

Update

DELETE/api/v1/events/{event}/categories/{id}

Delete

Sessions

GET/api/v1/events/{event}/sessions

List sessions

POST/api/v1/events/{event}/sessions

Create (object) or bulk upsert by externalId (array)

Body: SessionInput | SessionInput[]

GET/api/v1/events/{event}/sessions/{id}

Get

PATCH/api/v1/events/{event}/sessions/{id}

Update

DELETE/api/v1/events/{event}/sessions/{id}

Delete

POST/api/v1/events/{event}/sessions/import

CSV import

Map

GET/api/v1/events/{event}/elements

List map elements (?levelId)

POST/api/v1/events/{event}/elements

Create element

Body: { levelId, kind: wall|line|text|poi|zone|image|shape|entrance|stage|room, geometry, props }

PUT/api/v1/events/{event}/elements

Replace a level's elements

Body: { levelId, elements[] }

GET/api/v1/events/{event}/elements/{id}

Get

PATCH/api/v1/events/{event}/elements/{id}

Update

DELETE/api/v1/events/{event}/elements/{id}

Delete

Wayfinding

GET/api/v1/events/{event}/wayfinding

Nodes, edges, transitions (?levelId)

PUT/api/v1/events/{event}/wayfinding

Replace a level's network

Body: { levelId, nodes[], edges[] }

POST/api/v1/events/{event}/wayfinding/generate

Auto-generate aisle network from geometry

Body: { levelId, cellSize?, clearance?, apply? }

GET/api/v1/events/{event}/wayfinding/transitions

List transitions

POST/api/v1/events/{event}/wayfinding/transitions

Create transition

Body: { name, kind: stairs|escalator|elevator|ramp|door|bridge, accessible, nodeIds[], travelSeconds }

PATCH/api/v1/events/{event}/wayfinding/transitions/{id}

Update

DELETE/api/v1/events/{event}/wayfinding/transitions/{id}

Delete

GET/api/v1/events/{event}/routepublic

Public routing

Query: from (booth:A101 | exhibitor:slug | element:id | point:L1:x,y | A101)to (…)accessible (1)via (repeatable)

POST/api/v1/events/{event}/route/optimizepublic

Public multi-stop optimisation

Body: { start, stops[], accessible?, returnToStart? }

Sponsorship

GET/api/v1/events/{event}/banners

Sponsor banners

POST/api/v1/events/{event}/banners

Create banner

Body: { placement: search_top|list_inline|map_corner|detail_top|splash, imageUrl, linkUrl?, exhibitorId?, title?, weight?, startsAt?, endsAt?, active? }

PATCH/api/v1/events/{event}/banners/{id}

Update

DELETE/api/v1/events/{event}/banners/{id}

Delete

Analytics

POST/api/v1/events/{event}/analyticspublic

Public: ingest viewer analytics (sendBeacon)

Body: { events: [{ type: view|search|booth_click|exhibitor_view|category_select|route|bookmark|share|custom_button|banner_impression|banner_click|level_switch|position, sessionId?, targetType?, targetId?, query?, levelId?, x?, y?, meta? }] }

GET/api/v1/events/{event}/analytics/summary

Analytics summary

Query: from (ISO)to (ISO)

GET/api/v1/events/{event}/analytics/exhibitors/{id}

Per-exhibitor stats

Integrations

POST/api/v1/events/{event}/integrations/grip/sync

Sync exhibitors from Grip / a JSON or CSV feed

Body: { sourceUrl?, dryRun? }

Webhooks

GET/api/v1/webhooks

List webhooks

POST/api/v1/webhooks

Create webhook (secret returned once)

Body: { url, eventId?, events?: ["*"] | (booth.created|booth.updated|booth.deleted|booth.status_changed|booth.assigned|booth.unassigned|booth.on_hold|exhibitor.created|exhibitor.updated|exhibitor.deleted|order.created|order.paid|order.cancelled|order.expired|floorplan.published|session.created|session.updated|session.deleted|extra.assigned|extra.removed)[], active? }

PATCH/api/v1/webhooks/{id}

Update

DELETE/api/v1/webhooks/{id}

Delete

POST/api/v1/webhooks/{id}/test

Send a test delivery

GET/api/v1/webhooks/{id}/deliveries

Recent deliveries

POST/api/v1/webhooks/process

Deliver pending/retrying webhooks now

Media

POST/api/v1/media

Upload a file (multipart `file`) → { url }

GET/api/v1/geocode

Venue search → [{ name, address, lat, lng, kind }] (event wizard)

Query: q (venue name or address)

Compatibility

POST/api/v1/compat/expofp/{action}

ExpoFP JSON API v1 compatibility shim (POST, token in body)

Actions: list-events, set-webhook-url, list-exhibitors, bulk-read-exhibitors, get-exhibitor, get-exhibitor-id, add-exhibitor, update-exhibitor, delete-exhibitor, set-exhibitor-logo, set-exhibitor-leading-image, list-booths, get-booth, update-booth, add-exhibitor-booth, remove-exhibitor-booth, list-categories, add-category, update-category, remove-category, list-extras, list-exhibitor-extras, add-exhibitor-extra, remove-exhibitor-extra, sessions/get, sessions/upsert, sessions/delete

Public

GET/e/{event}/data.jsonpublic

Public plan bundle (published)

Query: preview (1 (organiser session) for live data)

GET/e/{event}/data.expofp.jsonpublic

Public ExpoFP-compatible data.json

GET/e/{event}/version.jsonpublic

Published version probe