API reference
Every Sorted REST endpoint — method, path, auth, and request/response shapes. The same surface is available over MCP and as a machine-readable JSON catalog.
Base URL: https://sorted.fast/api
Authentication
Present your key in the
X-API-Key
header, or as Authorization: Bearer <key>. Organizer-scoped keys restrict all operations to the authenticated organizer's events. Admin keys (no associated user) have unrestricted access but must supply organizer_id explicitly when creating events. New organizers (and their first key) can be created with no prior credentials via POST /api/organizers/register.
Need a key? Create one in your dashboard. Connecting an agent over MCP? See the MCP setup guide.
Discovery & docs
/api
Public
Machine-readable JSON API catalog.
{
"status": 200,
"shape": "application/json"
}
/.well-known/agent-card.json
Public
A2A Agent Card for agent-to-agent discovery.
{
"status": 200,
"shape": "application/json"
}
/.well-known/agent.json
Public
Legacy A2A Agent Card path retained for older clients.
{
"status": 200,
"shape": "application/json"
}
/.well-known/oauth-protected-resource
Public
RFC 9728 OAuth Protected Resource Metadata for the MCP endpoint. Advertises bearer/API-key auth. When a Stytch authorization server is configured, the `authorization_servers` field is populated with the issuer and OAuth 2.1 access tokens are accepted alongside API keys; otherwise the field is omitted and the `x-auth-note` explains the API-key-only deviation.
{
"status": 200,
"shape": "application/json"
}
/oauth/authorize
Public
OAuth authorization endpoint for the MCP "click to connect" flow. Requires a signed-in Sorted user; unauthenticated visitors are routed through magic-link login and returned here with the original query params intact.
{
"status": 200,
"shape": "text/html"
}
/oauth/trusted-auth/jwks.json
Public
Public JWKS for the MCP trusted-auth token (front half of the OAuth flow). No auth required.
{
"status": 200,
"shape": "application/json"
}
/llms.txt
Public
Concise Markdown site map for LLM agents.
{
"status": 200,
"shape": "text/plain"
}
/agents.md
Public
Operational instructions for LLM agents using Sorted.
{
"status": 200,
"shape": "text/markdown"
}
/docs.md
Public
Plain Markdown documentation for humans and agents.
{
"status": 200,
"shape": "text/markdown"
}
/mcp
Auth required
Model Context Protocol endpoint (JSON-RPC 2.0 over HTTP) for Sorted's primary event and registration platform. Authenticate with `Authorization: Bearer <api_key>` (same keys as X-API-Key), or with an OAuth 2.1 access token via the "click to connect" flow when an authorization server is configured (see /.well-known/oauth-protected-resource). Exposes event and registration management as the core tools; virtual waiting-room queues are a separate secondary feature for merchants avoiding a registration-flow migration. Also exposes docs/API catalog as MCP resources. No agent-side payment: paid events still collect student payments via the hosted flow.
{
"body": {
"id": "string | integer",
"params": "object (method-specific)",
"method": "initialize | tools/list | tools/call | resources/list | resources/read",
"jsonrpc": "2.0"
}
}
{
"status": 200,
"shape": "application/json (JSON-RPC 2.0 response)"
}
Events
/api/events
Auth required
List events. Organizer keys return only the authenticated organizer's events. Admin keys return all events. Paginated via limit/offset query params.
{
"query": {
"offset": "integer (optional, default 0, max 100000) — items to skip",
"limit": "integer (optional, default 100, max 500) — items per page"
}
}
Show response (75 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"id": "integer",
"status": "active | expired | full | cancelled",
"address": "string | null — full street address",
"title": "string",
"body": "string",
"datetime": "ISO8601 UTC datetime",
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"price": "integer in cents",
"cancelled_at": "ISO8601 UTC datetime | null",
"capacity": "integer",
"pricing_mode": "free | fixed | dynamic",
"max_price": "integer in cents | null",
"min_price": "integer in cents | null",
"organizer_id": "integer",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer",
"waitinglist_capacity": "integer",
"current_price": "integer in cents | null",
"latitude": "number | null — map pin latitude",
"location_name": "string | null — venue name",
"longitude": "number | null — map pin longitude",
"image_urls": "array of strings — event photos, ordered (first = primary)",
"registration_count": "integer (live from ETS)",
"agent": {
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"agent_payment_required": "false",
"deliverables": {
"embed_code": "string",
"embed_url": "string",
"public_url": "string"
},
"hosted_registration": "true — Sorted hosts the public and embedded registration flows",
"hosted_registration_ready": "boolean — false when the event is closed or paid registration is blocked by missing Stripe setup",
"registration_processing": "sorted",
"workflow": "publish_public_url_or_embed_code"
},
"embed_code": "string — ready-to-paste iframe HTML for the hosted registration form",
"embed_url": "string — iframe-ready registration form URL",
"primary_image_url": "string | null — the primary image (event hero + catalog card)",
"public_url": "string — organizer-namespaced event detail page (/organizers/:slug/events/:id)"
}
],
"pagination": {
"count": "integer — items in this response",
"offset": "integer — applied offset",
"limit": "integer — applied page size",
"has_more": "boolean — true when more items exist beyond this page"
}
}
}
/api/events/:event_id
Auth required
Get a single event by ID with live registration count and current price.
Show response (67 lines) Hide response
{
"status": 200,
"shape": {
"data": {
"id": "integer",
"status": "active | expired | full | cancelled",
"address": "string | null — full street address",
"title": "string",
"body": "string",
"datetime": "ISO8601 UTC datetime",
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"price": "integer in cents",
"cancelled_at": "ISO8601 UTC datetime | null",
"capacity": "integer",
"pricing_mode": "free | fixed | dynamic",
"max_price": "integer in cents | null",
"min_price": "integer in cents | null",
"organizer_id": "integer",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer",
"waitinglist_capacity": "integer",
"current_price": "integer in cents | null",
"latitude": "number | null — map pin latitude",
"location_name": "string | null — venue name",
"longitude": "number | null — map pin longitude",
"image_urls": "array of strings — event photos, ordered (first = primary)",
"registration_count": "integer (live from ETS)",
"agent": {
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"agent_payment_required": "false",
"deliverables": {
"embed_code": "string",
"embed_url": "string",
"public_url": "string"
},
"hosted_registration": "true — Sorted hosts the public and embedded registration flows",
"hosted_registration_ready": "boolean — false when the event is closed or paid registration is blocked by missing Stripe setup",
"registration_processing": "sorted",
"workflow": "publish_public_url_or_embed_code"
},
"embed_code": "string — ready-to-paste iframe HTML for the hosted registration form",
"embed_url": "string — iframe-ready registration form URL",
"primary_image_url": "string | null — the primary image (event hero + catalog card)",
"public_url": "string — organizer-namespaced event detail page (/organizers/:slug/events/:id)"
}
}
}
/api/events
Auth required
Create an event. organizer_id is inferred from the API key for organizer keys. Admin keys must supply organizer_id explicitly. Starts an EventWorker immediately. pricing_mode is locked after creation. Returns public_url, embed_url, embed_code, and agent metadata. Agents do not pay to create or host events; for paid events, Sorted collects a platform fee from student payments via Stripe Connect destination charges.
Show response (24 lines) Hide response
{
"body": {
"event": {
"address": "string (optional full street address)",
"title": "string (required)",
"body": "string (required; Markdown supported — bare URLs auto-link)",
"currency": "ISO 4217 code (default: USD; any Stripe-supported presentment currency; immutable after creation)",
"datetime": "ISO8601 UTC datetime (required)",
"price": "integer in Stripe minor units (required; >= 1 for fixed, explicit initial price for dynamic; USD cents 1050 = $10.50, zero-decimal currencies like JPY use whole units)",
"capacity": "integer > 0 (required)",
"pricing_mode": "free | fixed | dynamic (default: fixed)",
"max_price": "integer in Stripe minor units (required if pricing_mode is dynamic; upper bound only)",
"min_price": "integer in Stripe minor units (required if pricing_mode is dynamic; lower bound only)",
"organizer_id": "integer (admin keys only)",
"recurrence_frequency": "none | days | weeks | months | years (default: none)",
"recurrence_interval": "integer >= 0 (default: 0)",
"waitinglist_capacity": "integer >= 0 (default: 0)",
"image_keys": "array of S3 object keys; the first is the primary image (event hero + catalog card)",
"latitude": "number (optional, for the map pin)",
"location_name": "string (optional venue name)",
"longitude": "number (optional, for the map pin)"
}
}
}
Show response (67 lines) Hide response
{
"status": 201,
"shape": {
"data": {
"id": "integer",
"status": "active | expired | full | cancelled",
"address": "string | null — full street address",
"title": "string",
"body": "string",
"datetime": "ISO8601 UTC datetime",
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"price": "integer in cents",
"cancelled_at": "ISO8601 UTC datetime | null",
"capacity": "integer",
"pricing_mode": "free | fixed | dynamic",
"max_price": "integer in cents | null",
"min_price": "integer in cents | null",
"organizer_id": "integer",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer",
"waitinglist_capacity": "integer",
"current_price": "integer in cents | null",
"latitude": "number | null — map pin latitude",
"location_name": "string | null — venue name",
"longitude": "number | null — map pin longitude",
"image_urls": "array of strings — event photos, ordered (first = primary)",
"registration_count": "integer (live from ETS)",
"agent": {
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"agent_payment_required": "false",
"deliverables": {
"embed_code": "string",
"embed_url": "string",
"public_url": "string"
},
"hosted_registration": "true — Sorted hosts the public and embedded registration flows",
"hosted_registration_ready": "boolean — false when the event is closed or paid registration is blocked by missing Stripe setup",
"registration_processing": "sorted",
"workflow": "publish_public_url_or_embed_code"
},
"embed_code": "string — ready-to-paste iframe HTML for the hosted registration form",
"embed_url": "string — iframe-ready registration form URL",
"primary_image_url": "string | null — the primary image (event hero + catalog card)",
"public_url": "string — organizer-namespaced event detail page (/organizers/:slug/events/:id)"
}
}
}
/api/events/:event_id
Auth required
Update an event. All fields are optional except pricing_mode and currency, which cannot change after creation — to change those, cancel the event and create a new one (price itself is editable). Restarts the EventWorker.
Show response (23 lines) Hide response
{
"body": {
"event": {
"address": "string (optional full street address)",
"title": "string",
"body": "string",
"currency": "ISO 4217 code (immutable after creation)",
"datetime": "ISO8601 UTC datetime",
"price": "integer in Stripe minor units (>= 1 for fixed; explicit initial/current price for dynamic)",
"capacity": "integer > 0",
"pricing_mode": "free | fixed | dynamic (immutable after creation)",
"max_price": "integer in Stripe minor units (dynamic upper bound)",
"min_price": "integer in Stripe minor units (dynamic lower bound)",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer >= 0",
"waitinglist_capacity": "integer >= 0",
"image_keys": "array of S3 object keys; the first is the primary image (event hero + catalog card)",
"latitude": "number (optional, for the map pin)",
"location_name": "string (optional venue name)",
"longitude": "number (optional, for the map pin)"
}
}
}
Show response (67 lines) Hide response
{
"status": 200,
"shape": {
"data": {
"id": "integer",
"status": "active | expired | full | cancelled",
"address": "string | null — full street address",
"title": "string",
"body": "string",
"datetime": "ISO8601 UTC datetime",
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"price": "integer in cents",
"cancelled_at": "ISO8601 UTC datetime | null",
"capacity": "integer",
"pricing_mode": "free | fixed | dynamic",
"max_price": "integer in cents | null",
"min_price": "integer in cents | null",
"organizer_id": "integer",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer",
"waitinglist_capacity": "integer",
"current_price": "integer in cents | null",
"latitude": "number | null — map pin latitude",
"location_name": "string | null — venue name",
"longitude": "number | null — map pin longitude",
"image_urls": "array of strings — event photos, ordered (first = primary)",
"registration_count": "integer (live from ETS)",
"agent": {
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"agent_payment_required": "false",
"deliverables": {
"embed_code": "string",
"embed_url": "string",
"public_url": "string"
},
"hosted_registration": "true — Sorted hosts the public and embedded registration flows",
"hosted_registration_ready": "boolean — false when the event is closed or paid registration is blocked by missing Stripe setup",
"registration_processing": "sorted",
"workflow": "publish_public_url_or_embed_code"
},
"embed_code": "string — ready-to-paste iframe HTML for the hosted registration form",
"embed_url": "string — iframe-ready registration form URL",
"primary_image_url": "string | null — the primary image (event hero + catalog card)",
"public_url": "string — organizer-namespaced event detail page (/organizers/:slug/events/:id)"
}
}
}
/api/events/:event_id
Auth required
Soft-cancel an event. Sets cancelled_at, stops the EventWorker, and removes the event from public listings. Data is preserved.
Show response (67 lines) Hide response
{
"status": 200,
"shape": {
"data": {
"id": "integer",
"status": "active | expired | full | cancelled",
"address": "string | null — full street address",
"title": "string",
"body": "string",
"datetime": "ISO8601 UTC datetime",
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"price": "integer in cents",
"cancelled_at": "ISO8601 UTC datetime | null",
"capacity": "integer",
"pricing_mode": "free | fixed | dynamic",
"max_price": "integer in cents | null",
"min_price": "integer in cents | null",
"organizer_id": "integer",
"recurrence_frequency": "none | days | weeks | months | years",
"recurrence_interval": "integer",
"waitinglist_capacity": "integer",
"current_price": "integer in cents | null",
"latitude": "number | null — map pin latitude",
"location_name": "string | null — venue name",
"longitude": "number | null — map pin longitude",
"image_urls": "array of strings — event photos, ordered (first = primary)",
"registration_count": "integer (live from ETS)",
"agent": {
"payment": {
"status": "not_required | required | unavailable",
"status_reason": "free_event | organizer_stripe_ready | organizer_stripe_not_configured",
"platform_fee_percent": "integer percentage charged on student payments",
"agent_payment_required": "false — event hosting and creation do not require agent payment",
"event_is_paid": "boolean",
"platform_fee_collection": "application_fee_amount | none",
"platform_fee_source": "student_payment | null",
"student_payment_processor": "stripe_connect_destination_charge | null",
"student_payment_required": "boolean"
},
"agent_payment_required": "false",
"deliverables": {
"embed_code": "string",
"embed_url": "string",
"public_url": "string"
},
"hosted_registration": "true — Sorted hosts the public and embedded registration flows",
"hosted_registration_ready": "boolean — false when the event is closed or paid registration is blocked by missing Stripe setup",
"registration_processing": "sorted",
"workflow": "publish_public_url_or_embed_code"
},
"embed_code": "string — ready-to-paste iframe HTML for the hosted registration form",
"embed_url": "string — iframe-ready registration form URL",
"primary_image_url": "string | null — the primary image (event hero + catalog card)",
"public_url": "string — organizer-namespaced event detail page (/organizers/:slug/events/:id)"
}
}
}
/api/events/:event_id/photos
Auth required
Add a photo to an event by URL. The server downloads the image (publicly reachable http/https URL on default ports, max 5 MB, jpeg/png/webp/gif) and appends it to the event's photos. The first photo is the primary image. Rate-limited per key (API_PHOTO_UPLOAD_RATE_LIMIT, default 30/min).
{
"body": {
"url": "string (publicly reachable image URL)"
}
}
{
"status": 201,
"shape": {
"data": {
"key": "string",
"url": "string | null",
"event_id": "integer",
"image_keys": "array of S3 object keys (first is primary)",
"image_urls": "array of CDN URLs",
"primary_image_url": "string | null"
}
}
}
/api/events/:event_id/photos/primary
Auth required
Make an existing photo the event's primary (cover/catalog) image by moving its key to the front of image_keys.
{
"body": {
"key": "string (one of the event's image_keys)"
}
}
{
"status": 200,
"shape": {
"data": {
"event_id": "integer",
"image_keys": "array of S3 object keys (first is primary)",
"image_urls": "array of CDN URLs",
"primary_image_url": "string | null"
}
}
}
/api/events/:event_id/photos?key=:key
Auth required
Remove a photo (by object key) from an event.
{
"status": 200,
"shape": {
"data": {
"event_id": "integer",
"image_keys": "array of S3 object keys (first is primary)",
"image_urls": "array of CDN URLs",
"primary_image_url": "string | null"
}
}
}
/api/events/:event_id/price_history
Auth required
Get the full dynamic pricing history for an event, newest first.
Show response (15 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"id": "integer",
"reason": "init | registration | tick | manual_change | other",
"timestamp": "ISO8601 UTC datetime",
"event_id": "integer",
"original_price": "integer in cents | null",
"new_price": "integer in cents"
}
]
}
}
Registrations
/api/registrations
Auth required
List registrations. Organizer keys return registrations for the organizer's events only. Paginated via limit/offset query params.
{
"query": {
"offset": "integer (optional, default 0, max 100000) — items to skip",
"limit": "integer (optional, default 100, max 500) — items per page"
}
}
Show response (21 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"id": "string (composite: event_id-user_public_id)",
"status": "registered | waitlisted",
"event_id": "integer",
"amount_paid": "integer in cents",
"user_id": "string (user public id, 8-char handle)",
"received_at": "ISO8601 UTC datetime (microsecond precision)"
}
],
"pagination": {
"count": "integer — items in this response",
"offset": "integer — applied offset",
"limit": "integer — applied page size",
"has_more": "boolean — true when more items exist beyond this page"
}
}
}
/api/registrations/:event_id/:user_id
Auth required
Get a single registration by event and user ID.
{
"status": 200,
"shape": {
"data": {
"id": "string (composite: event_id-user_public_id)",
"status": "registered | waitlisted",
"event_id": "integer",
"amount_paid": "integer in cents",
"user_id": "string (user public id, 8-char handle)",
"received_at": "ISO8601 UTC datetime (microsecond precision)"
}
}
}
/api/registrations
Auth required
Queue a registration via the high-concurrency ETS path. Returns 202 Accepted immediately. Provide exactly one of user_id (existing user) or email (a new student account is created automatically via the email hot path); they are mutually exclusive. amount_paid defaults to the current dynamic price if omitted. Rate-limited per API key (default 600/min — sized for bulk roster imports) — 429 with a Retry-After header when exceeded.
{
"body": {
"registration": {
"email": "string (required unless user_id is given; mutually exclusive with user_id)",
"event_id": "integer (required)",
"amount_paid": "integer in cents (optional, defaults to current price)",
"user_id": "string — user public id, 8-char handle (required unless email is given; mutually exclusive with email)"
}
}
}
{
"status": 202,
"shape": {
"message": "Registration queued for processing.",
"status": "accepted",
"next": {
"description": "202 means received, not confirmed. Poll `next.url` for the final status (email registrations resolve asynchronously — see next.description).",
"action": "poll_status",
"url": "/api/registrations/:event_id/:user_id"
}
}
}
/api/registrations/:event_id/:user_id
Auth required
Update a registration's status or amount_paid.
{
"body": {
"registration": {
"status": "registered | waitlisted",
"amount_paid": "integer in cents"
}
}
}
{
"status": 200,
"shape": {
"data": {
"id": "string (composite: event_id-user_public_id)",
"status": "registered | waitlisted",
"event_id": "integer",
"amount_paid": "integer in cents",
"user_id": "string (user public id, 8-char handle)",
"received_at": "ISO8601 UTC datetime (microsecond precision)"
}
}
}
/api/registrations/:event_id/:user_id
Auth required
Remove a student from an event.
{
"status": 204,
"shape": null
}
Queues
/api/queues
Auth required
List queue events. Organizer keys return only the organizer's own queues; admin keys return all queues. Paginated via limit/offset query params.
{
"query": {
"offset": "integer (optional, default 0, max 100000) — items to skip",
"limit": "integer (optional, default 100, max 500) — items per page"
}
}
Show response (29 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"active": "boolean",
"id": "integer",
"description": "string | null",
"title": "string",
"queue_length": "integer (live from ETS)",
"organizer_id": "integer",
"ends_at": "ISO8601 UTC datetime | null",
"max_queue_size": "integer | null",
"release_rate": "integer — visitors released per interval",
"starts_at": "ISO8601 UTC datetime | null",
"target_url": "string — where released visitors are sent",
"token_ttl_minutes": "integer — release-token lifetime in minutes",
"embed_url": "string — iframe-ready waiting-room URL",
"waiting_room_url": "string — public waiting-room page"
}
],
"pagination": {
"count": "integer — items in this response",
"offset": "integer — applied offset",
"limit": "integer — applied page size",
"has_more": "boolean — true when more items exist beyond this page"
}
}
}
/api/queues
Auth required
Create a Queue as a Service event and start its QueueWorker (when active). This is Sorted's separate, secondary virtual waiting-room offering for merchants that need surge protection without migrating to the core registration platform. organizer_id is inferred from the API key for organizer keys; admin keys must supply it.
Show response (16 lines) Hide response
{
"body": {
"queue_event": {
"active": "boolean",
"description": "string",
"title": "string (required)",
"organizer_id": "integer (admin keys only)",
"ends_at": "ISO8601 UTC datetime",
"max_queue_size": "integer > 0",
"release_rate": "integer > 0 (required) — visitors released per interval",
"starts_at": "ISO8601 UTC datetime",
"target_url": "string (required) — where released visitors are sent",
"token_ttl_minutes": "integer > 0 (required) — release-token lifetime"
}
}
}
Show response (21 lines) Hide response
{
"status": 201,
"shape": {
"data": {
"active": "boolean",
"id": "integer",
"description": "string | null",
"title": "string",
"queue_length": "integer (live from ETS)",
"organizer_id": "integer",
"ends_at": "ISO8601 UTC datetime | null",
"max_queue_size": "integer | null",
"release_rate": "integer — visitors released per interval",
"starts_at": "ISO8601 UTC datetime | null",
"target_url": "string — where released visitors are sent",
"token_ttl_minutes": "integer — release-token lifetime in minutes",
"embed_url": "string — iframe-ready waiting-room URL",
"waiting_room_url": "string — public waiting-room page"
}
}
}
/api/queues/:id
Auth required
Get a single queue event with its live queue length.
Show response (21 lines) Hide response
{
"status": 200,
"shape": {
"data": {
"active": "boolean",
"id": "integer",
"description": "string | null",
"title": "string",
"queue_length": "integer (live from ETS)",
"organizer_id": "integer",
"ends_at": "ISO8601 UTC datetime | null",
"max_queue_size": "integer | null",
"release_rate": "integer — visitors released per interval",
"starts_at": "ISO8601 UTC datetime | null",
"target_url": "string — where released visitors are sent",
"token_ttl_minutes": "integer — release-token lifetime in minutes",
"embed_url": "string — iframe-ready waiting-room URL",
"waiting_room_url": "string — public waiting-room page"
}
}
}
/api/queues/:id
Auth required
Update a queue event. Active queues restart their QueueWorker.
Show response (15 lines) Hide response
{
"body": {
"queue_event": {
"active": "boolean",
"description": "string",
"title": "string",
"ends_at": "ISO8601 UTC datetime",
"max_queue_size": "integer > 0",
"release_rate": "integer > 0",
"starts_at": "ISO8601 UTC datetime",
"target_url": "string",
"token_ttl_minutes": "integer > 0"
}
}
}
Show response (21 lines) Hide response
{
"status": 200,
"shape": {
"data": {
"active": "boolean",
"id": "integer",
"description": "string | null",
"title": "string",
"queue_length": "integer (live from ETS)",
"organizer_id": "integer",
"ends_at": "ISO8601 UTC datetime | null",
"max_queue_size": "integer | null",
"release_rate": "integer — visitors released per interval",
"starts_at": "ISO8601 UTC datetime | null",
"target_url": "string — where released visitors are sent",
"token_ttl_minutes": "integer — release-token lifetime in minutes",
"embed_url": "string — iframe-ready waiting-room URL",
"waiting_room_url": "string — public waiting-room page"
}
}
}
/api/queues/:id
Auth required
Delete a queue event and stop its QueueWorker.
{
"status": 204,
"shape": null
}
/api/queues/:id/join
Auth required
Join a visitor to a queue via the high-concurrency ETS path. Returns 202 Accepted immediately; position is processed asynchronously. Rate-limited per API key (default 60/min) — 429 with a Retry-After header when exceeded.
{
"body": {
"visitor_id": "string (required) — caller-assigned visitor identifier"
}
}
{
"status": 202,
"shape": {
"message": "Joined the queue.",
"status": "accepted",
"next": {
"description": "Poll `next.url` until status is no longer \"waiting\".",
"action": "poll_status",
"url": "/api/queues/:id/status/:visitor_id"
}
}
}
/api/queues/:id/status/:visitor_id
Auth required
Get a visitor's current position in the queue and estimated wait.
{
"status": 200,
"shape": {
"position": "integer (when waiting)",
"status": "waiting | not_in_queue",
"estimated_wait_seconds": "integer (when waiting)"
}
}
/api/queues/verify_token
Auth required
Verify a queue release token at the protected destination. Returns valid: true with token data, or 401 valid: false.
{
"body": {
"token": "string (required) — the release token issued to the visitor"
}
}
{
"status": 200,
"shape": {
"data": "object (when valid)",
"valid": "boolean"
}
}
Accounts & organizers
/api/organizers/register
Public
Public, unauthenticated organizer self-registration. Creates an organizer account from scratch and returns a freshly minted API key (raw, shown ONCE) plus the organizer slug and public path, so an agent can immediately authenticate and operate. password is optional (omit for a passwordless magic-link account). slug must be URL-safe, 3-100 chars, not reserved, and unique. Throttled per-IP. This is the only public write endpoint under /api.
{
"body": {
"organizer": {
"name": "string (required, 4-100 chars)",
"password": "string (optional, 12-72 chars; omit for magic-link account)",
"email": "string (required, unique)",
"slug": "string (required, URL-safe lowercase a-z0-9 and dashes, 3-100 chars)"
}
}
}
Show response (28 lines) Hide response
{
"status": 201,
"shape": {
"data": {
"authentication": {
"header": "X-API-Key",
"note": "string"
},
"api_key": {
"active": "boolean",
"id": "integer",
"name": "string",
"key": "string (full — shown only at creation)",
"inserted_at": "ISO8601 UTC datetime",
"user_id": "integer | null"
},
"organizer": {
"id": "integer",
"name": "string",
"email": "string",
"role": "organizer",
"slug": "string",
"public_url": "string",
"public_path": "string — /organizers/:slug"
}
}
}
}
/api/users
Auth required
List all users. Paginated via limit/offset query params.
{
"query": {
"offset": "integer (optional, default 0, max 100000) — items to skip",
"limit": "integer (optional, default 100, max 500) — items per page"
}
}
Show response (20 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"id": "integer",
"name": "string | null",
"email": "string",
"role": "user | organizer | admin",
"slug": "string | null (organizer path-namespace identifier)"
}
],
"pagination": {
"count": "integer — items in this response",
"offset": "integer — applied offset",
"limit": "integer — applied page size",
"has_more": "boolean — true when more items exist beyond this page"
}
}
}
/api/organizers
Auth required
List all users with the organizer role. Paginated via limit/offset query params.
{
"query": {
"offset": "integer (optional, default 0, max 100000) — items to skip",
"limit": "integer (optional, default 100, max 500) — items per page"
}
}
Show response (20 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"id": "integer",
"name": "string | null",
"email": "string",
"role": "user | organizer | admin",
"slug": "string | null (organizer path-namespace identifier)"
}
],
"pagination": {
"count": "integer — items in this response",
"offset": "integer — applied offset",
"limit": "integer — applied page size",
"has_more": "boolean — true when more items exist beyond this page"
}
}
}
API keys
/api/keys
Auth required
List active API keys. Organizer keys return only their own keys. Admin keys return all active keys. Key strings are masked in list responses.
Show response (15 lines) Hide response
{
"status": 200,
"shape": {
"data": [
{
"active": "boolean",
"id": "integer",
"name": "string",
"key": "string (first 8 chars + masked)",
"inserted_at": "ISO8601 UTC datetime",
"user_id": "integer | null"
}
]
}
}
/api/keys
Auth required
Create a new API key. The full key string is returned once at creation. Admin keys must supply user_id explicitly; organizer keys create keys for themselves.
{
"body": {
"api_key": {
"name": "string (required)",
"user_id": "integer (admin keys only)"
}
}
}
{
"status": 201,
"shape": {
"data": {
"active": "boolean",
"id": "integer",
"name": "string",
"key": "string (full — shown only at creation)",
"inserted_at": "ISO8601 UTC datetime",
"user_id": "integer | null"
}
}
}
/api/keys/:id
Auth required
Revoke an API key by deactivating it. Organizer keys can only revoke their own keys. Admin keys can revoke any key.
{
"status": 204,
"shape": null
}
Payouts
/api/stripe/onboarding_link
Auth required
Get a Stripe Connect onboarding URL for the authenticated organizer to enable payouts on paid events. Organizer key required. The URL must be completed by the human account organizer in a browser (Stripe KYC: identity + bank) — agents cannot complete it. Hand the URL to the organizer.
{
"status": 200,
"shape": {
"data": {
"stripe_account_id": "string",
"onboarding_complete": "boolean",
"onboarding_url": "string — open in a browser to finish payout setup"
}
}
}
Embed
/events/:event_id/embed
Public
Embeddable registration form. Iframe-friendly LiveView — no navigation or app chrome. Visitors enter email and optional name; an account is created if needed. No API key required.
{
"status": 200,
"shape": "text/html"
}
Other
https://github.com/josefrichter/sorted-claude-plugin
Public
Claude Code plugin. Installs the Sorted MCP server and five skills (create-event, list-events, register-student, setup-queue, manage-registrations). Install with: claude plugin install josefrichter/sorted-claude-plugin
{
"status": 200,
"shape": "text/html"
}
/api/uploads/presign
Auth required
Create a presigned PUT URL for uploading raw image bytes directly to storage. PUT the bytes to upload_url with the same Content-Type header, then attach the returned key to an event via PATCH /api/events/:event_id (image_keys) or the photo endpoints. Allowed content types: image/jpeg, image/png, image/webp, image/gif.
{
"body": {
"filename": "string (optional)",
"content_type": "image/jpeg | image/png | image/webp | image/gif",
"content_length": "integer bytes (required, max 5000000; part of the signature)"
}
}
{
"status": 201,
"shape": {
"data": {
"key": "string (S3 object key to attach to an event)",
"content_type": "string",
"content_length": "integer bytes",
"expires_in": "integer seconds",
"upload_url": "string (presigned PUT URL, expires)"
}
}
}
Error responses
400
Bad request
401
Missing or invalid API key
403
Valid key but not authorized to access this resource
404
Resource not found
422
Validation failed — per-field errors — {"code":"validation_failed","errors":{"field_name":["error message"]}}
503
Service unavailable
envelope
Error responses carry a stable machine `code` (branch on this, not the status text), a human `error` message, and a `hint` describing what to do next. Some errors add fields (e.g. payment_requirements, http_status, retry_after). A 503 with retry_after also sets the Retry-After header. — {"error":"human-readable message","code":"stable_machine_slug","hint":"what to do next"}