{"version":"1.6.0","authentication":{"type":"apiKey","header":"X-API-Key","notes":"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."},"product":{"primary":"Resilient, agent-ready event and registration infrastructure for high-concurrency signups.","secondary":"Queue as a Service is a separate virtual waiting-room option for merchants that need immediate surge protection without migrating their registration flow to Sorted."},"errors":{"400":"Bad request","401":"Missing or invalid API key","403":"Valid key but not authorized to access this resource","404":"Resource not found","422":{"description":"Validation failed — per-field errors","shape":{"code":"validation_failed","errors":{"field_name":["error message"]}}},"503":"Service unavailable","envelope":{"description":"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.","shape":{"error":"human-readable message","code":"stable_machine_slug","hint":"what to do next"}}},"endpoints":[{"id":"register_organizer","request":{"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)"}}},"path":"/api/organizers/register","description":"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.","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"}}}},"method":"POST","auth_required":false},{"id":"api_docs","request":null,"path":"/api","description":"Machine-readable JSON API catalog.","response":{"status":200,"shape":"application/json"},"method":"GET","auth_required":false},{"id":"a2a_agent_card","request":null,"path":"/.well-known/agent-card.json","description":"A2A Agent Card for agent-to-agent discovery.","response":{"status":200,"shape":"application/json"},"method":"GET","auth_required":false},{"id":"legacy_a2a_agent_card","request":null,"path":"/.well-known/agent.json","description":"Legacy A2A Agent Card path retained for older clients.","response":{"status":200,"shape":"application/json"},"method":"GET","auth_required":false},{"id":"oauth_protected_resource","request":null,"path":"/.well-known/oauth-protected-resource","description":"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.","response":{"status":200,"shape":"application/json"},"method":"GET","auth_required":false},{"id":"oauth_authorize","request":null,"path":"/oauth/authorize","description":"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.","response":{"status":200,"shape":"text/html"},"method":"GET","auth_required":false},{"id":"oauth_trusted_auth_jwks","request":null,"path":"/oauth/trusted-auth/jwks.json","description":"Public JWKS for the MCP trusted-auth token (front half of the OAuth flow). No auth required.","response":{"status":200,"shape":"application/json"},"method":"GET","auth_required":false},{"id":"claude_code_plugin","request":null,"path":"https://github.com/josefrichter/sorted-claude-plugin","description":"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","response":{"status":200,"shape":"text/html"},"method":"GET","auth_required":false},{"id":"llms_txt","request":null,"path":"/llms.txt","description":"Concise Markdown site map for LLM agents.","response":{"status":200,"shape":"text/plain"},"method":"GET","auth_required":false},{"id":"agents_md","request":null,"path":"/agents.md","description":"Operational instructions for LLM agents using Sorted.","response":{"status":200,"shape":"text/markdown"},"method":"GET","auth_required":false},{"id":"human_docs_md","request":null,"path":"/docs.md","description":"Plain Markdown documentation for humans and agents.","response":{"status":200,"shape":"text/markdown"},"method":"GET","auth_required":false},{"id":"mcp_endpoint","request":{"body":{"id":"string | integer","params":"object (method-specific)","method":"initialize | tools/list | tools/call | resources/list | resources/read","jsonrpc":"2.0"}},"path":"/mcp","description":"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.","response":{"status":200,"shape":"application/json (JSON-RPC 2.0 response)"},"method":"POST","auth_required":true},{"id":"list_events","request":{"query":{"offset":"integer (optional, default 0, max 100000) — items to skip","limit":"integer (optional, default 100, max 500) — items per page"}},"path":"/api/events","description":"List events. Organizer keys return only the authenticated organizer's events. Admin keys return all events. Paginated via limit/offset query params.","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"}}},"method":"GET","auth_required":true},{"id":"get_event","request":null,"path":"/api/events/:event_id","description":"Get a single event by ID with live registration count and current price.","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)"}}},"method":"GET","auth_required":true},{"id":"create_event","request":{"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)"}}},"path":"/api/events","description":"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.","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)"}}},"method":"POST","auth_required":true},{"id":"update_event","request":{"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)"}}},"path":"/api/events/:event_id","description":"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.","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)"}}},"method":"PATCH","auth_required":true},{"id":"cancel_event","request":null,"path":"/api/events/:event_id","description":"Soft-cancel an event. Sets cancelled_at, stops the EventWorker, and removes the event from public listings. Data is preserved.","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)"}}},"method":"DELETE","auth_required":true},{"id":"add_event_photo","request":{"body":{"url":"string (publicly reachable image URL)"}},"path":"/api/events/:event_id/photos","description":"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).","response":{"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"}}},"method":"POST","auth_required":true},{"id":"set_primary_event_photo","request":{"body":{"key":"string (one of the event's image_keys)"}},"path":"/api/events/:event_id/photos/primary","description":"Make an existing photo the event's primary (cover/catalog) image by moving its key to the front of image_keys.","response":{"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"}}},"method":"PUT","auth_required":true},{"id":"remove_event_photo","request":null,"path":"/api/events/:event_id/photos?key=:key","description":"Remove a photo (by object key) from an event.","response":{"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"}}},"method":"DELETE","auth_required":true},{"id":"presign_photo_upload","request":{"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)"}},"path":"/api/uploads/presign","description":"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.","response":{"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)"}}},"method":"POST","auth_required":true},{"id":"get_price_history","request":null,"path":"/api/events/:event_id/price_history","description":"Get the full dynamic pricing history for an event, newest first.","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"}]}},"method":"GET","auth_required":true},{"id":"list_registrations","request":{"query":{"offset":"integer (optional, default 0, max 100000) — items to skip","limit":"integer (optional, default 100, max 500) — items per page"}},"path":"/api/registrations","description":"List registrations. Organizer keys return registrations for the organizer's events only. Paginated via limit/offset query params.","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"}}},"method":"GET","auth_required":true},{"id":"get_registration","request":null,"path":"/api/registrations/:event_id/:user_id","description":"Get a single registration by event and user ID.","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)"}}},"method":"GET","auth_required":true},{"id":"create_registration","request":{"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)"}}},"path":"/api/registrations","description":"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.","response":{"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"}}},"method":"POST","auth_required":true},{"id":"update_registration","request":{"body":{"registration":{"status":"registered | waitlisted","amount_paid":"integer in cents"}}},"path":"/api/registrations/:event_id/:user_id","description":"Update a registration's status or amount_paid.","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)"}}},"method":"PATCH","auth_required":true},{"id":"delete_registration","request":null,"path":"/api/registrations/:event_id/:user_id","description":"Remove a student from an event.","response":{"status":204,"shape":null},"method":"DELETE","auth_required":true},{"id":"list_queues","request":{"query":{"offset":"integer (optional, default 0, max 100000) — items to skip","limit":"integer (optional, default 100, max 500) — items per page"}},"path":"/api/queues","description":"List queue events. Organizer keys return only the organizer's own queues; admin keys return all queues. Paginated via limit/offset query params.","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"}}},"method":"GET","auth_required":true},{"id":"create_queue","request":{"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"}}},"path":"/api/queues","description":"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.","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"}}},"method":"POST","auth_required":true},{"id":"get_queue","request":null,"path":"/api/queues/:id","description":"Get a single queue event with its live queue length.","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"}}},"method":"GET","auth_required":true},{"id":"update_queue","request":{"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"}}},"path":"/api/queues/:id","description":"Update a queue event. Active queues restart their QueueWorker.","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"}}},"method":"PATCH","auth_required":true},{"id":"delete_queue","request":null,"path":"/api/queues/:id","description":"Delete a queue event and stop its QueueWorker.","response":{"status":204,"shape":null},"method":"DELETE","auth_required":true},{"id":"join_queue","request":{"body":{"visitor_id":"string (required) — caller-assigned visitor identifier"}},"path":"/api/queues/:id/join","description":"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.","response":{"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"}}},"method":"POST","auth_required":true},{"id":"queue_status","request":null,"path":"/api/queues/:id/status/:visitor_id","description":"Get a visitor's current position in the queue and estimated wait.","response":{"status":200,"shape":{"position":"integer (when waiting)","status":"waiting | not_in_queue","estimated_wait_seconds":"integer (when waiting)"}},"method":"GET","auth_required":true},{"id":"verify_queue_token","request":{"body":{"token":"string (required) — the release token issued to the visitor"}},"path":"/api/queues/verify_token","description":"Verify a queue release token at the protected destination. Returns valid: true with token data, or 401 valid: false.","response":{"status":200,"shape":{"data":"object (when valid)","valid":"boolean"}},"method":"POST","auth_required":true},{"id":"list_users","request":{"query":{"offset":"integer (optional, default 0, max 100000) — items to skip","limit":"integer (optional, default 100, max 500) — items per page"}},"path":"/api/users","description":"List all users. Paginated via limit/offset query params.","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"}}},"method":"GET","auth_required":true},{"id":"list_organizers","request":{"query":{"offset":"integer (optional, default 0, max 100000) — items to skip","limit":"integer (optional, default 100, max 500) — items per page"}},"path":"/api/organizers","description":"List all users with the organizer role. Paginated via limit/offset query params.","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"}}},"method":"GET","auth_required":true},{"id":"list_keys","request":null,"path":"/api/keys","description":"List active API keys. Organizer keys return only their own keys. Admin keys return all active keys. Key strings are masked in list responses.","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"}]}},"method":"GET","auth_required":true},{"id":"create_key","request":{"body":{"api_key":{"name":"string (required)","user_id":"integer (admin keys only)"}}},"path":"/api/keys","description":"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.","response":{"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"}}},"method":"POST","auth_required":true},{"id":"delete_key","request":null,"path":"/api/keys/:id","description":"Revoke an API key by deactivating it. Organizer keys can only revoke their own keys. Admin keys can revoke any key.","response":{"status":204,"shape":null},"method":"DELETE","auth_required":true},{"id":"stripe_onboarding_link","request":null,"path":"/api/stripe/onboarding_link","description":"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.","response":{"status":200,"shape":{"data":{"stripe_account_id":"string","onboarding_complete":"boolean","onboarding_url":"string — open in a browser to finish payout setup"}}},"method":"POST","auth_required":true},{"id":"embed_form","request":null,"path":"/events/:event_id/embed","description":"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.","response":{"status":200,"shape":"text/html"},"method":"GET","auth_required":false}],"base_url":"https://sorted.fast/api"}