# Get Application Source: https://docs.gomry.com/api-reference/applications/get-application GET /applications/{applicationId} Retrieve a single form submission by ID # Get Application Returns a single application (form submission) by ID. **Required scope:** `applications:read` ## Path Parameters ## Response Returns the full application schema (see [List Applications](/api-reference/applications/list-applications)), plus a `history` array that the list endpoint omits. The application's status change log, oldest-first. Append-only — each entry records a status transition recorded inside Gomry (e.g. acceptance/rejection). It does **not** track changes to `metadata`; keep your own changelog inside `metadata` if you need one. User ID that made the change ISO-8601 timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" "https://www.gomry.com/api/v1/applications/APPLICATION_ID" ``` # List Applications Source: https://docs.gomry.com/api-reference/applications/list-applications GET /applications List form submissions (applications) for your organization # List Applications Returns a paginated list of form submissions. Each application is one user's response to a form. **Required scope:** `applications:read` ## Query Parameters Maximum: `200`. Comma-separated. Allowed: `Draft`, `Pending`, `Accepted`, `Rejected`. Defaults to all four. Filter to applications submitted to a specific form. Filter to applications submitted by a specific contact. ISO-8601 timestamp. Only return applications updated at or after this time. Use with `updated_to` for incremental sync. ISO-8601 timestamp. Only return applications updated at or before this time. ## Response The submitter's contact ID The submitter's user ID, if signed in `Draft`, `Pending`, `Accepted`, `Rejected` Matches the `id` of a question on the form String, number, ISO date, array, or object depending on `type` Linked payment, if the form requires payment Free-form integration data. Keys are client-defined and never interpreted by Gomry. Writable via [Update Application](/api-reference/applications/update-application). Empty object if never set. `utm_source` captured from the form URL at submission time `utm_medium` captured from the form URL at submission time `utm_campaign` captured from the form URL at submission time `utm_content` captured from the form URL at submission time `utm_term` captured from the form URL at submission time ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/applications?form_id=FORM_ID&status=Accepted" ``` # Update Application Source: https://docs.gomry.com/api-reference/applications/update-application PATCH /applications/{applicationId} Attach or update integration metadata on a form submission # Update Application Updates the `metadata` on an existing application. `metadata` is a free-form key/value object for your own integration data — Gomry never interprets these keys. Use it to tag applications with anything your system needs: a cohort, an external funnel stage, a CRM record ID. **Required scope:** `applications:write` The application's internal `status` (`Draft`, `Pending`, `Accepted`, `Rejected`) is **not** writable through this endpoint — it drives acceptance/rejection emails and list membership inside Gomry. Track your own funnel stages as keys inside `metadata` instead. ## Path Parameters The unique identifier of the application. ## Request Body The body uses **strict validation**: only `metadata` is accepted, and any other field is rejected with a `400` error. Key/value pairs to **merge** onto the application's existing metadata. Keys you don't include are left untouched. Set a key's value to `null` to remove that key. Keys are client-defined and never interpreted by Gomry. ### Merge behaviour Each `PATCH` merges — it does not replace the whole bag. To keep a changelog, read the current value, append, and write it back: ```json theme={null} { "metadata": { "funnel_stage": "Interview", "stage_history": [ { "stage": "Submitted", "at": "2026-05-01" }, { "stage": "Interview", "at": "2026-05-20" } ] } } ``` ## Response Returns the updated application, including its `metadata` and `history` (the internal status change log — see [Get Application](/api-reference/applications/get-application)). The updated application object (same schema as [List Applications](/api-reference/applications/list-applications), plus `history`). ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"metadata": {"target_cohort": "Fall 2025", "funnel_stage": "Interview"}}' \ "https://www.gomry.com/api/v1/applications/AbCdEfGhIjKlMnOpQrSt" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/applications/AbCdEfGhIjKlMnOpQrSt", { method: "PATCH", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ metadata: { target_cohort: "Fall 2025", funnel_stage: "Interview" }, }), } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "form_id": "FORM_ID", "contact_id": "CONTACT_ID", "status": "Accepted", "metadata": { "target_cohort": "Fall 2025", "funnel_stage": "Interview" }, "history": [ { "status": "Pending", "changed_by": "USER_ID", "changed_at": "2026-05-01T12:00:00.000Z" }, { "status": "Accepted", "changed_by": "USER_ID", "changed_at": "2026-05-20T09:00:00.000Z" } ], "updated_at": "2026-06-03T10:30:00.000Z" } } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "formErrors": [], "fieldErrors": {} } } ``` # Create Attendee Source: https://docs.gomry.com/api-reference/attendees/create-attendee POST /events/{eventId}/attendees Create one or more tickets for a contact on an event # Create Attendee Creates one or more tickets ("attendees") on an event your API key's organization owns. Mirrors the dashboard's manual-add flow: * Upserts the contact (creates or updates by email). * If an `invited` ticket exists for this email/contact on the event, it's upgraded and counted toward the requested quantity. * All tickets in a single request share one `payment_id` so the QR ticket page groups them. * Capacity is enforced against `quantity_total` on the ticket class and the event's `capacity`. By default no confirmation email is sent — pass `send_email: true` to opt in. Requires the `attendees:write` scope. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Path Parameters The unique identifier of the event. ## Request Body The ID of the ticket class to issue. Attendee email address. Attendee first name (1..120 chars). Attendee last name (1..120 chars). Optional phone number (max 40 chars). How many tickets to create for this contact (1..100). Defaults to `1`. Override the initial status. Allowed: `valid`, `pending_approval`, `checked_in`, `invited`. Defaults to `pending_approval` when the ticket class requires approval, otherwise `valid`. Recorded on each ticket. `cash` (default), `free`, or `external`. Paid tickets continue to flow through Stripe — this field is informational for manually-recorded sales. When `true`, the event's confirmation email is sent to the attendee. Default `false`. ## Response Returns `201 Created` with: Array of attendee (ticket) objects — see [Get Attendee](/api-reference/attendees/get-attendee). Shared payment ID across all tickets in the batch. Number of tickets actually created (may differ from request when an existing invited ticket was upgraded). ## Errors * `400 Validation failed` — invalid body shape. * `404 Event not found` / `Ticket class not found` — resource missing or not owned by your org. * `409` — capacity exceeded; the message lists how many seats remain. ```bash cURL theme={null} curl -X POST -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: attendee-$(uuidgen)" \ -d '{ "ticket_class_id": "prod_AbCdEf", "contact": { "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe" }, "quantity": 2, "send_email": false }' \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ ticket_class_id: "prod_AbCdEf", contact: { email: "jane@example.com", first_name: "Jane", last_name: "Doe", }, quantity: 2, }), } ); const { data, payment_id, quantity } = await response.json(); ``` ```json 201 Created theme={null} { "data": [ { "id": "tkt_AAA", "status": "valid", "ticket_class_id": "prod_AbCdEf", "ticket_class_name": "Early Bird", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "created_at": "2026-05-26T12:00:00.000Z", "updated_at": "2026-05-26T12:00:00.000Z" }, { "id": "tkt_BBB", "status": "valid", "ticket_class_id": "prod_AbCdEf", "ticket_class_name": "Early Bird", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "created_at": "2026-05-26T12:00:00.000Z", "updated_at": "2026-05-26T12:00:00.000Z" } ], "payment_id": "pay_XYZ", "quantity": 2 } ``` # Delete Attendee Source: https://docs.gomry.com/api-reference/attendees/delete-attendee DELETE /events/{eventId}/attendees/{attendeeId} Soft-delete an attendee (ticket) # Delete Attendee Soft-deletes a single attendee (ticket). Routes through the same canonical action as `PATCH { status: "deleted" }`, so refund/`statusChangeHistory` side effects fire correctly. Requires the `attendees:write` scope. ## Path Parameters The unique identifier of the event. The unique identifier of the attendee (ticket). ## Response Returns `200 OK` with: ```json theme={null} { "deleted": true, "id": "tkt_AAA" } ``` ## Errors * `404 Attendee not found` — ticket missing or not owned by your org. * `422` — the underlying delete was rejected (e.g. the ticket is part of an in-flight upgrade flow). The response `error` field carries the reason. ```bash cURL theme={null} curl -X DELETE -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/tkt_AAA ``` ```javascript JavaScript theme={null} await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/tkt_AAA", { method: "DELETE", headers: { "X-API-KEY": "your_api_key" } } ); ``` # Get Attendee Source: https://docs.gomry.com/api-reference/attendees/get-attendee GET /events/{eventId}/attendees/{attendeeId} Retrieve details of a single attendee # Get Attendee Returns the details of a single attendee (ticket) for an event. ## Path Parameters The unique identifier of the event. The unique identifier of the attendee (ticket document ID). ## Response Unique ticket identifier Ticket status ID of the ticket class (product) Name of the ticket class Ticket barcode Attendee first name Attendee last name Attendee email address ISO 8601 check-in timestamp ISO 8601 registration timestamp ISO 8601 last-updated timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/ticket_001 ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/ticket_001", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "ticket_001", "status": "valid", "ticket_class_id": "prod_general", "ticket_class_name": "General Admission", "barcode": "GOMRY-A1B2C3", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "checked_in_at": null, "created_at": "2025-06-01T12:00:00.000Z", "updated_at": "2025-06-01T12:00:00.000Z" } } ``` ```json 404 theme={null} { "error": "Attendee not found" } ``` # List Attendees Source: https://docs.gomry.com/api-reference/attendees/list-attendees GET /events/{eventId}/attendees List attendees for an event with pagination and status filtering # List Attendees Returns a paginated list of attendees (ticket holders) for an event. ## Path Parameters The unique identifier of the event. ## Query Parameters Page number (starts at 1). Number of attendees per page. Maximum: `200`. Comma-separated list of ticket statuses to include. Allowed values: `valid`, `checked_in`, `pending_approval`, `invited`. Example: `?status=valid,checked_in` Resolve a Gomry ticket QR code to its attendee(s). QR codes encode a URL of the form `https://gomry.com/ticket/{eventId}?pk={pk}` — pass that `pk` here to look up the matching ticket(s). The value may be a paymentID (group purchases return all tickets under the payment) or a ticket document ID. Filtered to the same `status` set; `page` and `page_size` still apply. ## Response Unique ticket identifier Ticket status ID of the ticket class (product) Name of the ticket class Ticket barcode Attendee first name Attendee last name Attendee email address ISO 8601 check-in timestamp ISO 8601 registration timestamp ISO 8601 last-updated timestamp Total number of matching attendees Current page number Items per page Total number of pages ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees?page=1&page_size=50" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees?page=1&page_size=50", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await response.json(); ``` ```json 200 theme={null} { "data": [ { "id": "ticket_001", "status": "valid", "ticket_class_id": "prod_general", "ticket_class_name": "General Admission", "barcode": "GOMRY-A1B2C3", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "checked_in_at": null, "created_at": "2025-06-01T12:00:00.000Z", "updated_at": "2025-06-01T12:00:00.000Z" }, { "id": "ticket_002", "status": "checked_in", "ticket_class_id": "prod_vip", "ticket_class_name": "VIP", "barcode": "GOMRY-D4E5F6", "first_name": "John", "last_name": "Smith", "email": "john@example.com", "checked_in_at": "2025-07-15T14:30:00.000Z", "created_at": "2025-05-20T09:00:00.000Z", "updated_at": "2025-07-15T14:30:00.000Z" } ], "pagination": { "total": 234, "page": 1, "page_size": 50, "total_pages": 5 } } ``` # Update Attendee Source: https://docs.gomry.com/api-reference/attendees/update-attendee PATCH /events/{eventId}/attendees/{attendeeId} Update an attendee's status # Update Attendee Updates an attendee (ticket) status. Delegates to the same path the Gomry dashboard uses, so upgrade-flow routing, capacity locks, and Stripe refund side effects fire correctly. Requires the `attendees:write` scope. ## Path Parameters The unique identifier of the event. The unique identifier of the attendee (ticket). ## Request Body Target status. Allowed values: * `valid` — approve a pending ticket * `checked_in` — mark as scanned at the door * `pending_approval` — return a valid ticket to the approval queue * `rejected` — refuse a pending ticket (may trigger Stripe refund for paid tickets) * `deleted` — soft-delete the ticket (also achievable via DELETE) Send the attendee an email about the status change. Default `false`. Custom message included in the notification email (max 10000 chars). Ignored when `notify_guest` is false. ## Response Returns `200 OK` with the updated attendee in the same shape as [Get Attendee](/api-reference/attendees/get-attendee). ## Errors * `400 Validation failed` — invalid body shape. * `404 Attendee not found` — ticket missing or not owned by your org. * `422` — the underlying transition was rejected (e.g. you tried to approve a ticket whose pending upgrade was cancelled). The response `error` field carries the reason. ```bash cURL theme={null} curl -X PATCH -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "status": "checked_in" }' \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/tkt_AAA ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/attendees/tkt_AAA", { method: "PATCH", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ status: "checked_in" }), } ); const { data } = await response.json(); ``` # Get Catalog Event Source: https://docs.gomry.com/api-reference/catalog/get-catalog-event GET /catalog/events/{event_id} Retrieve one catalog event with its buyable ticket types and registration questions # Get Catalog Event Returns a single catalog event, resolved with everything needed to sell it. Requires `catalog: read`. This is the **only** endpoint that populates `ticket_types` and `registration_questions` — both are `null` on the [list endpoint](/api-reference/catalog/list-catalog-events). Resolving ticket types costs one availability read per event, and for externally supplied inventory one upstream call per event. Doing that for a whole page would multiply the cost of a list by its size, for data a list view does not use. The intended pattern is: page the list, then read the detail for what you intend to sell. ## Path Parameters The event identifier, from `data[].id` on the list endpoint. Always a string. ## Response Every field from the [list response](/api-reference/catalog/list-catalog-events#response), plus the two resolved arrays below. The buyable ticket types. **Pass this as `items[].id` when creating a checkout session.** This is the ticket type id, not the event id. Tier name, e.g. "General Admission". Tier description. Price per ticket in major units. **`null` means unknown or variable, never free** — check `free` and `pricing`. ISO 4217 code. The only statement that a ticket costs nothing. `fixed` — `price` is the amount. `donation` — the buyer chooses any amount. `flexible` — the buyer chooses, at or above `minimum_price`. Floor for `flexible` pricing; `null` otherwise. Buyer-paid service fee per ticket. `null` when the organizer absorbs fees — a real answer meaning the fee exists but the buyer is not charged it, and different from `0`. Whether this type can be bought right now. Composed from stock, the sales window, and the tier's status. Smallest orderable quantity. Always at least 1. Largest quantity in one order, or `null` for no limit. A per-order cap, not per-person. When this tier goes on sale. When it stops. When `true`, a completed checkout yields a **pending request, not admission**. Tell your buyer. A link that opens Gomry checkout with this tier preselected. See [Buying without a payment token](#buying-without-a-payment-token). Questions the organizer asks every attendee. Echo back as `attendee_answers[].question_id`. The question label. Helper text, when the organizer wrote one. `text`, `email`, `phoneNumber`, `date`, `linkedin`, `organization` are free text. `dropdown` and `multipleChoice` are a closed list in `options`. `file` cannot be answered through this API. Allowed values for `dropdown` and `multipleChoice`; empty otherwise. The organizer's own flag, not a suggestion. `false` for `file` questions — an upload has no representation in a JSON checkout body. Surfaced rather than hidden so you can tell the buyer the organizer will follow up. **An empty array and `null` mean different things.** `[]` means we asked and there is nothing — no ticket types on sale, or no registration questions. `null` means not loaded on this endpoint. An agent that reads `[]` as "sold out" on a list response would skip every event in the catalog. ## Availability is a boolean, not a count `available` is deliberately a boolean, and no remaining-stock number is published anywhere on this surface. This feed crosses organizations. A remaining count sampled twice reconstructs both an organizer's inventory and their sell-through, and publishes it to every approved integrator — including their competitors. Your only question is "can I sell this right now", which the boolean answers. ## Buying without a payment token Most agents cannot complete an [ACP checkout session](/api-reference/checkout-sessions/create-checkout-session). Completing one needs a delegated payment token, and minting a Stripe Shared Payment Token requires a browser-based Payment Element plus a registered agent account with Stripe — which a personal assistant, a CLI, or an MCP client structurally does not have. `checkout_url` is the path for everyone else, and it is the pattern the industry settled on: **the agent does discovery and cart-building, then hands the buyer a link.** The card never leaves the browser. ``` GET /catalog/events → find the event GET /catalog/events/{id} → pick a tier, read the questions ticket_types[].checkout_url → hand this to your user ``` The link opens Gomry's normal checkout with that tier preselected. It carries `utm_campaign=catalogApi`, so a sale that started in your agent is attributed to the catalog rather than to Gomry's own discovery. It is always present — including on a tier that is sold out or not yet on sale, because the page is still worth linking and `available` already says whether it will sell. Use ACP checkout when you are an agent **platform** that can hold a delegated payment token. Use `checkout_url` for everything else. They are complementary, not alternatives — nothing needs to be onboarded to use the link. ## Why you must read the questions Roughly 30% of events ask something at registration, and most of those mark at least one answer required. An agent that ignores them sells a ticket the organizer considers incomplete. Collect the answers from your buyer and send them on `POST /checkout_sessions` as `attendee_answers`. A session with unanswered required questions stays `not_ready_for_payment` and reports them as blocking `messages[]`. ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/catalog/events/AbCdEfGhIjKlMnOpQrSt" ``` ```javascript JavaScript theme={null} const res = await fetch( `https://www.gomry.com/api/v1/catalog/events/${eventId}`, { headers: { "X-API-KEY": "your_api_key" } } ); const event = await res.json(); const sellable = event.ticket_types.filter((t) => t.available); const mustAsk = event.registration_questions.filter( (q) => q.required && q.answerable ); ``` ```python Python theme={null} import requests res = requests.get( f"https://www.gomry.com/api/v1/catalog/events/{event_id}", headers={"X-API-KEY": "your_api_key"}, ) event = res.json() sellable = [t for t in event["ticket_types"] if t["available"]] ``` ```json 200 theme={null} { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Tech Conference 2026", "description": "A two-day conference covering the latest in tech.", "url": "https://www.gomry.com/event/summer-tech-conference-2026-AbCdEfGhIjKlMnOpQrSt", "start": { "utc": "2026-07-15T14:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2026-07-16T22:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "latitude": 40.7128, "longitude": -74.006 }, "location_type": "physical", "cover_image": "https://storage.googleapis.com/...", "price": { "min": 79.0, "max": 249.0, "currency": "USD", "is_free": false }, "availability": "on_sale", "sales_start": null, "categories": ["Technology"], "organization": { "id": "OrgAbCdEfGhIjKlMnOpQ", "name": "Tech Events Co" }, "ticket_types": [ { "id": "TktAbCdEfGhIjKlMnOpQ", "name": "General Admission", "description": "Access to all talks.", "price": 79.0, "currency": "USD", "free": false, "pricing": "fixed", "minimum_price": null, "fee": 4.95, "available": true, "min_per_order": 1, "max_per_order": 10, "sales_start": { "utc": "2026-04-01T00:00:00.000Z", "timezone": "America/New_York" }, "sales_end": { "utc": "2026-07-15T14:00:00.000Z", "timezone": "America/New_York" }, "requires_approval": false, "checkout_url": "https://www.gomry.com/event/summer-tech-conference-2026-AbCdEfGhIjKlMnOpQrSt/getTickets?utm_medium=web&utm_campaign=catalogApi&ticket_type=TktAbCdEfGhIjKlMnOpQ" } ], "registration_questions": [ { "id": "QstAbCdEfGhIjKlMnOpQ", "title": "Dietary requirements", "subtitle": "Let us know if you have any.", "type": "dropdown", "options": ["None", "Vegetarian", "Vegan", "Gluten-free"], "required": true, "answerable": true } ] } ``` # Get Product Feed Source: https://docs.gomry.com/api-reference/catalog/get-product-feed GET /catalog/feed The whole public catalog as an ACP-conformant product feed # Get Product Feed Returns the entire public catalog as a [ACP Product Feed](https://developers.openai.com/commerce/specs/feed) in JSONL — one JSON object per line, no wrapping array. Requires `catalog: read`. This is the format agent platforms ingest to index your events. It is a **whole-catalog snapshot**, not a paginated endpoint. ## Query Parameters Set to `json` to get a wrapped JSON object with build counters instead of JSONL. Useful for inspecting a feed before shipping it; JSONL is what an uploader consumes. ## Response `200` with `Content-Type: application/x-ndjson; charset=utf-8`. Each line is one product row: Stable across refreshes — an agent's dedupe key. The event id. Event name, truncated to 150 characters. Truncated to 5,000 characters. Falls back to a factual line derived from the event when the organizer left it blank. The canonical, buyable event page. The organizer's name. A real name, never a placeholder. Also the organizer. Gomry is the marketplace, not the seller. Cover image. Required — see skipped rows below. `in_stock`, `out_of_stock`, `pre_order`, `backorder`, or `unknown`. `"79.99 USD"` — major units, one space, uppercase ISO 4217. Always `true`. Currently `false` on every row. See below. Always `true`. A ticket is delivered, not shipped — this keeps rows out of shipping-cost and delivery-estimate surfaces. `Events > Music`, `Events > Virtual`, or `Events`. Uppercase ISO 3166-1 alpha-2. Where the ticket can actually be bought. Always `false`. ### Length caps Truncation prefers a word boundary, but only when one falls in the last 15% of the limit — otherwise the text is cut at the limit exactly, so a long unbroken string does not lose a large tail to a distant space. An event is always truncated, never dropped, for being too long. ## Availability mapping The feed's vocabulary is narrower than the catalog's: | Catalog | Feed | | -------------- | -------------- | | `on_sale` | `in_stock` | | `sold_out` | `out_of_stock` | | `not_yet_open` | `out_of_stock` | | `closed` | `out_of_stock` | | `unknown` | `unknown` | `not_yet_open` maps to `out_of_stock`, **not** `pre_order`. Pre-order means "buy now, delivered later"; a sales window that has not opened cannot be bought at all, and saying otherwise sends a buyer to a checkout that refuses them. ## About `is_eligible_checkout` Every row currently reports `false`. This flag is a claim about the **merchant's checkout integration**, not about one event's sales window. The spec is explicit that it does not itself complete checkout onboarding and requires a separately enabled integration. Gomry's ACP checkout endpoints are live and conformant, but no agent platform currently consumes them for public purchases — so asserting `true` would claim an integration that is not in place. It flips in the same change that turns a real integration on. Never per-event. ## Why the feed has no date or venue It has no field for either. The ACP feed format was written for retail, and there is no date, venue, or location field anywhere in the spec. `product_category` is therefore the only place the nature of the item survives, which is why it is always populated. Date, venue, and coordinates are available on the [catalog endpoints](/api-reference/catalog/get-catalog-event). ## Rows that are skipped An event is omitted rather than published with invented data. A row is skipped when it has no quotable price, no organizer name, or **no cover image**. A missing cover image is the most common reason a live event does not appear in the feed. It will be listable on `/catalog/events` and absent here, with no error. If you expect an event in the feed and it is missing, check its cover image first. ## Errors No feed has been built yet. **Retry shortly — do not treat this as an empty catalog.** Sent with `Retry-After`. A feed exists but is too old to stand behind, which means recent rebuilds have been failing. We withhold it rather than serve prices nobody has checked since. Sent with `Retry-After`. The stored feed could not be read. Anything we cannot vouch for returns `503` rather than a short feed. A `200` carrying fewer rows than the catalog holds would be read as "these are all your events", and the platform would de-index everything missing. Treat a non-2xx as "keep yesterday's feed", never as "the catalog shrank". One case is **not** covered by that guarantee: if an individual event's detail lookup fails, the feed falls back to the list row for that event, which may then be skipped for want of a price. The response is still a `200` and the event is counted in `skipped` rather than `count`. So a `200` is not proof the feed is complete. Compare `count` against the previous run with `?format=json`, and treat a sudden drop as a signal to investigate rather than as a catalog that shrank. ## Freshness and caching The feed is **built on a schedule and served from the prepared copy**, so the request returns quickly however large the catalog is — it is not rebuilt while you wait. `Last-Modified` tells you when the copy you received was built, and `?format=json` carries the same timestamp as `meta.built_at`, so you can tell whether anything moved since your last pull. Responses are `Cache-Control: public, s-maxage=900, stale-while-revalidate=3600`. The feed is identical for every caller, so one cached copy serves all of them. The spec's own refresh cadence is daily; fetching more often than the rebuild interval returns the same copy. ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/catalog/feed" \ -o gomry-feed.jsonl ``` ```bash Inspect with counters theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/catalog/feed?format=json" | jq '.meta' ``` ```python Python theme={null} import requests, json res = requests.get( "https://www.gomry.com/api/v1/catalog/feed", headers={"X-API-KEY": "your_api_key"}, ) res.raise_for_status() # never treat a failure as an empty catalog rows = [json.loads(line) for line in res.text.splitlines() if line] ``` ```json JSONL (one line shown) theme={null} { "item_id": "AbCdEfGhIjKlMnOpQrSt", "title": "Summer Tech Conference 2026", "description": "A two-day conference covering the latest in tech.", "url": "https://www.gomry.com/event/summer-tech-conference-2026-AbCdEfGhIjKlMnOpQrSt", "brand": "Tech Events Co", "seller_name": "Tech Events Co", "image_url": "https://storage.googleapis.com/...", "availability": "in_stock", "price": "79.00 USD", "is_eligible_search": true, "is_eligible_checkout": false, "is_digital": true, "product_category": "Events > Technology", "target_countries": ["US"], "accepts_returns": false } ``` ```json format=json theme={null} { "data": [ "…rows…" ], "meta": { "count": 11, "scanned": 15, "skipped": 4 } } ``` # List Catalog Events Source: https://docs.gomry.com/api-reference/catalog/list-catalog-events GET /catalog/events Page the cross-organization catalog of live public events # List Catalog Events Returns a cursor-paginated list of live, public, discoverable events from **every organizer on Gomry**. Requires `catalog: read`. Unlike the rest of this API, this endpoint is not scoped to your organization — see [Agentic Commerce](/commerce/introduction). ## Query Parameters Events per page. Maximum `100`. Opaque. Pass `pagination.next_cursor` back verbatim — never construct one. Filter by venue city. ISO-3166 country name or code, matched case-insensitively. ISO 8601 datetime. Only events starting at or after this instant. ISO 8601 datetime. Must be at or after `starts_after`. Three-letter currency code. **Required whenever you send `min_price` or `max_price`.** Minimum ticket price in major units. Requires `currency`. Maximum ticket price in major units. Requires `currency`, and must be at or above `min_price`. `true` or `false`. Filters to free or paid events. Substring match on name and description. Up to 200 characters. `currency` is mandatory alongside a price bound. Without it, `max_price=50` would compare 50 EUR against 50 USD against 50 GBP across a mixed-currency catalog and return results you cannot detect as wrong. Prices are not converted to a base currency on this endpoint. ## Response Event identifier. Always a string — never assume numeric. Event name. Event description. The canonical, buyable event page. Safe to send a buyer to. ISO 8601 instant in UTC. IANA timezone. Same shape as `start`. Venue name. Street address. City. State or province. Country. Latitude. Longitude. `physical`, `virtual`, or `null` when undetermined. Treat `null` as unknown, not as physical. Cover image URL. Cheapest ticket in major units. **`null` means unknown, never free.** Most expensive ticket. Same rule. ISO 4217 code. Only `true` on a real zero. `on_sale`, `sold_out`, `not_yet_open`, `closed`, or `unknown`. When sales open, if they have not yet. Organizer-assigned categories. Organizer identifier. Organizer name. This is the seller. Always `null` here. Use the [detail endpoint](/api-reference/catalog/get-catalog-event). Always `null` here. Use the detail endpoint. Pass to `cursor` for the next page. `null` means the catalog ends here. Whether more pages follow. There is deliberately **no `total`**. A cross-organization count is not knowable without a full scan, and a number that is either expensive or wrong is worse than an authoritative end signal. ## Paging correctly **`next_cursor: null` is the only end-of-catalog signal.** An empty `data` array mid-catalog is a page where every event failed the eligibility gate — not a terminator. A consumer that stops on an empty page silently truncates its sweep and never notices. ```javascript Sweeping the whole catalog theme={null} let cursor = null; const events = []; do { const url = new URL("https://www.gomry.com/api/v1/catalog/events"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { "X-API-KEY": key } }); if (!res.ok) throw new Error(`Catalog page failed: ${res.status}`); const page = await res.json(); events.push(...page.data); cursor = page.pagination.next_cursor; // the ONLY stop condition } while (cursor); ``` Errors always leave as a non-2xx. We never answer a failure with an empty page and a null cursor, because that would assert "the catalog ends here" and make an outage look like a shrinking catalog. ## Event price vs ticket type prices There are two layers, and you will use both. **Event level** — `price` and `availability`, on every list row. `price` is a range, not a single number: `min` is the cheapest way in ("from \$25"), `max` the dearest. `availability` is one verdict for the whole event. Use these to search, rank, filter and display. **Ticket type level** — `ticket_types[]`, each with its own `price` and `available`. These are the things a buyer actually orders, and `ticket_types[].id` is what you reference when building an order. They are returned by the [detail endpoint](/api-reference/catalog/get-catalog-event) only; on this endpoint `ticket_types` is always `null`. When the event range is derived from live ticket classes, only types a buyer could buy **right now** count toward it. Anything not on sale, anything outside its sales window, and donation or pay-what-you-want types (which have no single price to quote) are excluded — so the "from" price is never one checkout would refuse. The two layers are computed at different moments and, for sellers whose inventory lives behind their own API, by different paths. On a fast-moving event they can differ slightly — a tier can sell out between your list call and your detail call. Treat the event range as the number you **display**, and the ticket type's own `price` as the number you **charge against**. If they disagree, the ticket type is authoritative. ## Understanding `unknown` `availability: "unknown"` means we could not determine it. It is **not** a synonym for `on_sale`. Treat it as "check the event page" — never as a green light to sell. Likewise `price.min: null` means the price is unknown, not that the event is free. `is_free: true` is the only statement that a ticket costs nothing. Both are uncommon: `price` and `availability` are resolved from live ticket classes, so a populated value is the normal case. You will still see `unknown` or a `null` price where an event has nothing we can read a price from — and you should keep handling both, because they are the honest answer rather than a guess. ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/catalog/events?limit=50&city=New%20York" ``` ```javascript JavaScript theme={null} const res = await fetch( "https://www.gomry.com/api/v1/catalog/events?limit=50&city=New+York", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await res.json(); ``` ```python Python theme={null} import requests res = requests.get( "https://www.gomry.com/api/v1/catalog/events", headers={"X-API-KEY": "your_api_key"}, params={"limit": 50, "city": "New York"}, ) page = res.json() ``` ```json 200 theme={null} { "data": [ { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Tech Conference 2026", "description": "A two-day conference covering the latest in tech.", "url": "https://www.gomry.com/event/summer-tech-conference-2026-AbCdEfGhIjKlMnOpQrSt", "start": { "utc": "2026-07-15T14:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2026-07-16T22:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "latitude": 40.7128, "longitude": -74.006 }, "location_type": "physical", "cover_image": "https://storage.googleapis.com/...", "price": { "min": 79.0, "max": 249.0, "currency": "USD", "is_free": false }, "availability": "on_sale", "sales_start": null, "categories": ["Technology"], "organization": { "id": "OrgAbCdEfGhIjKlMnOpQ", "name": "Tech Events Co" }, "ticket_types": null, "registration_questions": null } ], "pagination": { "next_cursor": "eyJlbmQiOiIyMDI2LTA3LTE2VDIyOjAwOjAwWiJ9", "has_more": true } } ``` # Cancel Checkout Session Source: https://docs.gomry.com/api-reference/checkout-sessions/cancel-checkout-session POST /checkout_sessions/{checkout_session_id}/cancel Abandon an open checkout session # Cancel Checkout Session Marks an open session as abandoned. Requires `checkout: write` and an allowlisted partner key. **No signature is needed** — cancel carries no body. Send `API-Version` as on every other checkout call. Cancel when your buyer walks away, so the session cannot later be paid by a stray retry and your own records show why it ended. An open session does **not** hold inventory — tickets are only claimed at completion. Cancelling therefore releases nothing and is not required to free stock, but it is still the correct way to close a cart you have abandoned. ## Path Parameters The session to cancel. ## Response `200` with the session, now `status: "canceled"`. Cancellation is terminal. A canceled session cannot be updated or completed — both return `409 invalid_session_state`. ## A completed session cannot be canceled Once tickets are issued, this endpoint is the wrong tool and returns `409 invalid_session_state`. Cancelling a checkout session is not a refund — it only abandons a cart that was never paid. To refund or cancel a real order, use the organizer's own tools or the [Payments API](/api-reference/payments/list-payments). Refunds follow the organizer's policy, not the agent's. ```bash cURL theme={null} curl -X POST \ "https://www.gomry.com/api/v1/checkout_sessions/acp_sess_AbCdEfGhIjKlMnOpQrSt/cancel" \ -H "X-API-KEY: your_api_key" \ -H "API-Version: 2025-09-12" ``` ```javascript JavaScript theme={null} const res = await fetch( `https://www.gomry.com/api/v1/checkout_sessions/${sessionId}/cancel`, { method: "POST", headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "API-Version": "2025-09-12", }, } ); const session = await res.json(); // status: "canceled" ``` ```json 200 theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "status": "canceled", "currency": "usd", "line_items": [ { "id": "li_1", "item": { "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }, "base_amount": 15800, "discount": 0, "subtotal": 15800, "tax": 0, "total": 16790 } ], "totals": [ { "type": "total", "display_text": "Total", "amount": 16790 } ], "messages": [] } ``` # Complete Checkout Session Source: https://docs.gomry.com/api-reference/checkout-sessions/complete-checkout-session POST /checkout_sessions/{checkout_session_id}/complete Charge the delegated payment token and issue tickets # Complete Checkout Session Charges the payment token and issues tickets. This is the only endpoint that moves money. Requires `checkout: write`, an allowlisted partner key, and a **signed request**. Signature verification matters most here: it runs over the raw body before parsing, so a tampered amount or a swapped token cannot reach the payment logic. The session must be `ready_for_payment`. Completing one that is not returns `409 invalid_session_state`. ## Path Parameters The session to pay. ## Body A **Stripe Shared Payment Token**. An opaque, single-use handle minted by your payment provider. Must be `stripe`. Optional billing address. A final chance to supply or correct the buyer before the ticket is issued. **`payment_data` accepts a token and nothing else resembling a card.** The object is validated strictly: a key like `card` or `card_number` is **rejected with `400`**, not ignored. Gomry has no raw-PAN code path. Accepting cardholder data here would move the platform from PCI SAQ-A to SAQ-D, and a card number reaching our logs and request traces is the one failure that cannot be walked back. If your agent holds a raw card number rather than a token, it cannot complete a Gomry checkout — send the buyer to the event `url` from the catalog instead. ## Response `200` with the [session](/api-reference/checkout-sessions/get-checkout-session#response), now `status: "completed"` and carrying an `order`. Surface `order.permalink_url` to your buyer. It opens without a Gomry login and is the same link the confirmation email sends. **`completed` means tickets exist.** We never report `completed` on a succeeded payment that failed to produce tickets — that would be money taken without a product. If you get `completed`, the tickets are issued. ## Completion is idempotent Completing an already-completed session **returns the existing order** rather than charging again, and concurrent `complete` calls on one session cannot both charge. Always send an `Idempotency-Key`, and reuse the same one when retrying. If a response is lost in transit, retrying with the same key is safe. ## Approval-required tickets When a tier has `requires_approval: true`, a successful completion produces a **pending request, not admission**. The session reports `completed` and the payment is taken, but the organizer must still approve. Check `requires_approval` on the [catalog detail](/api-reference/catalog/get-catalog-event) before selling, and tell your buyer. An agent that reports "you're going" on an approval-required ticket has told the buyer something untrue. ## Failures | Code | Status | What to do | | ------------------------- | ------ | ---------------------------------------------------------------------------------------------- | | `payment_declined` | `402` | Do not retry the same token. Ask for another payment method. | | `payment_requires_action` | `402` | The payment needs 3DS. Surface this to your buyer. | | `amount_mismatch` | `409` | The total changed since you quoted it. Re-read the session, confirm the new total, then retry. | | `sold_out` | `409` | Inventory went while the session was open. | | `invalid_session_state` | `409` | Not `ready_for_payment` — read the session. | `amount_mismatch` exists so a buyer is never charged a number they did not agree to. Never retry it blindly with the old total; re-read the session and re-confirm. ```bash cURL theme={null} curl -X POST \ "https://www.gomry.com/api/v1/checkout_sessions/acp_sess_AbCdEfGhIjKlMnOpQrSt/complete" \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -H "API-Version: 2025-09-12" \ -H "Timestamp: 2026-09-10T12:02:00Z" \ -H "Signature: base64-hmac-sha256" \ -H "Idempotency-Key: 8f14e45f-ea0c-4b9f-9c2a-1d3e5f7a9b0c" \ -d '{ "payment_data": { "token": "spt_1AbCdEfGhIjKlMnOpQrSt", "provider": "stripe" } }' ``` ```javascript JavaScript theme={null} const body = JSON.stringify({ payment_data: { token: sharedPaymentToken, provider: "stripe" }, }); const timestamp = new Date().toISOString(); const signature = crypto .createHmac("sha256", process.env.GOMRY_ACP_SIGNING_SECRET) .update(`${timestamp}.${body}`) .digest("base64"); const res = await fetch( `https://www.gomry.com/api/v1/checkout_sessions/${sessionId}/complete`, { method: "POST", headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "Content-Type": "application/json", "API-Version": "2025-09-12", Timestamp: timestamp, Signature: signature, "Idempotency-Key": idempotencyKey, // reuse on retry }, body, } ); const session = await res.json(); if (session.status === "completed") { showBuyer(session.order.permalink_url); } ``` ```json 200 theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "status": "completed", "currency": "usd", "line_items": [ { "id": "li_1", "item": { "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }, "base_amount": 15800, "discount": 0, "subtotal": 15800, "tax": 0, "total": 16790 } ], "totals": [ { "type": "items_base_amount", "display_text": "Tickets", "amount": 15800 }, { "type": "fee", "display_text": "Service fee", "amount": 990 }, { "type": "total", "display_text": "Total", "amount": 16790 } ], "messages": [], "order": { "id": "ord_AbCdEfGhIjKlMnOpQrSt", "checkout_session_id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "permalink_url": "https://www.gomry.com/ticket/AbCdEfGhIjKlMnOpQrSt?pk=pay_XyZ123" } } ``` ```json 402 declined theme={null} { "type": "processing_error", "code": "payment_declined", "message": "The payment method was declined." } ``` # Create Checkout Session Source: https://docs.gomry.com/api-reference/checkout-sessions/create-checkout-session POST /checkout_sessions Open an ACP checkout session for one event's tickets # Create Checkout Session Opens a checkout session and returns a priced cart. Requires `checkout: write`, an allowlisted partner key, and a **signed request**. See [Agentic Commerce](/commerce/introduction#access). ## Headers `2025-09-12`. Always send it. A different value is refused with `unsupported_api_version`; an absent one is treated as the current version. Base64 HMAC-SHA256 over `{timestamp}.{raw_body}`. RFC 3339, within 5 minutes of our clock. Recommended. Echoed back on the response. ## Body 1–20 lines. A **ticket type id** from `ticket_types[].id` on the [catalog detail endpoint](/api-reference/catalog/get-catalog-event). Not an event id. 1–50, subject to the tier's own `max_per_order`. **Required by Gomry, though optional in the ACP spec.** A ticket is admission for a named person: it prints the attendee's name and the door list is read by a human. A nameless ticket is technically valid and useless at the door. Where the ticket is delivered. Optional. The whole object may be omitted at create and supplied later with [update](/api-reference/checkout-sessions/update-checkout-session) — but the session will not reach `ready_for_payment` without it. Up to 50 answers to the event's registration questions. A **Gomry extension** to ACP: additive and optional, so a conforming client that never sends it still works — but an event with required questions reports them as blocking messages until they arrive. From `registration_questions[].id`. The answer as a string, whatever the question's type. For `dropdown` and `multipleChoice`, must be one of the published `options`. Accepted for spec conformance. ACP is shaped for physical goods; a ticket does not ship. Stored for the record, never used to fulfil — except for posted tickets, where it is required. **Unknown fields are rejected with `400 invalid_body`,** not ignored. A body carrying something like `card_number` must fail loudly rather than be quietly accepted into our logs. No field on this API ever carries a card number. ## Response `201` with a [checkout session](/api-reference/checkout-sessions/get-checkout-session#response). Read `status` first: | Status | What to do | | ----------------------- | ---------------------------------------------------------------------------------- | | `not_ready_for_payment` | Read `messages[]`, fix what is missing, then update the session. | | `ready_for_payment` | Proceed to [complete](/api-reference/checkout-sessions/complete-checkout-session). | ## One session, one event All items must belong to the same event and the same currency. Mixing them returns `multiple_events` or `currency_mismatch`. Sell two events as two sessions. ```bash cURL theme={null} curl -X POST "https://www.gomry.com/api/v1/checkout_sessions" \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -H "API-Version: 2025-09-12" \ -H "Timestamp: 2026-09-10T12:00:00Z" \ -H "Signature: base64-hmac-sha256" \ -H "Idempotency-Key: 8f14e45f-ea0c-4b9f-9c2a-1d3e5f7a9b0c" \ -d '{ "items": [{ "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }], "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "attendee_answers": [ { "question_id": "QstAbCdEfGhIjKlMnOpQ", "value": "Vegetarian" } ] }' ``` ```javascript JavaScript theme={null} import crypto from "crypto"; const body = JSON.stringify({ items: [{ id: "TktAbCdEfGhIjKlMnOpQ", quantity: 2 }], buyer: { name: "Ada Lovelace", email: "ada@example.com" }, }); const timestamp = new Date().toISOString(); const signature = crypto .createHmac("sha256", process.env.GOMRY_ACP_SIGNING_SECRET) .update(`${timestamp}.${body}`) .digest("base64"); const res = await fetch("https://www.gomry.com/api/v1/checkout_sessions", { method: "POST", headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "Content-Type": "application/json", "API-Version": "2025-09-12", Timestamp: timestamp, Signature: signature, "Idempotency-Key": crypto.randomUUID(), }, body, // the exact string that was signed }); const session = await res.json(); ``` ```json 201 theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "payment_provider": { "provider": "stripe", "supported_payment_methods": ["card"] }, "status": "ready_for_payment", "currency": "usd", "line_items": [ { "id": "li_1", "item": { "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }, "base_amount": 15800, "discount": 0, "subtotal": 15800, "tax": 0, "total": 16790 } ], "fulfillment_address": null, "fulfillment_option_id": "digital", "fulfillment_options": [ { "type": "digital", "id": "digital", "title": "Digital ticket", "subtitle": "Delivered by email", "subtotal": 0, "tax": 0, "total": 0 } ], "totals": [ { "type": "items_base_amount", "display_text": "Tickets", "amount": 15800 }, { "type": "fee", "display_text": "Service fee", "amount": 990 }, { "type": "total", "display_text": "Total", "amount": 16790 } ], "messages": [], "links": [ { "type": "terms_of_use", "url": "https://www.gomry.com/terms" }, { "type": "privacy_policy", "url": "https://www.gomry.com/privacy" } ] } ``` ```json 201 blocked theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "status": "not_ready_for_payment", "messages": [ { "type": "error", "code": "missing", "param": "buyer.name", "content_type": "plain", "content": "A buyer name is required to issue a ticket." } ] } ``` # Get Checkout Session Source: https://docs.gomry.com/api-reference/checkout-sessions/get-checkout-session GET /checkout_sessions/{checkout_session_id} Retrieve the current state of a checkout session # Get Checkout Session Returns a session as currently stored. Requires `checkout: write` and an allowlisted partner key. **No signature is needed** — a `GET` has no body to sign. Send `API-Version` as on every other checkout call. Sessions are scoped to the API key that created them. A session belonging to another key returns `404 session_not_found`. ## Path Parameters The session id returned at creation. ## Response Session identifier. `not_ready_for_payment`, `ready_for_payment`, `completed`, or `canceled`. **`completed` means tickets exist.** A succeeded payment with no issued tickets is never reported as `completed` — that would be money taken without a product. Attendee name. Delivery address. Optional. Always `stripe`. Methods accepted for this session. ISO 4217, lowercase. Line identifier. The requested `{ id, quantity }`. Pre-discount, in **minor units** (cents). Discount applied. After discount, before tax and fees. Tax. Line total. Ordered rows for display. `type` is one of `items_base_amount`, `items_discount`, `subtotal`, `discount`, `fulfillment`, `tax`, `fee`, `total`; `display_text` is what to show a buyer; `amount` is in minor units. Always exactly one option — the buyer is not choosing a delivery speed, they are being told how the ticket they already chose will arrive. `digital` or `shipping`. Most tickets are digital, but some externally supplied inventory is a **paper ticket sent by post** — for those, `type` is `shipping` and a `fulfillment_address` is required. `digital` or `shipping`. Display title. Display subtitle. Fulfillment cost in minor units. The selected option. Stored address, when one was supplied. Why the session is not payable, or what to tell the buyer. `type: "error"` blocks payment; `type: "info"` is advisory. See [Checkout Errors](/commerce/errors#blocking-messages-vs-errors). Seller policy documents. `type` is `terms_of_use`, `privacy_policy`, or `seller_shop_policies`. The event page URL is **not** here — that field is for policies, not merchandising. Get the event URL from the [catalog](/api-reference/catalog/get-catalog-event). Present only once `status` is `completed`. Order identifier. This session. Where your buyer views what they bought. Openable by the customer with no Gomry login — the same link the confirmation email sends. Safe to surface directly. Responses are always `Cache-Control: no-store`. Sessions are per-buyer and must never be shared by a cache. ```bash cURL theme={null} curl "https://www.gomry.com/api/v1/checkout_sessions/acp_sess_AbCdEfGhIjKlMnOpQrSt" \ -H "X-API-KEY: your_api_key" \ -H "API-Version: 2025-09-12" ``` ```javascript JavaScript theme={null} const res = await fetch( `https://www.gomry.com/api/v1/checkout_sessions/${sessionId}`, { headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "API-Version": "2025-09-12", }, } ); const session = await res.json(); ``` ```json 200 completed theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "payment_provider": { "provider": "stripe", "supported_payment_methods": ["card"] }, "status": "completed", "currency": "usd", "line_items": [ { "id": "li_1", "item": { "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }, "base_amount": 15800, "discount": 0, "subtotal": 15800, "tax": 0, "total": 16790 } ], "fulfillment_address": null, "fulfillment_option_id": "digital", "fulfillment_options": [ { "type": "digital", "id": "digital", "title": "Digital ticket", "subtitle": "Delivered by email", "subtotal": 0, "tax": 0, "total": 0 } ], "totals": [ { "type": "items_base_amount", "display_text": "Tickets", "amount": 15800 }, { "type": "fee", "display_text": "Service fee", "amount": 990 }, { "type": "total", "display_text": "Total", "amount": 16790 } ], "messages": [], "links": [{ "type": "terms_of_use", "url": "https://www.gomry.com/terms" }], "order": { "id": "ord_AbCdEfGhIjKlMnOpQrSt", "checkout_session_id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "permalink_url": "https://www.gomry.com/ticket/AbCdEfGhIjKlMnOpQrSt?pk=pay_XyZ123" } } ``` # Update Checkout Session Source: https://docs.gomry.com/api-reference/checkout-sessions/update-checkout-session POST /checkout_sessions/{checkout_session_id} Patch the cart, buyer, or answers on an open session # Update Checkout Session Patches an open session and returns it re-priced. This is how you clear blocking `messages[]` and move a session to `ready_for_payment`. Requires `checkout: write`, an allowlisted partner key, and a **signed request** — same headers as [create](/api-reference/checkout-sessions/create-checkout-session#headers). Note the method: ACP uses `POST`, not `PATCH`, for updates. ## Path Parameters The session to update. ## Body Every field is optional — send only what changes. 1–20 lines. **Replaces the cart wholesale**, it is not merged. To change one line, send all the lines you want. `name` and `email` required when present. Commonly used to supply the buyer after creating a session without one. Up to 50 answers. **Replaces the stored set wholesale**, like `items`. Required before payment when the cart contains a posted ticket. The chosen option's id. `items` and `attendee_answers` **replace**, they do not merge. Sending `items: [{id: "A", quantity: 1}]` on a session that held A and B removes B. ## Response `200` with the re-priced [session](/api-reference/checkout-sessions/get-checkout-session#response). Totals are recalculated on every update, so a price change or a tier selling out shows up here rather than surprising you at completion. ## Terminal sessions cannot be updated A `completed` or `canceled` session is immutable and returns `409 invalid_session_state`. A completed session is immutable because editing the cart behind a paid order would make the stored session disagree with what the buyer was actually charged for and what their tickets say. ## Working through blockers ```javascript Clearing blocking messages theme={null} let session = await createSession(items); while (session.status === "not_ready_for_payment") { const blockers = session.messages.filter((m) => m.type === "error"); if (!blockers.length) break; // not payable, but nothing actionable const patch = {}; for (const m of blockers) { if (m.param === "buyer.name") patch.buyer = await askBuyer(); if (m.param?.startsWith("attendee_answers")) { patch.attendee_answers = await askQuestions(); } if (m.code === "out_of_stock") return handleSoldOut(m); } if (!Object.keys(patch).length) return giveUp(blockers); session = await updateSession(session.id, patch); } ``` ```bash cURL theme={null} curl -X POST \ "https://www.gomry.com/api/v1/checkout_sessions/acp_sess_AbCdEfGhIjKlMnOpQrSt" \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -H "API-Version: 2025-09-12" \ -H "Timestamp: 2026-09-10T12:01:00Z" \ -H "Signature: base64-hmac-sha256" \ -d '{ "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" } }' ``` ```javascript JavaScript theme={null} const body = JSON.stringify({ buyer: { name: "Ada Lovelace", email: "ada@example.com" }, }); const timestamp = new Date().toISOString(); const signature = crypto .createHmac("sha256", process.env.GOMRY_ACP_SIGNING_SECRET) .update(`${timestamp}.${body}`) .digest("base64"); const res = await fetch( `https://www.gomry.com/api/v1/checkout_sessions/${sessionId}`, { method: "POST", headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "Content-Type": "application/json", "API-Version": "2025-09-12", Timestamp: timestamp, Signature: signature, }, body, } ); ``` ```json 200 theme={null} { "id": "acp_sess_AbCdEfGhIjKlMnOpQrSt", "buyer": { "name": "Ada Lovelace", "email": "ada@example.com" }, "status": "ready_for_payment", "currency": "usd", "line_items": [ { "id": "li_1", "item": { "id": "TktAbCdEfGhIjKlMnOpQ", "quantity": 2 }, "base_amount": 15800, "discount": 0, "subtotal": 15800, "tax": 0, "total": 16790 } ], "totals": [ { "type": "items_base_amount", "display_text": "Tickets", "amount": 15800 }, { "type": "fee", "display_text": "Service fee", "amount": 990 }, { "type": "total", "display_text": "Total", "amount": 16790 } ], "messages": [] } ``` # Create Contact Source: https://docs.gomry.com/api-reference/contacts/create-contact POST /contacts Create a new contact in your organization # Create Contact Creates a new contact in your organization. At least one of `email` or `phone_number` is required. If a contact with the same email already exists, the endpoint returns `409 Conflict` with the existing contact data. ## Request Body Email address. Will be normalized to lowercase. Must be a valid email format. At least one of `email` or `phone_number` is required. Phone number in international format (e.g., `+1234567890`). At least one of `email` or `phone_number` is required. First name. Last name. Job title. Company name. University name. Gender. Birthday (any string format, e.g., `1990-01-15`). Location (e.g., `New York, NY`). LinkedIn URL. Instagram handle or URL. Twitter handle or URL. GitHub handle or URL. Website URL. Array of tags to assign to the contact. ## Response Returns the created contact object with a `201 Created` status. The created contact object (same schema as [Get Contact](/api-reference/contacts/get-contact)). ```bash cURL theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "tags": ["speaker"]}' \ "https://www.gomry.com/api/v1/contacts" ``` ```javascript JavaScript theme={null} const response = await fetch("https://www.gomry.com/api/v1/contacts", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ email: "jane@example.com", first_name: "Jane", last_name: "Doe", tags: ["speaker"], }), }); const { data } = await response.json(); ``` ```json 201 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone_number": null, "status": "active", "job_title": null, "company": null, "university": null, "gender": null, "birthday": null, "location": null, "linkedin": null, "instagram": null, "twitter": null, "github": null, "website": null, "tags": ["speaker"], "created_at": "2025-07-15T10:00:00.000Z", "updated_at": "2025-07-15T10:00:00.000Z" } } ``` ```json 409 theme={null} { "error": "A contact with this email already exists", "data": { "id": "ExistingContactId", "first_name": "Jane", "email": "jane@example.com", "..." } } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "formErrors": ["At least one of email or phone_number is required"], "fieldErrors": {} } } ``` # Delete Contact Source: https://docs.gomry.com/api-reference/contacts/delete-contact DELETE /contacts/{contactId} Soft-delete a contact from your organization # Delete Contact Soft-deletes a contact by setting its status to `deleted`. The contact will no longer appear in list queries or be retrievable by ID. This operation is **not reversible** through the API. Contact Gomry support if you need to restore a deleted contact. ## Path Parameters The unique identifier of the contact to delete. ## Response Returns `204 No Content` on success with an empty body. ```bash cURL theme={null} curl -X DELETE \ -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt", { method: "DELETE", headers: { "X-API-KEY": "your_api_key" }, } ); // response.status === 204 ``` ```json 204 theme={null} // No response body ``` ```json 404 theme={null} { "error": "Contact not found" } ``` # Get Contact Source: https://docs.gomry.com/api-reference/contacts/get-contact GET /contacts/{contactId} Retrieve a single contact by ID # Get Contact Returns a single contact by its unique identifier. The contact must belong to your organization. `email`, `first_name`, and `last_name` are backfilled from the contact's linked user account when the contact record itself stores them empty (common for contacts created from form submissions or ticket purchases). A value stored directly on the contact always takes precedence. No extra scope is required — this comes with `contacts:read`. ## Path Parameters The unique identifier of the contact. ## Response Unique contact identifier First name Last name Email address Phone number Contact status Job title Company name University name Gender Birthday Location LinkedIn URL Instagram handle Twitter handle GitHub handle Website URL Array of tags ISO 8601 creation timestamp ISO 8601 last-updated timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone_number": "+1234567890", "status": "active", "job_title": "Engineer", "company": "Acme Inc", "university": null, "gender": null, "birthday": null, "location": "New York", "linkedin": null, "instagram": null, "twitter": null, "github": null, "website": null, "tags": ["vip"], "created_at": "2025-03-01T00:00:00.000Z", "updated_at": "2025-06-01T00:00:00.000Z" } } ``` ```json 404 theme={null} { "error": "Contact not found" } ``` # List Contact Applications Source: https://docs.gomry.com/api-reference/contacts/list-contact-applications GET /contacts/{contactId}/applications List form submissions made by a contact # List Contact Applications Returns the form submissions (applications) made by a given contact. **Required scopes:** `contacts:read` and `applications:read` ## Path Parameters ## Query Parameters Maximum: `200`. ## Response Each item in `data` is an [Application object](/api-reference/applications/list-applications). # List Contacts Source: https://docs.gomry.com/api-reference/contacts/list-contacts GET /contacts List contacts for your organization with pagination and filtering # List Contacts Returns a paginated list of contacts belonging to your organization. `email`, `first_name`, and `last_name` are backfilled from each contact's linked user account when the contact record itself stores them empty. A value stored directly on the contact always takes precedence. No extra scope is required — this comes with `contacts:read`. ## Query Parameters Page number (starts at 1). Number of contacts per page. Maximum: `200`. Comma-separated list of statuses to include. Allowed values: `active`, `inactive`. Example: `?status=active` Search contacts by email prefix. Case-insensitive. Matches the resolved email — including emails backfilled from the contact's linked user account. Example: `?search=jane@` ## Response Unique contact identifier First name Last name Email address Phone number Contact status (`active` or `inactive`) Job title Company name University name Gender Birthday Location LinkedIn URL Instagram handle Twitter handle GitHub handle Website URL Array of tags ISO 8601 creation timestamp ISO 8601 last-updated timestamp Total number of matching contacts Current page number Items per page Total number of pages ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/contacts?page=1&page_size=50" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/contacts?page=1&page_size=50", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await response.json(); ``` ```json 200 theme={null} { "data": [ { "id": "AbCdEfGhIjKlMnOpQrSt", "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone_number": "+1234567890", "status": "active", "job_title": "Engineer", "company": "Acme Inc", "university": null, "gender": null, "birthday": null, "location": "New York", "linkedin": "https://linkedin.com/in/janedoe", "instagram": null, "twitter": null, "github": "janedoe", "website": null, "tags": ["vip", "speaker"], "created_at": "2025-03-01T00:00:00.000Z", "updated_at": "2025-06-01T00:00:00.000Z" } ], "pagination": { "total": 1, "page": 1, "page_size": 50, "total_pages": 1 } } ``` # Update Contact Source: https://docs.gomry.com/api-reference/contacts/update-contact PATCH /contacts/{contactId} Update an existing contact's fields # Update Contact Updates an existing contact. Only provided fields are modified — omitted fields remain unchanged. The request body uses **strict validation**: unknown fields (e.g., `organizationID`) are rejected with a `400` error to prevent accidental data corruption. ## Path Parameters The unique identifier of the contact. ## Request Body All fields are optional. Only include the fields you want to update. Set a field to `null` to clear it. Email address. Will be normalized to lowercase. If the new email is already used by another contact in your organization, the request returns `409 Conflict`. Phone number. First name. Last name. Job title. Set to `null` to clear. Company name. Set to `null` to clear. University name. Set to `null` to clear. Gender. Set to `null` to clear. Birthday. Set to `null` to clear. Location. Set to `null` to clear. LinkedIn URL. Instagram handle. Twitter handle. GitHub handle. Website URL. Replace the contact's tags with this array. Contact status. Allowed values: `active`, `inactive`. ## Response Returns the updated contact object. The updated contact object (same schema as [Get Contact](/api-reference/contacts/get-contact)). ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"first_name": "Janet", "job_title": "CTO", "tags": ["vip", "speaker"]}' \ "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/contacts/AbCdEfGhIjKlMnOpQrSt", { method: "PATCH", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ first_name: "Janet", job_title: "CTO", tags: ["vip", "speaker"], }), } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "first_name": "Janet", "last_name": "Doe", "email": "jane@example.com", "phone_number": "+1234567890", "status": "active", "job_title": "CTO", "company": "Acme Inc", "university": null, "gender": null, "birthday": null, "location": "New York", "linkedin": null, "instagram": null, "twitter": null, "github": null, "website": null, "tags": ["vip", "speaker"], "created_at": "2025-03-01T00:00:00.000Z", "updated_at": "2025-07-15T10:30:00.000Z" } } ``` ```json 409 theme={null} { "error": "A contact with this email already exists" } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "formErrors": [], "fieldErrors": {} } } ``` # Get Custom Field Source: https://docs.gomry.com/api-reference/custom-fields/get-custom-field GET /custom-fields/{fieldId} Retrieve a single custom field definition by ID # Get Custom Field **Required scope:** `custom_fields:read` ## Path Parameters ## Response See [List Custom Fields](/api-reference/custom-fields/list-custom-fields) for the full schema. # List Custom Fields Source: https://docs.gomry.com/api-reference/custom-fields/list-custom-fields GET /custom-fields List custom field definitions for your organization # List Custom Fields Returns the custom field definitions used to interpret the `custom_fields` map on contact responses. **Required scope:** `custom_fields:read` ## Query Parameters Maximum: `200`. Filter by record type. Currently only `contact`. ## Response e.g. `text`, `multipleChoice`, `date`, `number`, `dropdown`, `file` Always `contact` today `active` or `deleted` ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" "https://www.gomry.com/api/v1/custom-fields" ``` # Create Event Source: https://docs.gomry.com/api-reference/events/create-event POST /events Create a new event in your organization # Create Event Creates a new event in your organization. A default "General Admission" ticket class is created automatically with the price (or free flag) from the request body so the event is immediately bookable. Requires the `events:write` scope. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key, so two integrations can use the same key value without collision. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Request Body Event name (max 200 chars). ISO-8601 datetime, or a local `YYYY-MM-DDTHH:mm` string interpreted in `timezone`. IANA timezone (e.g. `America/New_York`). Defaults to UTC. Same formats as `start.utc`. Must be after `start`. Must match `start.timezone` if provided. Physical address, OR a virtual meeting URL (`https://...`). When a URL is provided the event is flagged as virtual and the URL is stored as the meeting link. ISO-4217 currency code. Defaults to the organization's currency. Maximum number of tickets that can be sold. `0` or omitted means unlimited. Price for the default General Admission ticket class. `0` or omitted means free. Whether registrations require organizer approval. Defaults to `false`. Whether the event is hidden from public listings. Defaults to `false`. Absorb the Gomry service fee into the ticket price instead of adding it at checkout. Defaults to `false` (the buyer pays the fee on top). Settable at create so the event is never briefly live at a price that includes the fee on top — a price you never quoted, on a public page. This is the **event-wide default**. An individual ticket class can override it with its own `remove_service_fees`, so one event can absorb the fee on some tiers and pass it on for others — see [Create Ticket Class](/api-reference/ticket-classes/create-ticket-class). Long-form description shown on the event page. Space the event belongs to. Defaults to `GENERAL`. ## Response Returns the created event with `201 Created`. Same schema as [Get Event](/api-reference/events/get-event). ```bash cURL theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Summer Launch Party", "start": { "utc": "2026-07-15T18:00", "timezone": "America/New_York" }, "end": { "utc": "2026-07-15T22:00", "timezone": "America/New_York" }, "location": "Convention Center, New York", "capacity": 250, "ticket_price": 0 }' \ "https://www.gomry.com/api/v1/events" ``` ```json 201 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Launch Party", "status": "active", "registration_status": "open", "currency": "usd", "capacity": 250, "tickets_sold": 0, "start": { "utc": "2026-07-15T22:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2026-07-16T02:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center, New York", "address": "Convention Center, New York", "city": null, "state": null, "country": null }, "created_at": "2026-05-24T10:00:00.000Z", "updated_at": "2026-05-24T10:00:00.000Z" } } ``` ```json 400 theme={null} { "error": "end must be after start" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'events'.", "required_scope": "events:write" } ``` # Delete Event Source: https://docs.gomry.com/api-reference/events/delete-event DELETE /events/{eventId} Soft-delete an event # Delete Event Soft-deletes an event. The event is hidden from the dashboard, its public page, and the [List Events](/api-reference/events/list-events) endpoint. The underlying record is kept, so the id stays reserved and historical reporting is unaffected. **Only the primary host organization can delete an event.** An event your organization merely co-hosts is not visible to your key and returns `404`. Deleting does **not** refund tickets or notify attendees — cancel the event from the dashboard first if attendees must be told or refunded. Deleting an event that is already deleted returns `200` again; the call is idempotent. Requires the `events:write` scope. ## Path Parameters The unique identifier of the event. ## Response Returns `200 OK` with: ```json theme={null} { "deleted": true, "id": "AbCdEfGhIjKlMnOpQrSt" } ``` ## Errors * `404 Event not found` — the event does not exist, or is not owned by your organization (including events you co-host). * `403 insufficient_scope` — the API key does not have `events:write`. ```bash cURL theme={null} curl -X DELETE -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt ``` ```javascript JavaScript theme={null} await fetch("https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt", { method: "DELETE", headers: { "X-API-KEY": "your_api_key" }, }); ``` ```json 200 OK theme={null} { "deleted": true, "id": "AbCdEfGhIjKlMnOpQrSt" } ``` ```json 404 Not Found theme={null} { "error": "Event not found" } ``` # Get Event Source: https://docs.gomry.com/api-reference/events/get-event GET /events/{eventId} Retrieve details of a single event # Get Event Returns the details of an event that belongs to your organization. ## Path Parameters The unique identifier of the event (Firestore document ID). ## Response Unique event identifier Event name Event description URL-friendly slug Event status: `active`, `inactive`, or `deleted` `open` or `closed` Whether the event is private ISO 4217 currency code (e.g., `USD`, `EUR`) Maximum capacity Number of tickets sold ISO 8601 datetime in UTC IANA timezone (e.g., `America/New_York`) ISO 8601 datetime in UTC IANA timezone Venue name Street address City State or province Country Whether the organizer absorbs the Gomry service fee instead of adding it at checkout. This is the **event-wide default**; an individual ticket class can override it with its own `remove_service_fees` (see [Get Ticket Class](/api-reference/ticket-classes/get-ticket-class)). URL of the event cover image Virtual meeting URL (for online events) ISO 8601 creation timestamp ISO 8601 last-updated timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Tech Conference 2025", "description": "A two-day conference covering the latest in tech.", "slug": "summer-tech-conference-2025-AbCdEfGhIjKlMnOpQrSt", "status": "active", "registration_status": "open", "is_private": false, "currency": "USD", "capacity": 500, "tickets_sold": 234, "start": { "utc": "2025-07-15T14:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2025-07-16T22:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US" }, "remove_service_fees": false, "cover_image": "https://storage.googleapis.com/...", "meeting_url": null, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } } ``` ```json 404 theme={null} { "error": "Event not found" } ``` # List Events Source: https://docs.gomry.com/api-reference/events/list-events GET /events List all events for your organization # List Events Returns a paginated list of events belonging to your organization. ## Query Parameters Page number (starts at 1). Number of events per page. Maximum: `200`. Comma-separated list of event statuses to include. Allowed values: `active`, `inactive`. Example: `?status=active,inactive` ## Response Unique event identifier Event name Event description URL-friendly slug `active` or `inactive` `open` or `closed` Whether the event is private ISO 4217 currency code Maximum capacity Number of tickets sold ISO 8601 datetime in UTC IANA timezone ISO 8601 datetime in UTC IANA timezone Venue name Street address City State or province Country Whether the organizer absorbs the Gomry service fee instead of adding it at checkout. This is the **event-wide default**; an individual ticket class can override it with its own `remove_service_fees` (see [Get Ticket Class](/api-reference/ticket-classes/get-ticket-class)). URL of the event cover image Virtual meeting URL ISO 8601 creation timestamp ISO 8601 last-updated timestamp Total number of matching events Current page number Items per page Total number of pages ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/events?page=1&page_size=20" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events?page=1&page_size=20", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://www.gomry.com/api/v1/events", headers={"X-API-KEY": "your_api_key"}, params={"page": 1, "page_size": 20}, ) result = response.json() events = result["data"] ``` ```json 200 theme={null} { "data": [ { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Tech Conference 2025", "description": "A two-day conference covering the latest in tech.", "slug": "summer-tech-conference-2025-AbCdEfGhIjKlMnOpQrSt", "status": "active", "registration_status": "open", "is_private": false, "currency": "USD", "capacity": 500, "tickets_sold": 234, "start": { "utc": "2025-07-15T14:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2025-07-16T22:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US" }, "remove_service_fees": false, "cover_image": "https://storage.googleapis.com/...", "meeting_url": null, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } ], "pagination": { "total": 12, "page": 1, "page_size": 20, "total_pages": 1 } } ``` # Update Event Source: https://docs.gomry.com/api-reference/events/update-event PATCH /events/{event_id} Partially update an event in your organization # Update Event Partially updates an event. Only the fields supplied in the body are written — omitted fields are left unchanged. The event must belong to the organization associated with the API key; cross-org requests return `404 Not Found` with no information leak. Requires the `events:write` scope. ## Path Parameters Event ID (the canonical 20-character document ID, e.g. `AbCdEfGhIjKlMnOpQrSt`). ## Request Body At least one field is required. Unknown fields are rejected with `400`. Event name (1–200 chars). Triggers a taxonomy + search-relevance re-index in the background. Long-form description shown on the event page. Triggers a re-index. ISO-8601 datetime or a local `YYYY-MM-DDTHH:mm` string interpreted in `timezone`. IANA timezone (e.g. `America/New_York`). Same shape as `start`. Pass `null` to clear the end time (open-ended event). When both `start` and `end` are present, `end` must be strictly after `start` and the timezones must match. Replace any of the venue fields. Existing venue fields not present in the patch are NOT preserved — the full object you supply is what gets written, so include every field you want to keep. Convenience — a `https://...` URL is stored as the virtual meeting link, anything else is stored as `venue.address`. If you need finer control over venue fields, use `venue` instead. Maximum number of tickets that can be sold. `0` means unlimited. ISO-4217 currency code. Hide the event from public listings. Whether ticket prices are tax-inclusive. Allow attendees to RSVP (maybe / not going) in addition to confirming. Public URL of the cover image. `open` or `closed`. When `closed`, the public registration form is hidden and new bookings are rejected. Existing attendees are unaffected. `none`, `email`, or `email_and_phone`. Controls what an attendee must provide on the registration form. Absorb the Gomry service fee into the ticket price rather than adding it at checkout. This is the **event-wide default**. Changing it moves every ticket class that has no setting of its own; a class that has been given an explicit `remove_service_fees` keeps it and is unaffected — see [Update Ticket Class](/api-reference/ticket-classes/update-ticket-class). Restrict registrations to specific contact lists. `active` or `inactive`. Allowed list IDs. Message shown when registration is rejected. ## Response Returns the updated event as serialized by `GET /events/{event_id}`. ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Summer Launch Party — Updated", "capacity": 500, "registration_status": "open" }' \ "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt" ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Summer Launch Party — Updated", "status": "active", "registration_status": "open", "currency": "usd", "capacity": 500, "tickets_sold": 12, "start": { "utc": "2026-07-15T22:00:00.000Z", "timezone": "America/New_York" }, "end": { "utc": "2026-07-16T02:00:00.000Z", "timezone": "America/New_York" }, "venue": { "name": "Convention Center, New York", "address": "Convention Center, New York" }, "created_at": "2026-05-24T10:00:00.000Z", "updated_at": "2026-05-24T10:15:00.000Z" } } ``` ```json 400 theme={null} { "error": "At least one field is required" } ``` ```json 400 theme={null} { "error": "end must be after start" } ``` ```json 404 theme={null} { "error": "Event not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'events'.", "required_scope": "events:write" } ``` # Create Experience Source: https://docs.gomry.com/api-reference/experiences/create-experience POST /experiences Create a new experience (recurring offering) in your organization # Create Experience Creates a new experience (recurring offering — yoga class, workshop, coaching slot, …) in your organization. **An experience without a recurrence has no bookable time slots.** The experience template (name, price, duration, location, …) is only half of what an attendee sees. The other half is a **recurrence** — the cadence (frequency, interval, days of week) plus session templates (start time, capacity, provider) that produces the actual bookable date/time grid. This endpoint helps you in two ways so you never ship an unbookable experience: 1. **Auto-default**: if you supply at least one `providers[]` entry and omit `default_recurrence`, the API auto-attaches a starter recurrence (Mon–Fri 9am, UTC, never-ending, capacity = `default_capacity`). You can edit or replace it later via `POST /v1/experiences/{id}/recurrences`. 2. **Explicit `default_recurrence` field**: pass the full recurrence body inline and we create both atomically. If you omit `default_recurrence` **and** pass no providers, the response will include `recurrence_required: true` plus a hint string — the experience exists but won't show slots until you call [Create Recurrence](/api-reference/recurrences/create-recurrence) yourself. Requires the `experiences:write` scope. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key, so two integrations can use the same key value without collision. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Request Body Experience name (max 120 chars). Length of one session in minutes (1..1440). Maximum attendees per session (1..10000). `in_person`, `virtual`, or `hybrid` Venue name Street address Free-text instructions shown to attendees after they book. Required when `type` is `virtual`/`hybrid`. Only `per_session` is supported via the public API today (defaults to `per_session`). ISO 4217 currency code (`USD`, `EUR`, …). Stored lower-cased. Price per session (use `0` for free). Long-form description shown on the public booking page. Defaults: `booking_window: { min_hours_before: 1, max_days_ahead: 60 }` and `cancellation_policy: { refundable_until_hours_before: 24 }`. Cut-off in hours before session start. Maximum days in advance bookings are accepted. Optional `{ currency, amount }`. List of SpaceMember IDs running this experience. **Strongly recommended** — supplying at least one provider triggers the auto-default recurrence behavior described in the warning above. The first ID in the array is used as the provider for the auto-default. Optional. When supplied, a recurrence is created in the same request and the auto-default is skipped. Same shape as the body of [Create Recurrence](/api-reference/recurrences/create-recurrence): `provider_id`, `timezone`, `start_date`, `cadence`, `end_condition`, `session_templates`, and optional `status`. See that endpoint for field-level docs and validation rules. `none`, `email`, or `email_and_phone`. Defaults to `email`. Upper bound on seats per single booking (1..50). Defaults to 5. Space the experience belongs to. Defaults to `GENERAL` (the organization-wide bucket). ## Response Returns the created experience with `201 Created`. When no recurrence could be attached (omitted `default_recurrence` and empty `providers`), the response also carries `recurrence_required: true` plus a `hint` string telling you how to fix it. Same schema as [Get Experience](/api-reference/experiences/get-experience). Present and `true` only when the experience was created without a recurrence. Omitted from the response otherwise. Until you attach a recurrence, the experience will not show bookable slots. Present alongside `recurrence_required: true`. Plain-English instructions on how to make the experience bookable. ```bash With explicit recurrence (recommended) theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Sunset Yoga", "duration_minutes": 60, "default_capacity": 20, "location": { "type": "in_person", "city": "San Francisco" }, "pricing": { "currency": "USD", "amount": 25 }, "providers": ["sm_alice123"], "default_recurrence": { "provider_id": "sm_alice123", "timezone": "America/Los_Angeles", "start_date": "2026-08-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [2, 4] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "18:00", "capacity": 20, "provider_id": "sm_alice123" } ] } }' \ "https://www.gomry.com/api/v1/experiences" ``` ```bash With auto-default (Mon–Fri 9am UTC) theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Sunset Yoga", "duration_minutes": 60, "default_capacity": 20, "location": { "type": "in_person", "city": "San Francisco" }, "pricing": { "currency": "USD", "amount": 25 }, "providers": ["sm_alice123"] }' \ "https://www.gomry.com/api/v1/experiences" ``` ```bash Without providers (no recurrence attached) theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Sunset Yoga", "duration_minutes": 60, "default_capacity": 20, "location": { "type": "in_person", "city": "San Francisco" }, "pricing": { "currency": "USD", "amount": 25 } }' \ "https://www.gomry.com/api/v1/experiences" ``` ```json 201 (recurrence attached) theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Sunset Yoga", "status": "active", "registration_status": "open", "duration_minutes": 60, "default_capacity": 20, "pricing": { "model": "per_session", "currency": "usd", "amount": 25 }, "scheduling_rules": { "booking_window": { "min_hours_before": 1, "max_days_ahead": 60 }, "cancellation_policy": { "refundable_until_hours_before": 24 } }, "created_at": "2025-07-15T10:00:00.000Z", "updated_at": "2025-07-15T10:00:00.000Z" } } ``` ```json 201 (no recurrence — needs follow-up call) theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Sunset Yoga", "status": "active", "...": "..." }, "recurrence_required": true, "hint": "No recurrence was attached. The experience will not show bookable slots until you call POST /v1/experiences/{id}/recurrences. Tip: include `providers: [\"\"]` to auto-generate a Mon–Fri 9am starter recurrence on create." } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "fieldErrors": { "duration_minutes": ["Required"] } } } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'experiences'.", "required_scope": "experiences:write" } ``` # Delete Experience Source: https://docs.gomry.com/api-reference/experiences/delete-experience DELETE /experiences/{experienceId} Soft-delete an experience # Delete Experience Soft-deletes an experience. The experience is hidden from the dashboard, its public page, and the [List Experiences](/api-reference/experiences/list-experiences) endpoint. The underlying record is kept, so the id stays reserved and historical reporting is unaffected. Only experiences owned by your organization can be deleted; any other experience returns `404`. Deleting does **not** cancel or refund the experience's sessions and bookings, and attendees are **not** notified — handle those from the dashboard first if needed. An experience that is already deleted is no longer visible to your key and returns `404`. Requires the `experiences:write` scope. ## Path Parameters The unique identifier of the experience. ## Response Returns `200 OK` with: ```json theme={null} { "deleted": true, "id": "AbCdEfGhIjKlMnOpQrSt" } ``` ## Errors * `404 Experience not found` — the experience does not exist, is already deleted, or is not owned by your organization. * `403 insufficient_scope` — the API key does not have `experiences:write`. ```bash cURL theme={null} curl -X DELETE -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt ``` ```javascript JavaScript theme={null} await fetch("https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt", { method: "DELETE", headers: { "X-API-KEY": "your_api_key" }, }); ``` ```json 200 OK theme={null} { "deleted": true, "id": "AbCdEfGhIjKlMnOpQrSt" } ``` ```json 404 Not Found theme={null} { "error": "Experience not found" } ``` # Get Experience Source: https://docs.gomry.com/api-reference/experiences/get-experience GET /experiences/{experienceId} Retrieve details of a single experience # Get Experience Returns the details of an experience that belongs to your organization. Returns `404` for unknown ids or experiences owned by other organizations (no distinguishing detail is leaked). ## Path Parameters The unique identifier of the experience (Firestore document ID). ## Response Same schema as the items returned by [List Experiences](/api-reference/experiences/list-experiences). ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Sunset Yoga", "status": "active", "registration_status": "open", "duration_minutes": 60, "default_capacity": 20, "location": { "type": "in_person", "city": "San Francisco" }, "pricing": { "model": "per_session", "currency": "usd", "amount": 25 }, "scheduling_rules": { "booking_window": { "min_hours_before": 1, "max_days_ahead": 60 }, "cancellation_policy": { "refundable_until_hours_before": 24 } }, "bookings_count": 87, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } } ``` ```json 404 theme={null} { "error": "Experience not found" } ``` # List Experiences Source: https://docs.gomry.com/api-reference/experiences/list-experiences GET /experiences List all experiences (recurring offerings) for your organization # List Experiences Returns a paginated list of experiences (recurring offerings — yoga classes, workshops, coaching slots) belonging to your organization. Experiences are distinct from Events. An Event is a one-shot ticketed gathering with a fixed start/end. An Experience is a recurring catalog entry that generates bookable sessions on a schedule. ## Query Parameters Page number (starts at 1). Number of experiences per page. Maximum: `200`. Comma-separated list of statuses to include. Allowed values: `active`, `inactive`. Example: `?status=active,inactive` ## Response Unique experience identifier Experience name Description `active` or `inactive` `open` or `closed` — whether new bookings are accepted Length of one session in minutes Maximum attendees per session Upper bound on seats per single booking `none`, `email`, or `email_and_phone` Array of photo URLs First photo, if any `in_person`, `virtual`, or `hybrid` Venue name Street address For `virtual` / `hybrid` `per_session`, `package`, `subscription`, or `dynamic` ISO 4217 currency code Price (for `per_session` / `package` / `subscription`) For `dynamic` pricing For `dynamic` pricing For `dynamic` pricing For `package` pricing `week` or `month` (for `subscription`) Cut-off in hours before session start How far in advance bookings are allowed Optional `{ currency, amount }` Lifetime confirmed bookings When true, the organizer absorbs Gomry's service fee Whether the experience is in tier mode SpaceMember IDs of providers running this experience ISO 8601 creation timestamp ISO 8601 last-updated timestamp Total number of matching experiences Current page number Items per page Total number of pages ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/experiences?page=1&page_size=20" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/experiences?page=1&page_size=20", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await response.json(); ``` ```json 200 theme={null} { "data": [ { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Sunset Yoga", "description": "60-minute beach yoga session at sunset.", "status": "active", "registration_status": "open", "duration_minutes": 60, "default_capacity": 20, "max_booking_quantity": 5, "required_contact_method": "email", "photos": ["https://storage.googleapis.com/..."], "cover_image": "https://storage.googleapis.com/...", "location": { "type": "in_person", "venue": "Ocean Beach", "address": "Ocean Beach", "city": "San Francisco", "state": "CA", "country": "US", "instructions": null, "video_link": null }, "pricing": { "model": "per_session", "currency": "usd", "amount": 25, "base_price": null, "floor_price": null, "ceiling_price": null, "sessions_included": null, "interval": null }, "scheduling_rules": { "booking_window": { "min_hours_before": 1, "max_days_ahead": 60 }, "cancellation_policy": { "refundable_until_hours_before": 24, "late_cancel_fee": null } }, "bookings_count": 87, "remove_service_fees": false, "use_ticket_classes": false, "providers": [], "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } ], "pagination": { "total": 4, "page": 1, "page_size": 20, "total_pages": 1 } } ``` # Update Experience Source: https://docs.gomry.com/api-reference/experiences/update-experience PATCH /experiences/{experience_id} Partially update an experience in your organization # Update Experience Partially updates an experience. Only the fields supplied in the body are written — omitted fields are left unchanged. The experience must belong to the organization associated with the API key; cross-org requests return `404 Not Found` with no information leak. Requires the `experiences:write` scope. ## Path Parameters Experience ID (the service catalog document ID). ## Request Body At least one field is required. Unknown fields are rejected with `400`. Experience name (1–120 chars). Long-form description shown on the public booking page. Length of one session in minutes (1..1440). Maximum attendees per session (1..10000). Replace the location object. `type` is required when `location` is supplied. `in_person`, `virtual`, or `hybrid` Replace the pricing object. Currency is normalised to lowercase before persistence. When the currency changes, all child ticket classes have their pricing currency cascaded automatically — booking flows that validate parent/child currency equality continue to work. Only `per_session` is supported via the public API today (defaults to `per_session`). ISO 4217 currency code. Price per session. Partial scheduling rules. Each nested object (`booking_window`, `cancellation_policy`) is replaced wholesale if supplied; omit a nested object to keep its existing values. Invariants (`min_hours_before >= 0`, `max_days_ahead > 0`, `refundable_until_hours_before >= 0`) are enforced on the merged result. Optional `{ currency, amount }`. Replace the providers list. Pass `[]` to remove all providers. `none`, `email`, or `email_and_phone`. Upper bound on seats per single booking (1..50). `active` or `inactive`. Inactive experiences are hidden from the public catalog and reject new bookings. `open` or `closed`. When `closed` the experience stays publicly visible but the booking CTA is hidden. ## Response Returns the updated experience as serialized by `GET /experiences/{experience_id}`. ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Sunset Yoga — Premium", "pricing": { "model": "per_session", "currency": "EUR", "amount": 35 }, "scheduling_rules": { "booking_window": { "min_hours_before": 2, "max_days_ahead": 90 } } }' \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt" ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "Sunset Yoga — Premium", "status": "active", "registration_status": "open", "duration_minutes": 60, "default_capacity": 20, "pricing": { "model": "per_session", "currency": "eur", "amount": 35 }, "scheduling_rules": { "booking_window": { "min_hours_before": 2, "max_days_ahead": 90 }, "cancellation_policy": { "refundable_until_hours_before": 24 } }, "created_at": "2025-07-15T10:00:00.000Z", "updated_at": "2025-07-15T11:00:00.000Z" } } ``` ```json 400 theme={null} { "error": "At least one field is required" } ``` ```json 400 theme={null} { "error": "defaultCapacity must be >= 1" } ``` ```json 404 theme={null} { "error": "Experience not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'experiences'.", "required_scope": "experiences:write" } ``` # Get Form Source: https://docs.gomry.com/api-reference/forms/get-form GET /forms/{formId} Retrieve a single form by ID # Get Form Returns a single form by ID. The form's `questions` schema lets you interpret application answers. **Required scope:** `forms:read` ## Path Parameters The form's unique ID. ## Response See [List Forms](/api-reference/forms/list-forms) for the full schema. ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" "https://www.gomry.com/api/v1/forms/FORM_ID" ``` # List Forms Source: https://docs.gomry.com/api-reference/forms/list-forms GET /forms List forms (registration questionnaires) for your organization # List Forms Returns a paginated list of forms belonging to your organization. Forms include the question schema you'll use to interpret answers from the [Applications](/api-reference/applications/list-applications) endpoint. **Required scope:** `forms:read` ## Query Parameters Maximum: `200`. Comma-separated. Allowed: `active`, `inactive`. ## Response `active`, `inactive` e.g. `text`, `multipleChoice`, `dropdown`, `email`, `contact_custom_field` If this question maps to a contact field (e.g. `email`, `linkedin`) For `multipleChoice` and `dropdown` For `contact_custom_field`, the custom field ID ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" "https://www.gomry.com/api/v1/forms" ``` # Get List Source: https://docs.gomry.com/api-reference/lists/get-list GET /lists/{listId} Retrieve a single list by ID # Get List **Required scope:** `lists:read` ## Path Parameters ## Response See [List Lists](/api-reference/lists/list-lists) for the schema. # List Contacts in a List Source: https://docs.gomry.com/api-reference/lists/list-list-contacts GET /lists/{listId}/contacts List contacts that belong to a given list # List Contacts in a List Returns the contacts that are members of the given list. **Required scopes:** `lists:read` and `contacts:read` ## Path Parameters ## Query Parameters Maximum: `200`. ## Response Each item in `data` is a [Contact object](/api-reference/contacts/list-contacts). # List Lists Source: https://docs.gomry.com/api-reference/lists/list-lists GET /lists List contact lists (segments) for your organization # List Lists Returns a paginated list of contact lists (segments). **Required scope:** `lists:read` ## Query Parameters Maximum: `200`. ## Response `public` or `private` # Get Payment Source: https://docs.gomry.com/api-reference/payments/get-payment GET /payments/{paymentId} Retrieve a single payment by ID # Get Payment **Required scope:** `payments:read` ## Path Parameters ## Response See [List Payments](/api-reference/payments/list-payments) for the full schema. # List Payments Source: https://docs.gomry.com/api-reference/payments/list-payments GET /payments List payments for your organization # List Payments Returns a paginated list of payments. Defaults to status `succeeded` — pass `status` to include other states. **Required scope:** `payments:read` ## Query Parameters Maximum: `200`. Comma-separated. Allowed: `succeeded`, `pending`, `refunded`, `failed`, `checkout_started`, `authorized`, `authorization_expired`. ISO 8601 timestamp. Returns payments created on or after this time. ISO 8601 timestamp. Returns payments created on or before this time. ## Response ISO 4217 In the smallest currency unit (e.g. cents) User ID of the customer Linked form submission, if any e.g. `subscription_create`, `one_time_payment` For joining with Stripe-side records ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/payments?event_id=EVENT_ID&from=2025-01-01" ``` # Create Recurrence Source: https://docs.gomry.com/api-reference/recurrences/create-recurrence POST /experiences/{experience_id}/recurrences Attach a schedule to an experience so it produces bookable slots # Create Recurrence Attaches a schedule to an existing experience. The experience template itself must already exist (create it first via [Create Experience](/api-reference/experiences/create-experience)). The recurrence defines a **cadence** (how often), an **end condition** (when to stop), and one or more **session templates** (what time each day, with what capacity and provider). Once an active recurrence exists, the public booking page expands its rules on-the-fly into the date/time grid — no separate "publish slots" step is required. Requires the `experiences:write` scope. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key, so two integrations can use the same key value without collision. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Path Parameters Parent experience ID. Must belong to the API key's organization. ## Request Body Default provider (SpaceMember) for this recurrence. Used by session templates that don't specify their own `provider_id`. IANA timezone the schedule is anchored to (e.g. `America/New_York`). Session start times are interpreted in this zone. First date the recurrence is active. Accepts an ISO-8601 datetime or a `YYYY-MM-DD` date string. `daily`, `weekly`, or `monthly`. Every-N (1..52). `interval: 2` with `frequency: "weekly"` = every other week. Active weekdays for `weekly` cadence. 0=Sun..6=Sat. Discriminated union, one of: `{ type: "date", until: "" }` `{ type: "count", occurrences: <1..500> }` `{ type: "never" }` At least one template, up to 336. Each template materializes into one session per occurrence date. `HH:mm` 24-hour, local to `timezone`. Seats per occurrence (1..10000). SpaceMember ID hosting this slot. Optional per-template filter (0=Sun..6=Sat). When omitted, the template runs on every active day of the recurrence — letting you mix different hours per day (e.g. Mon–Fri 9–5 + Sat 10–14). Two templates can share a `start_time_of_day` only if their `days_of_week` are disjoint. `active` or `paused`. Defaults to `active`. ## Response Returns the created recurrence with `201 Created`. Same schema as [Get Recurrence](/api-reference/recurrences/get-recurrence). ```bash cURL theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "provider_id": "prov_xyz", "timezone": "America/New_York", "start_date": "2026-09-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [1, 3] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "09:00", "capacity": 12, "provider_id": "prov_xyz" }, { "start_time_of_day": "17:00", "capacity": 12, "provider_id": "prov_xyz" } ] }' \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt/recurrences" ``` ```json 201 theme={null} { "data": { "id": "rec_AbCdEfGh", "service_id": "AbCdEfGhIjKlMnOpQrSt", "provider_id": "prov_xyz", "timezone": "America/New_York", "start_date": "2026-09-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [1, 3] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "09:00", "capacity": 12, "provider_id": "prov_xyz", "days_of_week": null }, { "start_time_of_day": "17:00", "capacity": 12, "provider_id": "prov_xyz", "days_of_week": null } ], "status": "active", "created_at": "2026-05-25T10:00:00.000Z", "updated_at": "2026-05-25T10:00:00.000Z" } } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "fieldErrors": { "session_templates": ["At least one start time required"] } } } ``` ```json 404 theme={null} { "error": "Experience not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'experiences'.", "required_scope": "experiences:write" } ``` # Delete Recurrence Source: https://docs.gomry.com/api-reference/recurrences/delete-recurrence DELETE /experiences/{experience_id}/recurrences/{recurrence_id} Soft-delete a schedule # Delete Recurrence Soft-deletes a recurrence. Future empty slots immediately disappear from the booking page; sessions with existing bookings retain their snapshotted state so attendees aren't surprised. If you want to keep the recurrence on file without producing new slots, use [Update Recurrence](/api-reference/recurrences/update-recurrence) with `{ "status": "paused" }` instead — paused recurrences can be re-activated; deleted recurrences cannot. Requires the `experiences:write` scope. ## Path Parameters Parent experience ID. Recurrence ID. ## Response Returns `204 No Content` on success. ```bash cURL theme={null} curl -X DELETE \ -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt/recurrences/rec_AbCdEfGh" ``` ```text 204 theme={null} (empty body) ``` ```json 404 theme={null} { "error": "Recurrence not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'experiences'.", "required_scope": "experiences:write" } ``` # Get Recurrence Source: https://docs.gomry.com/api-reference/recurrences/get-recurrence GET /experiences/{experience_id}/recurrences/{recurrence_id} Retrieve a single recurrence by ID # Get Recurrence Returns a single recurrence. Both the parent experience and the recurrence must belong to the API key's organization; cross-org or mismatched parent IDs return `404 Not Found` with no information leak. Requires the `experiences:read` scope. ## Path Parameters The parent experience ID. The recurrence ID. ## Response Recurrence ID. Parent experience ID. Default provider (SpaceMember) for this recurrence. IANA timezone the schedule is anchored to. First date the recurrence is active (ISO-8601 datetime or `YYYY-MM-DD`). `daily`, `weekly`, or `monthly`. Every-N (e.g. `interval: 2` with `frequency: "weekly"` = every other week). Active weekdays (0=Sun..6=Sat). `null` when not constrained. Discriminated union — `{ type: "date", until }`, `{ type: "count", occurrences }`, or `{ type: "never" }`. `HH:mm` 24-hour, local to `timezone`. Seats per occurrence. SpaceMember hosting this slot. Optional per-template day filter; `null` = runs on every active day of the recurrence. `active` or `paused`. Paused recurrences are kept on file but don't produce bookable slots. ISO-8601. ISO-8601. ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt/recurrences/rec_AbCdEfGh" ``` ```json 200 theme={null} { "data": { "id": "rec_AbCdEfGh", "service_id": "AbCdEfGhIjKlMnOpQrSt", "provider_id": "prov_xyz", "timezone": "America/New_York", "start_date": "2026-09-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [1, 3] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "09:00", "capacity": 12, "provider_id": "prov_xyz", "days_of_week": null } ], "status": "active", "created_at": "2026-05-25T10:00:00.000Z", "updated_at": "2026-05-25T10:00:00.000Z" } } ``` ```json 404 theme={null} { "error": "Recurrence not found" } ``` # List Recurrences Source: https://docs.gomry.com/api-reference/recurrences/list-recurrences GET /experiences/{experience_id}/recurrences List the schedules attached to an experience # List Recurrences Returns every active schedule (recurrence) attached to an experience. Soft-deleted recurrences are not returned. A **recurrence** is the rule that produces bookable time slots — a cadence (frequency + interval + days of week), an end condition, and one or more **session templates** (start time, capacity, provider). Without at least one active recurrence, the experience page renders no time slots. Requires the `experiences:read` scope. The parent experience must belong to the API key's organization; cross-org requests return `404 Not Found`. ## Path Parameters Experience ID (the service catalog document ID). ## Response An array of recurrence objects, each with the shape returned by [Get Recurrence](/api-reference/recurrences/get-recurrence). ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt/recurrences" ``` ```json 200 theme={null} { "data": [ { "id": "rec_AbCdEfGh", "service_id": "AbCdEfGhIjKlMnOpQrSt", "provider_id": "prov_xyz", "timezone": "America/New_York", "start_date": "2026-09-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [1, 3] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "09:00", "capacity": 12, "provider_id": "prov_xyz", "days_of_week": null } ], "status": "active", "created_at": "2026-05-25T10:00:00.000Z", "updated_at": "2026-05-25T10:00:00.000Z" } ] } ``` ```json 404 theme={null} { "error": "Experience not found" } ``` # Update Recurrence Source: https://docs.gomry.com/api-reference/recurrences/update-recurrence PATCH /experiences/{experience_id}/recurrences/{recurrence_id} Edit an existing schedule # Update Recurrence Edits an existing schedule. Only the fields supplied in the body are written. Nested objects (`cadence`, `end_condition`, `session_templates`) are replaced **wholesale** when present — the recurrence rule is treated as a single versioned definition rather than a deep-merge target. Editing the rule retroactively updates every still-empty future slot. Sessions that already have bookings keep their snapshotted capacity/provider — attendees are never surprised by a silent change to something they've already booked. Requires the `experiences:write` scope. ## Path Parameters Parent experience ID. Recurrence ID. ## Request Body At least one field is required. Unknown fields are rejected with `400`. Default provider for this recurrence. IANA timezone (e.g. `America/New_York`). ISO-8601 datetime or `YYYY-MM-DD`. Full cadence object — same shape as on create. Supplying this field replaces the entire previous cadence. Full end-condition object — replaces the previous one. Full template list — replaces the previous one. Supplying an empty list is rejected. `active` or `paused`. Pause to keep the recurrence on file without producing new bookable slots. ## Response Returns the updated recurrence with the shape from [Get Recurrence](/api-reference/recurrences/get-recurrence). ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "status": "paused" }' \ "https://www.gomry.com/api/v1/experiences/AbCdEfGhIjKlMnOpQrSt/recurrences/rec_AbCdEfGh" ``` ```json 200 theme={null} { "data": { "id": "rec_AbCdEfGh", "service_id": "AbCdEfGhIjKlMnOpQrSt", "status": "paused", "provider_id": "prov_xyz", "timezone": "America/New_York", "start_date": "2026-09-01", "cadence": { "frequency": "weekly", "interval": 1, "days_of_week": [1, 3] }, "end_condition": { "type": "never" }, "session_templates": [ { "start_time_of_day": "09:00", "capacity": 12, "provider_id": "prov_xyz", "days_of_week": null } ], "created_at": "2026-05-25T10:00:00.000Z", "updated_at": "2026-05-25T11:30:00.000Z" } } ``` ```json 400 theme={null} { "error": "At least one field is required" } ``` ```json 404 theme={null} { "error": "Recurrence not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'experiences'.", "required_scope": "experiences:write" } ``` # Create Space Source: https://docs.gomry.com/api-reference/spaces/create-space POST /spaces Create a new space in your organization # Create Space Creates a new space in your organization. The created space is **immediately usable** as a `space_identifier` on [Create Event](/api-reference/events/create-event), [Create Experience](/api-reference/experiences/create-experience), and the other space-scoped endpoints. This is the building block for the **merchant-of-record / marketplace** pattern: provision one space per seller, then set each space's tax and legal-entity details with [Set Tax Details](/api-reference/spaces/set-tax-details) so receipts and VAT are attributed correctly. Requires the `spaces:write` scope. `identifier` must be unique within your organization and is used verbatim as `space_identifier` elsewhere. `name` must also be unique (case-insensitive). `GENERAL` is reserved for the organization-wide scope and cannot be used. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Request Body Display name (max 200 chars). Must be unique within the organization (case-insensitive). URL-safe slug used as `space_identifier` elsewhere. 1–64 characters: letters, numbers, hyphen (`-`) or underscore (`_`). Must be unique within the organization. `GENERAL` is reserved. Accent color token. Defaults to `zinc`. Icon name or emoji. Defaults to `general`. `emoji` or `icon`. Defaults to `emoji`. Contact email for the space (shown on receipts). Social profile URL. Social profile URL. Social profile URL. Social profile URL. Website URL. ## Response Returns the created space with `201 Created` — same schema as [Get Space](/api-reference/spaces/get-space). Tax details start `null`; set them with [Set Tax Details](/api-reference/spaces/set-tax-details). ```bash cURL theme={null} curl -X POST \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Milan", "identifier": "milan", "email": "milan@acme.com" }' \ "https://www.gomry.com/api/v1/spaces" ``` ```json 201 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "identifier": "milan", "name": "Milan", "status": "active", "color": "zinc", "icon": "general", "icon_type": "emoji", "email": "milan@acme.com", "tax_details": null, "created_at": "2025-07-15T10:00:00.000Z", "updated_at": "2025-07-15T10:00:00.000Z" } } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "fieldErrors": { "identifier": ["identifier must be 1–64 chars: letters, numbers, hyphen or underscore"] } } } ``` ```json 409 theme={null} { "error": "Space already exists", "details": { "identifier": "A space with this identifier already exists" } } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'spaces'.", "required_scope": "spaces:write" } ``` # Get Space Source: https://docs.gomry.com/api-reference/spaces/get-space GET /spaces/{spaceIdentifier} Retrieve a single space by its identifier # Get Space Returns a single space by its `identifier`. The space must belong to the organization associated with the API key — an identifier that doesn't resolve under your org returns `404` (no cross-tenant existence leak). `GENERAL` is the synthetic organization-wide scope, not a persisted space, so it also returns `404` here. Requires the `spaces:read` scope. ## Path Parameters The space's `identifier` (its slug), e.g. `milan`. ## Response A Space object — same schema as [List Spaces](/api-reference/spaces/list-spaces). ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/spaces/milan" ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "identifier": "milan", "name": "Milan", "status": "active", "color": "zinc", "icon": "general", "icon_type": "emoji", "email": "milan@acme.com", "tax_details": { "tax_type": "VAT", "vat_number": "IT12345678901", "vat_rate": 22, "legal_entity_name": "ACME Milan SRL", "legal_entity_address": "Via Roma 1, 20121 Milano", "country": "IT", "is_eu": true, "is_valid_vat_number": true, "notes_on_receipts": null }, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } } ``` ```json 404 theme={null} { "error": "Space not found" } ``` # List Spaces Source: https://docs.gomry.com/api-reference/spaces/list-spaces GET /spaces List the spaces (sub-divisions) in your organization # List Spaces Returns a paginated list of the **active** spaces in your organization. A space is a sub-division of an organization for running separate events — different cities, brands, teams, or (for a marketplace) sellers. Every event, experience, and contact belongs to a space and references it by its `identifier` (the `space_identifier` field on other endpoints). The organization-wide **`GENERAL`** scope is synthetic — it is not a persisted space and does not appear in this list. Target it elsewhere by omitting `space_identifier`. Requires the `spaces:read` scope. ## Query Parameters Page number (starts at 1). Number of spaces per page. Maximum: `200`. ## Response Unique space identifier (document ID) The human-set slug used as `space_identifier` elsewhere Display name `active` Accent color token Icon name or emoji `emoji` or `icon` Contact email shown on receipts `VAT` or `Sales Tax` Percentage (0–100) ISO 8601 creation timestamp ISO 8601 last-updated timestamp Total number of active spaces Current page number Items per page Total number of pages ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ "https://www.gomry.com/api/v1/spaces?page=1&page_size=20" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/spaces?page=1&page_size=20", { headers: { "X-API-KEY": "your_api_key" } } ); const { data, pagination } = await response.json(); ``` ```json 200 theme={null} { "data": [ { "id": "AbCdEfGhIjKlMnOpQrSt", "identifier": "milan", "name": "Milan", "status": "active", "color": "zinc", "icon": "general", "icon_type": "emoji", "email": "milan@acme.com", "linkedin": null, "instagram": null, "facebook": null, "twitter": null, "website": null, "tax_details": { "tax_type": "VAT", "vat_number": "IT12345678901", "vat_rate": 22, "legal_entity_name": "ACME Milan SRL", "legal_entity_address": "Via Roma 1, 20121 Milano", "country": "IT", "is_eu": true, "is_valid_vat_number": true, "notes_on_receipts": null }, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-06-20T15:30:00.000Z" } ], "pagination": { "total": 1, "page": 1, "page_size": 20, "total_pages": 1 } } ``` # Set Tax Details Source: https://docs.gomry.com/api-reference/spaces/set-tax-details PUT /spaces/{spaceIdentifier}/tax-details Set the tax and legal-entity details for a space # Set Tax Details Sets the **tax / legal-entity details** for a space: the legal entity name and address, VAT number and rate, tax type, country, and receipt notes applied to that space's sales and receipts. This is the merchant-of-record knob. A marketplace using Gomry as merchant of record can provision one space per seller ([Create Space](/api-reference/spaces/create-space)) and configure each seller's tax identity here, so every sale is billed and receipted under the correct entity. Requires the `spaces:write` scope. This endpoint uses **PUT (full replace)** semantics: the request replaces the entire `tax_details` object. Any field you omit is reset to its default (empty string / `0` / `false`). Send the complete set of values you want each time. This capability is intentionally **not** exposed through the AI assistant (Aven) or the MCP server — tax identity affects what buyers are charged and what their receipts say, so it is changed only through this explicitly-scoped API key endpoint or the dashboard. `GENERAL` has no persisted document and therefore no tax details. ## Path Parameters The space's `identifier` (its slug). ## Request Body `VAT` or `Sales Tax`. The legal entity's VAT / tax registration number. Tax rate as a percentage, `0`–`100` (e.g. `22` for Italy's 22% VAT). Registered legal entity name that appears on receipts. Registered legal entity address that appears on receipts. Country of the legal entity (ISO code or name). Whether the entity is in the EU (affects VAT handling). Whether the VAT number has been validated. You may set this if you validate externally; Gomry does not re-run VIES validation on this endpoint. Free-text note printed on this space's receipts. Optional. Updates the space's contact email (stored on the space, not inside `tax_details`). ## Response Returns the updated space — same schema as [Get Space](/api-reference/spaces/get-space) — with the new `tax_details`. ```bash cURL theme={null} curl -X PUT \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "tax_type": "VAT", "vat_number": "IT12345678901", "vat_rate": 22, "legal_entity_name": "ACME Milan SRL", "legal_entity_address": "Via Roma 1, 20121 Milano", "country": "IT", "is_eu": true, "notes_on_receipts": "Grazie per il tuo acquisto" }' \ "https://www.gomry.com/api/v1/spaces/milan/tax-details" ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "identifier": "milan", "name": "Milan", "status": "active", "tax_details": { "tax_type": "VAT", "vat_number": "IT12345678901", "vat_rate": 22, "legal_entity_name": "ACME Milan SRL", "legal_entity_address": "Via Roma 1, 20121 Milano", "country": "IT", "is_eu": true, "is_valid_vat_number": false, "notes_on_receipts": "Grazie per il tuo acquisto" }, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-07-15T12:30:00.000Z" } } ``` ```json 400 theme={null} { "error": "Validation failed", "details": { "fieldErrors": { "vat_rate": ["Number must be less than or equal to 100"] } } } ``` ```json 404 theme={null} { "error": "Space not found" } ``` ```json 403 theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'write' access to 'spaces'.", "required_scope": "spaces:write" } ``` # Update Space Source: https://docs.gomry.com/api-reference/spaces/update-space PATCH /spaces/{spaceIdentifier} Update a space's profile fields # Update Space Partial update of a space's profile fields — name, color, icon, contact email, and social links. Only the fields you supply are written. Tax and legal-entity details are **not** set here — use [Set Tax Details](/api-reference/spaces/set-tax-details). Deactivating or deleting a space remains a dashboard-only operation for now. Requires the `spaces:write` scope. Tenant-fenced on your organization — an identifier that doesn't resolve under your org returns `404`. ## Path Parameters The space's `identifier` (its slug). ## Request Body At least one field is required. New display name (max 200 chars). Must stay unique within the organization (case-insensitive). Accent color token. Icon name or emoji. `emoji` or `icon`. Contact email (shown on receipts). Social profile URL. Social profile URL. Social profile URL. Social profile URL. Website URL. The space's `identifier` is immutable via the API — it's referenced by existing events and experiences. Create a new space if you need a different identifier. ## Response Returns the updated space — same schema as [Get Space](/api-reference/spaces/get-space). ```bash cURL theme={null} curl -X PATCH \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Milan HQ", "website": "https://milan.acme.com" }' \ "https://www.gomry.com/api/v1/spaces/milan" ``` ```json 200 theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "identifier": "milan", "name": "Milan HQ", "status": "active", "website": "https://milan.acme.com", "tax_details": null, "created_at": "2025-03-01T10:00:00.000Z", "updated_at": "2025-07-15T12:00:00.000Z" } } ``` ```json 400 theme={null} { "error": "At least one field is required" } ``` ```json 404 theme={null} { "error": "Space not found" } ``` # Create Ticket Class Source: https://docs.gomry.com/api-reference/ticket-classes/create-ticket-class POST /events/{eventId}/ticket-classes Create a new ticket class (tier) on an event # Create Ticket Class Creates a new ticket class (a tier — General Admission, VIP, Early Bird, …) on an event your API key's organization owns. Requires the `ticket_classes:write` scope. ## Idempotency Pass an `Idempotency-Key` header (max 255 chars) to make POST retries safe. The first request executes the create; subsequent requests with the same key within 24 hours replay the original response verbatim — including the original status code — and add an `Idempotent-Replay: true` response header. Keys are scoped per API key. Two concurrent requests with the same key return `409 idempotent_request_in_progress` to the second caller. ## Path Parameters The unique identifier of the event. ## Request Body Ticket class name (1..120 chars). Must be unique among active ticket classes on the event. Optional description shown to attendees (max 10000 chars). ISO 4217 currency code (`USD`, `EUR`, …). Price per ticket. Pass `0` for free tickets. Marks the class as free. Defaults to `true` when `cost` is `0`. Total tickets available. Pass `0` (default) for unlimited. ISO 8601 datetime when ticket sales open. ISO 8601 datetime when ticket sales close. Minimum tickets per order (default `1`). Maximum tickets of this class allowed in a **single order**. `0` or omitted means unlimited, which is the default. Not a per-person limit: the same buyer can place another order. When `true`, new registrations land in `pending_approval` instead of `valid`. Default `false`. Absorb the Gomry service fee on **this ticket class** instead of adding it at checkout, overriding the event-level `remove_service_fees`. **Omit it to inherit the event** — there is no default on purpose. Sending an explicit value pins this class, so later changes to the event-wide setting no longer move it. Because the setting is per class, one event can absorb the fee on some tiers and pass it on for others (e.g. cover it on a members tier while general admission still pays it). Hide this ticket class from the public event page. Default `false`. ## Response Returns `201 Created` with the newly created ticket class in the same shape as [Get Ticket Class](/api-reference/ticket-classes/get-ticket-class). ## Errors * `400 Validation failed` — invalid body shape (see `details` for the Zod error). * `404 Event not found` — the event does not belong to your organization, or does not exist. * `409 A ticket class with this name already exists` — another active class on this event already uses this name. ```bash cURL theme={null} curl -X POST -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ticket-class-$(uuidgen)" \ -d '{ "name": "Early Bird", "description": "Limited release", "currency": "USD", "cost": 25, "quantity_total": 100, "minimum_per_order": 1, "maximum_per_order": 4 }' \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ name: "Early Bird", currency: "USD", cost: 25, quantity_total: 100, }), } ); const { data } = await response.json(); ``` ```json theme={null} { "data": { "id": "prod_AbCdEf", "name": "Early Bird", "description": "Limited release", "status": "active", "currency": "USD", "cost": 25, "free": false, "quantity_total": 100, "quantity_sold": 0, "sales_start": null, "sales_end": null, "minimum_per_order": 1, "maximum_per_order": 4, "require_approval": false, "remove_service_fees": null, "remove_service_fees_effective": false, "created_at": "2026-05-26T12:00:00.000Z", "updated_at": "2026-05-26T12:00:00.000Z" } } ``` # Delete Ticket Class Source: https://docs.gomry.com/api-reference/ticket-classes/delete-ticket-class DELETE /events/{eventId}/ticket-classes/{ticketClassId} Soft-delete a ticket class # Delete Ticket Class Soft-deletes a ticket class. The class is hidden from listing endpoints but remains queryable by ID for historical purposes. The class is also removed from the event's display ordering. **A ticket class with sold tickets cannot be deleted** — refund or move attendees first. Requires the `ticket_classes:write` scope. ## Path Parameters The unique identifier of the event. The unique identifier of the ticket class. ## Response Returns `200 OK` with: ```json theme={null} { "deleted": true, "id": "prod_AbCdEf" } ``` ## Errors * `404 Event not found` / `Ticket class not found` — resource missing or not owned by your org. * `409 Cannot delete a ticket class with sold tickets` — at least one ticket exists in `valid` or `checked_in` state for this class. Response body includes `sold_count`. ```bash cURL theme={null} curl -X DELETE -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_AbCdEf ``` ```javascript JavaScript theme={null} await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_AbCdEf", { method: "DELETE", headers: { "X-API-KEY": "your_api_key" } } ); ``` ```json 200 OK theme={null} { "deleted": true, "id": "prod_AbCdEf" } ``` ```json 409 Conflict theme={null} { "error": "Cannot delete a ticket class with sold tickets", "sold_count": 7 } ``` # Get Ticket Class Source: https://docs.gomry.com/api-reference/ticket-classes/get-ticket-class GET /events/{eventId}/ticket-classes/{ticketClassId} Retrieve details of a single ticket class # Get Ticket Class Returns the details of a single ticket class for an event. ## Path Parameters The unique identifier of the event. The unique identifier of the ticket class (product document ID). ## Response Unique ticket class identifier Ticket class name Ticket class description `active`, `archived`, or `deleted` ISO 4217 currency code Price per ticket Whether this is a free ticket class Total tickets available Number of tickets sold ISO 8601 datetime when sales open ISO 8601 datetime when sales close Minimum tickets per order Maximum tickets of this class allowed in a single order. `0` means unlimited. Whether registration requires manual approval This ticket class's **own** absorb setting. `true` = the organizer absorbs the Gomry service fee on this class; `false` = the buyer pays it on top; `null` = this class has no setting of its own and inherits the event's `remove_service_fees`. What actually applies to this class after inheritance is resolved. Use this field to price a ticket — it already accounts for the event-level default, so you never need to re-implement the precedence rule. ISO 8601 creation timestamp ISO 8601 last-updated timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_vip ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_vip", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": { "id": "prod_vip", "name": "VIP", "description": "Premium access with backstage pass", "status": "active", "currency": "USD", "cost": 150, "free": false, "quantity_total": 100, "quantity_sold": 34, "sales_start": "2025-03-01T00:00:00.000Z", "sales_end": "2025-07-14T00:00:00.000Z", "minimum_per_order": 1, "maximum_per_order": 2, "require_approval": false, "remove_service_fees": null, "remove_service_fees_effective": false, "created_at": "2025-02-15T10:30:00.000Z", "updated_at": "2025-06-20T08:00:00.000Z" } } ``` ```json 404 theme={null} { "error": "Ticket class not found" } ``` # List Ticket Classes Source: https://docs.gomry.com/api-reference/ticket-classes/list-ticket-classes GET /events/{eventId}/ticket-classes List all ticket classes for an event # List Ticket Classes Returns all active ticket classes (ticket types) for an event, ordered by the event's configured display order. ## Path Parameters The unique identifier of the event. ## Response Unique ticket class identifier Ticket class name (e.g., "General Admission", "VIP") Ticket class description `active`, `archived`, or `deleted` ISO 4217 currency code Price per ticket (0 for free tickets) Whether this is a free ticket class Total tickets available (null = unlimited) Number of tickets sold ISO 8601 datetime when sales open ISO 8601 datetime when sales close Minimum tickets per order Maximum tickets of this class allowed in a single order. `0` means unlimited. Whether registration requires manual approval This ticket class's **own** absorb setting. `true` = the organizer absorbs the Gomry service fee on this class; `false` = the buyer pays it on top; `null` = this class has no setting of its own and inherits the event's `remove_service_fees`. What actually applies to this class after inheritance is resolved. Use this field to price a ticket — it already accounts for the event-level default, so you never need to re-implement the precedence rule. ISO 8601 creation timestamp ISO 8601 last-updated timestamp ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key" \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes", { headers: { "X-API-KEY": "your_api_key" } } ); const { data } = await response.json(); ``` ```json 200 theme={null} { "data": [ { "id": "prod_general", "name": "General Admission", "description": "Standard entry to the event", "status": "active", "currency": "USD", "cost": 0, "free": true, "quantity_total": 400, "quantity_sold": 200, "sales_start": "2025-03-01T00:00:00.000Z", "sales_end": "2025-07-15T14:00:00.000Z", "minimum_per_order": 1, "maximum_per_order": 5, "require_approval": false, "remove_service_fees": null, "remove_service_fees_effective": false, "created_at": "2025-02-15T10:00:00.000Z", "updated_at": "2025-06-20T08:00:00.000Z" }, { "id": "prod_vip", "name": "VIP", "description": "Premium access with backstage pass", "status": "active", "currency": "USD", "cost": 150, "free": false, "quantity_total": 100, "quantity_sold": 34, "sales_start": "2025-03-01T00:00:00.000Z", "sales_end": "2025-07-14T00:00:00.000Z", "minimum_per_order": 1, "maximum_per_order": 2, "require_approval": false, "created_at": "2025-02-15T10:30:00.000Z", "updated_at": "2025-06-20T08:00:00.000Z" } ] } ``` # Update Ticket Class Source: https://docs.gomry.com/api-reference/ticket-classes/update-ticket-class PATCH /events/{eventId}/ticket-classes/{ticketClassId} Partial update of a ticket class # Update Ticket Class Updates one or more fields of a ticket class. `quantity_sold` is always recomputed server-side from the tickets collection and cannot be set by the client. Requires the `ticket_classes:write` scope. ## Path Parameters The unique identifier of the event. The unique identifier of the ticket class. ## Request Body All fields are optional but **at least one must be provided**. Fields not included are left unchanged. Ticket class name (1..120 chars). Description (max 10000 chars). ISO 4217 currency code. Price per ticket. Marks the class as free. Total tickets available (0 = unlimited). ISO 8601 datetime when sales open. ISO 8601 datetime when sales close. Minimum tickets per order. Maximum tickets of this class allowed in a **single order**. `0` or omitted means unlimited, which is the default. Not a per-person limit: the same buyer can place another order. Whether new registrations require approval. Absorb the Gomry service fee on **this ticket class** instead of adding it at checkout, overriding the event-level `remove_service_fees`. Three distinct behaviours: * `true` / `false` — set this class's own setting explicitly. * `null` — **clear** the override so the class inherits the event's setting again. * omitted — leave the current setting untouched. Hide from the public event page. ## Response Returns `200 OK` with the updated ticket class in the same shape as [Get Ticket Class](/api-reference/ticket-classes/get-ticket-class). ## Errors * `400 At least one field is required` — empty body. * `400 Validation failed` — invalid body shape. * `404 Event not found` / `Ticket class not found` — resource missing or not owned by your org. ```bash cURL theme={null} curl -X PATCH -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "cost": 30, "quantity_total": 200 }' \ https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_AbCdEf ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/AbCdEfGhIjKlMnOpQrSt/ticket-classes/prod_AbCdEf", { method: "PATCH", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ cost: 30, quantity_total: 200 }), } ); const { data } = await response.json(); ``` # Authentication Source: https://docs.gomry.com/authentication Authenticate your API requests with API keys # Authentication All requests to the Gomry API must include a valid API key. API keys are scoped to your **organization** and gated by a per-resource **scope map** that controls which resources the key can read or write. See [Scopes](/scopes) for the full list and how to configure them. ## Generating an API Key 1. Log in to your [Gomry dashboard](https://www.gomry.com) 2. Navigate to **Organization Settings → API Keys** 3. Click **Create API Key** and give it a descriptive name 4. Copy the key immediately — **it is only displayed once** Treat your API key like a password. Anyone with your key can read your organization's event data. Never commit it to version control or expose it in client-side code. ## Using Your API Key Include the key in the `X-API-KEY` header on every request: ```bash cURL theme={null} curl -H "X-API-KEY: your_api_key_here" \ https://www.gomry.com/api/v1/events/YOUR_EVENT_ID ``` ```javascript JavaScript theme={null} const response = await fetch( "https://www.gomry.com/api/v1/events/YOUR_EVENT_ID", { headers: { "X-API-KEY": "your_api_key_here", }, } ); const { data } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://www.gomry.com/api/v1/events/YOUR_EVENT_ID", headers={"X-API-KEY": "your_api_key_here"}, ) data = response.json()["data"] ``` ## Security Best Practices Never hardcode API keys in your source code. Use environment variables or a secrets manager. ```bash theme={null} export GOMRY_API_KEY="your_api_key_here" ``` API keys should only be used in server-side code. Never include them in frontend JavaScript, mobile apps, or any code that runs on user devices. Create a new key and revoke the old one periodically. You can manage keys in your dashboard under **Organization Settings → API Keys**. Create a dedicated key for each integration or service. This way, if a key is compromised, you can revoke it without affecting other integrations. ## Authentication Errors | Status | Error | Meaning | | ------ | ---------------------------- | --------------------------------------------------------------------------------------- | | `401` | `Missing X-API-KEY header` | No API key was provided in the request | | `401` | `Invalid or revoked API key` | The key doesn't exist or has been revoked | | `403` | `insufficient_scope` | The key is valid but lacks the required scope for this resource (see [Scopes](/scopes)) | # Checkout Errors Source: https://docs.gomry.com/commerce/errors Error codes returned by the ACP checkout endpoints # Checkout Errors The checkout endpoints use the ACP error envelope, which differs from the [standard Gomry error format](/errors) used everywhere else in this API. The catalog endpoints use the standard format. ```json theme={null} { "type": "invalid_request", "code": "sold_out", "message": "General Admission is sold out.", "param": "items[0].id" } ``` `invalid_request`, `processing_error`, or `service_unavailable`. A coarse class for clients that do not switch on `code`. The specific reason. See the table below. Human-readable. Safe to log; not intended to be shown to a buyer verbatim. Present when one field caused the failure — for example `items[0].id`. Every error response also carries `API-Version`, and echoes `Request-Id` and `Idempotency-Key` when you sent them. Correlating a failure is exactly when those headers matter most. ## Codes | Code | Status | Type | Meaning | | ------------------------- | ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_body` | `400` | `invalid_request` | The body failed validation. Check `param`. Also returned for an **unexpected field** — see the warning below. | | `invalid_header` | `400` | `invalid_request` | A required header is missing or malformed. | | `unsupported_api_version` | `400` | `invalid_request` | The `API-Version` header names a version other than `2025-09-12`. An **absent** header is accepted and treated as the current version. | | `item_not_found` | `400` | `invalid_request` | No ticket type matches `items[].id`. Usually an **event id passed where a ticket type id belongs**. | | `multiple_events` | `400` | `invalid_request` | The cart mixes ticket types from different events. One session sells one event. | | `currency_mismatch` | `400` | `invalid_request` | The cart mixes currencies. | | `invalid_signature` | `401` | `invalid_request` | The signature failed, the timestamp is outside the ±5 minute window, or the key is not on the ACP partner allowlist. | | `payment_declined` | `402` | `processing_error` | The delegated payment token was declined. | | `payment_requires_action` | `402` | `processing_error` | The payment needs additional authentication (3DS). | | `session_not_found` | `404` | `invalid_request` | No such session for this key. Sessions are scoped to the key that created them. | | `invalid_session_state` | `409` | `invalid_request` | The operation is not legal in the session's current state — completing a canceled session, for instance. | | `sold_out` | `409` | `invalid_request` | Not enough inventory remains. | | `quantity_limit_exceeded` | `409` | `invalid_request` | The requested quantity exceeds the organizer's per-order cap. | | `not_on_sale` | `409` | `invalid_request` | Sales have not opened, or have closed. | | `amount_mismatch` | `409` | `processing_error` | The total changed between quote and completion. Re-read the session and confirm the new total with your buyer. | | `request_not_idempotent` | `409` | `invalid_request` | A request with the same `Idempotency-Key` is still running. Retry shortly. | | `internal_error` | `500` | `processing_error` | Something failed on our side. | | `temporarily_unavailable` | `503` | `service_unavailable` | A dependency is unavailable. Retry with backoff. | **Unexpected fields are rejected, not ignored.** Request bodies are validated strictly, so a body carrying a field the schema does not define returns `400 invalid_body` rather than silently dropping it. This is deliberate and exists for one reason above all: a body containing something like `card_number` must fail loudly rather than be quietly accepted into our logs and traces. See [Agentic Commerce](/commerce/introduction#payment-is-token-only). ## Blocking messages vs errors An error is a failed **request**. A blocked **session** is not an error — it returns `200` with `status: "not_ready_for_payment"` and one or more entries in `messages[]`: ```json theme={null} { "status": "not_ready_for_payment", "messages": [ { "type": "error", "code": "missing", "param": "buyer.name", "content_type": "plain", "content": "A buyer name is required to issue a ticket." } ] } ``` `messages[].code` is a **separate, smaller enum** from the error codes above — the ACP spec closes it to six values: | Code | Meaning | | ------------------ | -------------------------------------------- | | `missing` | A required value has not been supplied. | | `invalid` | A supplied value is not acceptable. | | `out_of_stock` | The requested line cannot be filled. | | `payment_declined` | The payment attempt failed. | | `requires_sign_in` | The buyer must authenticate. | | `requires_3ds` | The payment needs additional authentication. | A `type: "error"` message blocks payment; `type: "info"` is advisory. Read `status` for the decision and `messages` for the reason. ## Retrying `500 internal_error`, `503 temporarily_unavailable`, and `409 request_not_idempotent`. Use exponential backoff, and reuse the same `Idempotency-Key` so a retry cannot double-charge. Every `400`. The request is wrong and will fail identically until changed. `409 sold_out`, `409 not_on_sale`, `409 amount_mismatch`. The world changed underneath the session. `GET` it, show the buyer what is now true, and only then retry. `401 invalid_signature` and `402 payment_declined`. Retrying a declined token will not make it succeed; ask for another payment method. # Agentic Commerce Source: https://docs.gomry.com/commerce/introduction Let AI agents discover and sell Gomry events through the Agentic Commerce Protocol # Agentic Commerce Gomry implements the [Agentic Commerce Protocol](https://developers.openai.com/commerce) (ACP), the open standard maintained by OpenAI and Stripe for letting AI agents discover products and complete purchases on a merchant's own stack. There are two halves, and they are independent: A cross-organization, read-only feed of live public events. This is how an agent finds out an event exists. ACP checkout sessions. This is how an agent sells a ticket without sending the buyer to a browser. A correct checkout is invisible without a catalog, and a catalog with no checkout still drives buyers to your event page. Most integrations start with the catalog. **Can't hold a payment token?** Most agents can't — minting one needs a browser and a Stripe agent account. Every ticket type in the catalog carries a `checkout_url` that opens Gomry checkout with that tier preselected, so your agent can do the discovery and hand the buyer a link. No onboarding, no signing secret, works today. See [Buying without a payment token](/api-reference/catalog/get-catalog-event#buying-without-a-payment-token). ## What makes this surface different Every other resource in this API resolves your key to exactly one organization and reads inside it. The commerce surface does not: * **Catalog is cross-organization.** One key returns live public events from every organizer on Gomry. * **Checkout sells on another organizer's behalf** and takes payment against their Stripe account. Because of that, both are off by default on every key, and checkout carries additional gates described below. ## The purchase flow `GET /v1/catalog/events` to page the catalog, then `GET /v1/catalog/events/{event_id}` for the one you intend to sell. The detail endpoint is the only place `ticket_types` and `registration_questions` are populated. `POST /v1/checkout_sessions` with `items[].id` set to a **ticket type id** from `ticket_types[].id`. The session comes back with line items, totals, and a `status`. While `status` is `not_ready_for_payment`, read `messages[]`. Each `error` message says what is missing — a buyer name, a required registration answer, an out-of-stock line. Patch the session with `POST /v1/checkout_sessions/{id}` until `status` is `ready_for_payment`. `POST /v1/checkout_sessions/{id}/complete` with a delegated payment token. On success the session reports `completed` and carries an `order` with a `permalink_url` your buyer can open. ## Payment is token-only **No endpoint on this API accepts a card number, and none ever will.** Payment reaches Gomry only as a delegated token — a Stripe Shared Payment Token in `payment_data.token`. This is a hard architectural boundary, not a current limitation. Gomry has no raw-PAN code path; every paid flow is Stripe-hosted. Request bodies are validated strictly, so a body carrying an unexpected field such as `card_number` is **rejected with a `400`** rather than silently ignored. Practically, this means an agent platform must be able to mint a delegated payment token. An agent that holds only a raw card number cannot complete a Gomry checkout, and should send the buyer to the event `url` from the catalog instead. ## Ids: what to pass where The single most common integration error is passing an **event id** where a **ticket type id** belongs. | You have | Where it comes from | Where it goes | | ------------------ | --------------------------------------------- | ----------------------------------------------- | | Event id | `data[].id` on the catalog list | The path of `GET /v1/catalog/events/{event_id}` | | **Ticket type id** | `ticket_types[].id` on the catalog **detail** | **`items[].id` when creating a session** | | Question id | `registration_questions[].id` on the detail | `attendee_answers[].question_id` | Passing an event id as `items[].id` returns `item_not_found`. ## Versioning Send an `API-Version` header on every checkout request. This deployment implements: ``` API-Version: 2025-09-12 ``` A header naming a **different** version is refused with `unsupported_api_version` rather than served best-effort, so you never ship against fields we do not send. An **absent** header is accepted and treated as the current version. The ACP spec lists it as required and we recommend always sending it — but with exactly one version in service, refusing a partner mid-integration over a missing header helps nobody. Do not rely on that leniency: send the header, so the day a second version exists your client keeps getting the one it was built against. The catalog endpoints ignore this header entirely. ## Access The commerce surface is granted per partner. Contact [support@gomry.com](mailto:support@gomry.com) to be onboarded. **Catalog** needs `catalog: read` on your API key. **Checkout** needs three independent things, all of which must be true: 1. `checkout: write` on the key. 2. The key id added to Gomry's ACP partner allowlist — a deploy-time change, not a dashboard toggle. 3. A **signing secret** issued to you, used to sign every request with a body. The second and third exist because a scope can be granted by anyone with settings access, and a surface that sells arbitrary organizers' tickets needs a gate that a misconfigured dashboard cannot open. See [Scopes](/scopes). ## Request signing Every checkout request **with a body** (`create`, `update`, `complete`) must carry two headers: Base64 HMAC-SHA256 over `{timestamp}.{raw_body}`, using your signing secret. RFC 3339. Must be within **5 minutes** of Gomry's clock, or the request is refused. The timestamp and body are signed together so a captured signature cannot be replayed with a fresh timestamp. Sign the **raw body bytes you actually send**. `JSON.parse` followed by `JSON.stringify` does not round-trip — key order, whitespace and number formatting all change — so signing a re-serialized object produces a signature that fails on a request that is otherwise perfectly authentic. Build the body string once, sign that string, and send that string. `GET` and `cancel` carry no body and need no signature. They still require an allowlisted key, and should still send `API-Version`. ```javascript Signing a request theme={null} import crypto from "crypto"; const body = JSON.stringify({ items: [{ id: ticketTypeId, quantity: 2 }] }); const timestamp = new Date().toISOString(); const signature = crypto .createHmac("sha256", process.env.GOMRY_ACP_SIGNING_SECRET) .update(`${timestamp}.${body}`) .digest("base64"); await fetch("https://www.gomry.com/api/v1/checkout_sessions", { method: "POST", headers: { "X-API-KEY": process.env.GOMRY_API_KEY, "Content-Type": "application/json", "API-Version": "2025-09-12", Timestamp: timestamp, Signature: signature, "Idempotency-Key": crypto.randomUUID(), }, body, // the same string that was signed }); ``` Secrets are rotated additively: during a cutover both the old and new secret verify, and the old one is retired once you confirm. ## Headers | Header | Applies to | Notes | | ----------------- | --------------- | -------------------------------------------------------------------------------------------------- | | `X-API-KEY` | All | Your Gomry API key. | | `API-Version` | Checkout | `2025-09-12`. Always send it; a *different* value is refused, an absent one is treated as current. | | `Signature` | Checkout writes | Required. See above. | | `Timestamp` | Checkout writes | Required. RFC 3339, ±5 min. | | `Idempotency-Key` | Checkout writes | Recommended. Echoed back on the response. | | `Request-Id` | Checkout | Optional. Echoed back, including on errors, so you can correlate a failure in your own logs. | Checkout responses are always `Cache-Control: no-store` — they are per-buyer and must never be shared by a cache. ## Errors Checkout uses the ACP error envelope, which is **different from the rest of this API**: ```json theme={null} { "type": "invalid_request", "code": "item_not_found", "message": "No ticket type matches the requested id.", "param": "items[0].id" } ``` `type` is one of `invalid_request`, `processing_error`, or `service_unavailable`. See [Checkout Errors](/commerce/errors) for every code and its status. The catalog endpoints use the [standard Gomry error format](/errors). # Errors Source: https://docs.gomry.com/errors Understand API error responses and status codes # Errors The Gomry API uses standard HTTP status codes to indicate the outcome of a request. Errors return a JSON body with an `error` field describing the issue. ## Error Response Format ```json theme={null} { "error": "Human-readable error message" } ``` For validation errors, additional detail may be included: ```json theme={null} { "error": "Invalid pagination parameters", "details": { "fieldErrors": { "page_size": ["Number must be less than or equal to 200"] } } } ``` ## Status Codes | Code | Meaning | When It Happens | | ----- | ------------------------- | ------------------------------------------------------------- | | `200` | **OK** | Request succeeded | | `400` | **Bad Request** | Invalid query parameters or request body | | `401` | **Unauthorized** | Missing or invalid API key | | `404` | **Not Found** | Resource doesn't exist or doesn't belong to your organization | | `429` | **Too Many Requests** | Rate limit exceeded — slow down and retry | | `500` | **Internal Server Error** | Something went wrong on our end | ## Handling Errors ```javascript JavaScript theme={null} const response = await fetch(url, { headers: { "X-API-KEY": apiKey } }); if (!response.ok) { const { error } = await response.json(); switch (response.status) { case 401: console.error("Authentication failed:", error); break; case 404: console.error("Resource not found:", error); break; case 429: // Retry after a short delay await new Promise((r) => setTimeout(r, 2000)); break; default: console.error(`API error (${response.status}):`, error); } } ``` ```python Python theme={null} import requests response = requests.get(url, headers={"X-API-KEY": api_key}) if response.status_code == 429: import time time.sleep(2) # retry... elif not response.ok: error = response.json().get("error", "Unknown error") raise Exception(f"API error ({response.status_code}): {error}") ``` A `404` response does **not** confirm the resource doesn't exist — it may belong to a different organization. The API intentionally returns `404` instead of `403` to avoid leaking information about other organizations' data. # Introduction Source: https://docs.gomry.com/introduction Build integrations with the Gomry Public API # Gomry Public API The Gomry API lets you programmatically access your event data — attendees, ticket classes, and more — to build integrations, sync with third-party tools, or power your own dashboards. ## Base URL All API requests use the following base URL: ``` https://www.gomry.com/api/v1 ``` ## Key Concepts Spaces are sub-divisions of your organization (cities, brands, teams, or marketplace sellers). Events, experiences, and contacts belong to a space, and each space can carry its own tax / legal-entity details. Events are the core resource. Every other resource (attendees, ticket classes) is scoped to an event. Attendees represent registrations. Each attendee holds a ticket for an event. Ticket classes define the types of tickets available for an event (e.g., General Admission, VIP). API keys authenticate requests and are scoped to your organization. Generate them in your dashboard. ## Quick Start Go to **Organization Settings → API Keys** in your Gomry dashboard and create a new key. Copy it immediately — it's only shown once. ```bash theme={null} curl -H "X-API-KEY: your_api_key_here" \ https://www.gomry.com/api/v1/events/YOUR_EVENT_ID ``` ```json theme={null} { "data": { "id": "AbCdEfGhIjKlMnOpQrSt", "name": "My Event", "status": "active", "start": { "utc": "2025-06-15T18:00:00.000Z", "timezone": "America/New_York" }, ... } } ``` ## Using Claude or another AI assistant? Skip the API entirely. The [Gomry MCP server](/mcp/introduction) lets Claude, Cursor, and Claude Desktop read your events, attendees, forms, applications, payments, and contacts directly — no code, no API key. Set it up once and ask questions like *"summarize last week's form submissions"* in plain language. # MCP Server Source: https://docs.gomry.com/mcp/introduction Let Claude and other AI assistants work with your Gomry data directly via the Model Context Protocol # Gomry MCP Server The Gomry MCP server lets AI assistants like **Claude**, **ChatGPT**, **Cursor**, and **Codex** read and manage your organization's data directly — events, attendees, forms, applications, payments, contacts, experiences, and more — without you copying anything by hand. It's the same data the [Public API](/introduction) exposes, but instead of writing HTTP calls, you give your AI client a single URL and it discovers the available tools on its own. **Best for:** analyzing form submissions, summarizing attendee lists, querying payments, exploring contacts, updating event data, and building one-off workflows where writing code would be overkill. **Not for:** programmatic integrations — use the [Public API](/introduction) for those. ## What you can ask Once connected, your AI assistant can answer questions like: "What did people answer on form X? Group the responses by question." "Who's checked in vs. still pending for next week's event?" "Show me last month's successful payments by event." "Which contacts have submitted applications but never paid?" ## How it differs from the API key | | Public API (`X-API-KEY`) | MCP Server (OAuth) | | --------------- | ---------------------------------------- | ---------------------------------------------------------------------------- | | **Who uses it** | Your code | An AI assistant | | **Auth** | `X-API-KEY` header | OAuth 2.1 — you sign in and click **Authorize** once | | **Setup** | Per-integration, in code | One URL, pasted into Claude / ChatGPT / Cursor once | | **Scope** | One organization, per-resource scope map | Every organization you belong to, limited by your dashboard RBAC privileges | | **Surface** | All public REST endpoints (read + write) | 89 organizer-visible tools (read + write, including reporting and analytics) | The two systems are independent — your API keys won't authenticate MCP calls and an MCP connection won't authenticate API calls. ## Security model * Every MCP connection is **tied to one user**. Calls are scoped server-side to the organizations that user belongs to; an MCP client can never read or modify any other organization's data. * Tool visibility mirrors your **dashboard privileges**. If you can't see attendees in the dashboard, the `list_attendees` tool isn't even advertised to your client. * Connections are **revocable from the client**: disconnecting the Gomry connector revokes its refresh token, and the current access token expires within the hour. * The endpoint is **rate-limited** at 120 requests per minute per user. ## Next steps Connect Claude, ChatGPT, or Cursor with OAuth in under two minutes. The full list of MCP tools and what they return. # Setup Source: https://docs.gomry.com/mcp/setup Connect Claude, ChatGPT, Cursor, Codex, or any other MCP client to Gomry with OAuth # Setup The Gomry MCP server is a hosted **remote MCP** endpoint. There is nothing to install and no token to copy: you add one URL to your AI client, sign in to Gomry when it asks, and you're done. ``` https://www.gomry.com/api/mcp ``` ## 1. Add the connector In your AI client, add a new custom connector / MCP server and paste the URL above. Leave any bearer-token or custom-header fields empty. Client-specific steps are in the tabs below. The client opens a Gomry page in your browser. Sign in with your Gomry account if you aren't already, then click **Authorize**. Any signed-in Gomry user can connect. The assistant gets access to every organization you're a member of, limited to what your role in each one allows. You're redirected back to the client, which loads the tool list and shows Gomry in its tool picker. ## 2. Client-specific instructions 1. Open **Settings → Connectors**. 2. Click **Add custom connector**. 3. Name it `Gomry` and paste the server URL: ``` https://www.gomry.com/api/mcp ``` 4. Click **Add**, then **Connect**. Authorize in the Gomry page that opens. 1. Open **Settings → Connectors** (or **Apps**, depending on the version). 2. Click **Create** / **Add custom connector**. 3. Name it `Gomry` and paste the MCP server URL: ``` https://www.gomry.com/api/mcp ``` 4. Leave authentication on **OAuth** and save. Authorize in the Gomry page that opens. Add this entry to your `~/.cursor/mcp.json` (create the file if it doesn't exist): ```json theme={null} { "mcpServers": { "gomry": { "url": "https://www.gomry.com/api/mcp" } } } ``` Open Cursor's **MCP** settings and click the login / authenticate action next to `gomry` to run the OAuth flow. Add `https://www.gomry.com/api/mcp` as a **streamable HTTP** MCP server, leave the bearer-token and custom-header options empty, then run the client's login / authenticate command for that server. It opens the Gomry authorization page in your browser and calls back to a local port on your machine. Any client that supports **remote MCP over streamable HTTP with OAuth** works. Configure it with: | Setting | Value | | -------------- | ---------------------------------- | | Server URL | `https://www.gomry.com/api/mcp` | | Transport | Streamable HTTP | | Authentication | OAuth (leave tokens/headers empty) | The server publishes standard discovery documents, so clients find the authorization server on their own: * `https://www.gomry.com/.well-known/oauth-protected-resource` * `https://www.gomry.com/.well-known/oauth-authorization-server` ## 3. Verify it works In your client, ask: > "List my events." The assistant should invoke the `list_events` tool and return your organization's events. If you belong to more than one organization, it may ask which one you mean. ## How authentication works The server is an OAuth 2.1 authorization server (PKCE, dynamic client registration) in front of the MCP endpoint: 1. The client discovers the authorization server via `/.well-known/oauth-protected-resource`. 2. The client registers itself and sends you to the Gomry authorization page. 3. You sign in (Firebase auth, same as the dashboard) and click **Authorize**. 4. The client receives a short-lived access token (1 hour) and a refresh token (30 days, rotated on every use), and sends the access token as `Authorization: Bearer …` on each MCP call. Nothing is shared with the client beyond access to the tools your privileges allow. If a client explicitly requests the `openid` / `email` scopes (some workspaces require it), Gomry also returns your account email and whether it's verified — nothing else. ## Disconnecting Remove or disconnect the Gomry connector in your AI client. This revokes the refresh token on Gomry's side, so the client can't renew access; the current access token expires within the hour. You can reconnect at any time by running the authorization flow again. ## Alternative: personal token URL If your client can't run OAuth, you can use a personal token instead: 1. In the Gomry dashboard, open **Personal Settings** (your account settings, not organization settings) → **Connect to Claude**. 2. Click **Generate Server URL** and copy the URL. It looks like `https://www.gomry.com/api/mcp?token=…` and is shown **once**. 3. Paste it as the server URL in your client. The token is accepted as `?token=` or `Authorization: Bearer …`. Treat the URL like a password: anyone holding it can act as you across every organization you belong to. You have at most one active token; **Regenerate** replaces it and **Revoke** (same section) disables it within five minutes. ## Errors you might hit | Status | Body | Meaning | | ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `401` | `Missing authentication.` | The client isn't sending credentials. Re-run the connector's login / authorize step. | | `401` | `Invalid or expired access token` | The OAuth session expired and couldn't be refreshed. Disconnect and reconnect the connector. | | `401` | `Invalid or revoked MCP token` | Personal-token path: the token was revoked or regenerated. Generate a new URL. | | `429` | `Rate limit exceeded.` | More than 120 requests/minute for this user. Slow down. | | `-32001` (JSON-RPC) | `Tool not found: …` | The tool exists but your privileges don't include it, OR the tool name is wrong. See [Available tools](/mcp/tools). | | OAuth | `redirect_uri does not match a registered redirect URI` | Seen with older CLI clients that pick a random local port. Remove the server from the client and add it again. | # Available Tools Source: https://docs.gomry.com/mcp/tools Every MCP tool the Gomry server exposes — what it returns, what privilege it requires # Available Tools The MCP server exposes **92 organizer-visible tools** across read, write, reporting, contact, and meta surfaces. Internal-only tools are not advertised here. Each entry below comes from the server's live tool catalog. Tools are advertised to your client **only if your dashboard privileges include the required RBAC permission**. Meta tools and the feature-request tool do not require a privilege. ## Organizations and meta List the organizations the authenticated user belongs to. Use an organization ID from the response to scope subsequent calls. **Read**
**Required privilege:** None
**Arguments:** None
Create a new organization and make the authenticated user its administrator. The new organization includes the standard starter setup. **Write**
**Required privilege:** None
**Arguments:** **`name`**, `contact_email`, `country`
## Events List the organization’s events. Active events are returned by default; use `status` to include inactive events and pagination to page through results. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, `page`, `page_size`, `status` Equivalent REST: [`GET /events`](/api-reference/events/list-events)
Get one event by ID. Events outside the organizations you belong to are returned as not found. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`** Equivalent REST: [`GET /events/:id`](/api-reference/events/get-event)
Create an event. If `end` is omitted, the event ends two hours after `start`. **Write**
**Required privilege:** `EVENTS_CREATE_EVENTS`
**Arguments:** `organization_id`, **`name`**, **`location`**, `description`, `capacity`, `require_approval`, `is_private`, `currency`, **`start`**, `end`, `ticket_price`, `space_identifier`
Update selected fields on an event. This is a partial update; `venue`, when supplied, replaces the existing venue rather than merging with it. **Destructive write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, `name`, `description`, `capacity`, `location`, `start`, `end`, `venue`, `currency`, `is_private`, `tax_inclusive`, `rsvp_enabled`, `cover_img`, `registration_status`, `required_contact_method`, `remove_service_fees`, `submission_restricted`
Delete an event (soft delete: the event is hidden from the dashboard and its public page). Only the primary host organization can delete — an event co-hosted by this organization is refused. Tickets are NOT refunded and attendees are NOT notified. This is destructive and cannot be undone through the API, so confirm with the user before calling. Deleting an already-deleted event succeeds with `already_deleted: true`. Shares one declaration with the in-app assistant's `deleteEvent`, which asks the organizer to confirm before running. **Destructive write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**
List the organization’s spaces — the sub-divisions an event belongs to, each with its own calendar, team, and permissions. Includes the General space every organization has, whose `id` is `null`. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, `search`
Move an event to another space, together with its ticket classes and promo codes. Accepts a space identifier, id, or unambiguous name (`GENERAL` for the General space). An event that has already sold tickets cannot be moved. `event_id` is advertised as optional because the schema is shared with the in-app assistant, which defaults it to the event in view — over MCP it is required, and a call without it is refused. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`space`**
## Attendees and tickets List ticket attendees for an event, including basic attendee and ticket details. Use the applications tools for form answers. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, `page`, `page_size`, `status` Equivalent REST: [`GET /events/:id/attendees`](/api-reference/attendees/list-attendees)
Get one ticket attendee by ID for an event. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, **`attendee_id`** Equivalent REST: [`GET /events/:id/attendees/:attendeeId`](/api-reference/attendees/get-attendee)
Create a confirmed ticket for a contact. With `send_email: true`, the tool sends a ticket confirmation email. **Write**
**Required privilege:** `TICKETS_CREATE_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, **`ticket_class_id`**, **`contact`**, `quantity`, `status`, `payment_method`, `send_email`
Invite a contact to an event. The invitation creates an invited ticket and queues the invitation email; contacts who already have a ticket may be skipped. **Write**
**Required privilege:** `EMAILS_CREATE_EMAILS`
**Arguments:** `organization_id`, **`event_id`**, **`contact`**, `custom_message`
Update an attendee’s ticket status. The operation can affect capacity and, for deleted tickets, refund handling. **Destructive write**
**Required privilege:** `TICKETS_UPDATE_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, **`attendee_id`**, **`status`**, `notify_guest`, `custom_message`
## Ticket classes and promo codes List ticket classes for an event. It can also return experience pricing tiers; edit those tiers with the experience ticket-class tools. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`** Equivalent REST: [`GET /events/:id/ticket-classes`](/api-reference/ticket-classes/list-ticket-classes)
Get one ticket class by ID for an event. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`ticket_class_id`** Equivalent REST: [`GET /events/:id/ticket-classes/:tcId`](/api-reference/ticket-classes/get-ticket-class)
Create a ticket class for an event. Set `cost` to zero for a free ticket. `maximum_per_order` caps how many of this class fit in a single order. `0` or omitted means unlimited. It is not a per-person limit: the same buyer can place another order. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`name`**, `description`, **`cost`**, `currency`, `free`, `quantity_total`, `hide_on_event_page`, `minimum_per_order`, `maximum_per_order`, `require_approval`, `sales_start`, `sales_end`
Update selected fields on a ticket class. Sold quantity is calculated by the server and cannot be set directly. **Destructive write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`ticket_class_id`**, `name`, `description`, `cost`, `currency`, `free`, `quantity_total`, `hide_on_event_page`, `minimum_per_order`, `maximum_per_order`, `require_approval`, `sales_start`, `sales_end`
Create a discount code for an event. Provide either a fixed `discount_amount` or a `percent_off` value, and optionally limit the code to specific ticket classes. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`code`**, `discount_amount`, `percent_off`, `max_quantity`, `valid_from`, `valid_until`, `applied_to_tickets`
Update a discount code’s settings. The code itself cannot be renamed, and fixed and percentage discounts remain mutually exclusive. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`promo_code_id`**, `discount_amount`, `percent_off`, `max_quantity`, `valid_from`, `valid_until`, `applied_to_tickets`
Deactivate a discount code so it can no longer be redeemed. Existing orders that used it remain unchanged. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, **`promo_code_id`**
## Experiences, recurrences, and bookings List the organization’s bookable experiences. Active experiences are returned by default; use `status` to include inactive ones. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, `page`, `page_size`, `status`
Get one experience, including its pricing, location, scheduling rules, and capacity. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**
List the schedules attached to an experience. Each schedule describes a cadence, end condition, and session templates. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**
Get one schedule attached to an experience. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`recurrence_id`**
Create a bookable experience with its default pricing, location, and capacity settings. **Write**
**Required privilege:** `EVENTS_CREATE_EVENTS`
**Arguments:** `organization_id`, **`name`**, `description`, **`duration_minutes`**, **`default_capacity`**, **`location`**, **`price`**, **`currency`**, `space_id`
Create a copy of an existing experience. **Write**
**Required privilege:** `EVENTS_CREATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, `new_name`, `description`
Update selected fields on an experience. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, `name`, `description`, `cover_image`, `photos`, `location`, `registration_status`, `required_contact_method`, `max_booking_quantity`, `remove_service_fees`, `duration_minutes`, `default_capacity`, `price`, `currency`, `status`, `use_ticket_classes`
Delete an experience (soft delete: the experience is hidden from the dashboard and its public page). Only experiences owned by this organization can be deleted. Sessions and bookings are NOT cancelled or refunded and attendees are NOT notified. This is destructive and cannot be undone through the API, so confirm with the user before calling. An unknown or already-deleted experience is reported as not found. Shares one declaration with the in-app assistant's `deleteExperience`, which asks the organizer to confirm before running. **Destructive write**
**Required privilege:** `EVENTS_DELETE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**
List the pricing tiers for an experience. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**
Create a pricing tier for an experience. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`name`**, `description`, **`price`**, `currency`, **`capacity`**, `color`, `require_approval`, `hide_on_public_page`
Update an experience pricing tier. The `version` argument identifies the version being edited. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`ticket_class_id`**, **`version`**, `name`, `description`, `price`, `capacity`, `color`, `require_approval`, `hide_on_public_page`
Archive an experience pricing tier so it is no longer offered. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`ticket_class_id`**, **`version`**
Set the display order of an experience’s pricing tiers. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`order`**
List bookings for an experience, including the sessions and booking status. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, `status`, `session_id`, `limit`
Get one booking for an experience session. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`session_id`**, **`booking_id`**
Create a recurring schedule for an experience. Weekly schedules require `days_of_week` in the cadence. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`cadence`**, **`start_date`**, **`start_time_of_day`**, `timezone`, `capacity`, `end_condition`, `status`
Update an experience schedule, including its cadence, dates, end condition, or status. A new cadence replaces the previous cadence. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`recurrence_id`**, `cadence`, `start_date`, `start_time_of_day`, `session_template_index`, `timezone`, `end_condition`, `status`
Create one additional session outside an experience’s recurring schedule. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`start_local`**, `timezone`, `provider_id`, `duration_minutes`, `capacity`, `notes`, `location`
Update one experience session’s capacity, duration, notes, or location. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`session_id`**, `capacity`, `notes`, `duration_minutes`, `location`
## Forms and applications List the organization’s forms. Forms include the question definitions used to interpret application answers. **Read**
**Required privilege:** `FORMS_READ_FORMS`
**Arguments:** `organization_id`, `page`, `page_size`, `status` Equivalent REST: [`GET /forms`](/api-reference/forms/list-forms)
Get one form, including its question definitions. **Read**
**Required privilege:** `FORMS_READ_FORMS`
**Arguments:** `organization_id`, **`form_id`** Equivalent REST: [`GET /forms/:id`](/api-reference/forms/get-form)
List form applications and their answers. Filter by form, contact, status, dates, or sort order to analyze submissions. **Read**
**Required privilege:** `FORMS_READ_FORMS`
**Arguments:** `organization_id`, `page`, `page_size`, `form_id`, `contact_id`, `status`, `created_after`, `created_before`, `submitted_after`, `submitted_before`, `sort` Equivalent REST: [`GET /applications`](/api-reference/applications/list-applications)
Get one form application, including all submitted answers. **Read**
**Required privilege:** `FORMS_READ_FORMS`
**Arguments:** `organization_id`, **`application_id`** Equivalent REST: [`GET /applications/:id`](/api-reference/applications/get-application)
Update an application’s metadata. Only the fields supplied are changed. **Destructive write**
**Required privilege:** `FORMS_UPDATE_FORMS`
**Arguments:** `organization_id`, **`application_id`**, **`metadata`**
Set the status of one or more applications. **Destructive write**
**Required privilege:** `SUBMISSIONS_UPDATE_SUBMISSIONS`
**Arguments:** `organization_id`, **`application_ids`**, **`status`**, `notify_applicant`
## Custom fields and payments List custom-field definitions, optionally filtered by record type. **Read**
**Required privilege:** `FIELDS_READ_FIELDS`
**Arguments:** `organization_id`, `page`, `page_size`, `record_type` Equivalent REST: [`GET /custom-fields`](/api-reference/custom-fields/list-custom-fields)
Get one custom-field definition by ID. **Read**
**Required privilege:** `FIELDS_READ_FIELDS`
**Arguments:** `organization_id`, **`field_id`** Equivalent REST: [`GET /custom-fields/:id`](/api-reference/custom-fields/get-custom-field)
List payments, newest first. Filter by event, product, form, date range, or status; the default status is `succeeded`. **Read**
**Required privilege:** `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, `page`, `page_size`, `event_id`, `product_id`, `form_id`, `from`, `to`, `status` Equivalent REST: [`GET /payments`](/api-reference/payments/list-payments)
Get one payment by ID, including its receipt URL when available. **Read**
**Required privilege:** `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, **`payment_id`** Equivalent REST: [`GET /payments/:id`](/api-reference/payments/get-payment)
## Contacts and lists List contacts with pagination, sorting, and optional space or list filters. **Read**
**Required privilege:** `CONTACTS_READ_CONTACTS`
**Arguments:** `organization_id`, `spaceId`, `page`, `pageSize`, `sortBy`, `sortOrder`, `listIds`
Search contacts by name, email, and other text fields. Results are ranked by relevance. **Read**
**Required privilege:** `CONTACTS_READ_CONTACTS`
**Arguments:** `organization_id`, **`query`**, `page`, `pageSize`
List the organization’s contact lists, optionally filtered by name. **Read**
**Required privilege:** `LISTS_READ_LISTS`
**Arguments:** `organization_id`, `search`
Add one or more contacts to a contact list. **Write**
**Required privilege:** `CONTACTS_UPDATE_CONTACTS`
**Arguments:** `organization_id`, **`listId`**, **`contactIds`**
Remove one or more contacts from a contact list. Contacts themselves are not deleted. **Destructive write**
**Required privilege:** `CONTACTS_UPDATE_CONTACTS`
**Arguments:** `organization_id`, **`listId`**, **`contactIds`**
Create or add a contact to the organization. **Write**
**Required privilege:** `CONTACTS_CREATE_CONTACTS`
**Arguments:** `organization_id`, `email`, `phone_number`, `first_name`, `last_name`, `job_title`, `company`, `university`, `gender`, `birthday`, `location`, `linkedin`, `instagram`, `twitter`, `github`, `website`, `tags`
Update selected fields on a contact. **Destructive write**
**Required privilege:** `CONTACTS_UPDATE_CONTACTS`
**Arguments:** `organization_id`, **`contact_id`**, `email`, `phone_number`, `first_name`, `last_name`, `job_title`, `company`, `university`, `gender`, `birthday`, `location`, `linkedin`, `instagram`, `twitter`, `github`, `website`, `tags`, `status`
Create a contact list. List names must be unique within the organization. **Write**
**Required privilege:** `LISTS_CREATE_LISTS`
**Arguments:** `organization_id`, **`name`**, `description`, `color`
Archive a contact list and remove its memberships. The contacts remain in the organization. **Write**
**Required privilege:** `LISTS_UPDATE_LISTS`
**Arguments:** `organization_id`, **`listId`**
## Organizations, members, and roles Invite a user to join the organization with a specified role. **Write**
**Required privilege:** `PERMISSIONS_CREATE_PERMISSIONS`
**Arguments:** `organization_id`, **`email`**, **`role`**, `space_id`
Change an organization member’s role. **Destructive write**
**Required privilege:** `PERMISSIONS_UPDATE_PERMISSIONS`
**Arguments:** `organization_id`, **`user_id`**, **`role`**, `space_id`
Remove a member from the organization. **Destructive write**
**Required privilege:** `PERMISSIONS_UPDATE_PERMISSIONS`
**Arguments:** `organization_id`, **`user_id`**, `space_id`
Update organization settings. Only the fields supplied are changed. **Destructive write**
**Required privilege:** `ORGANIZATION_SETTINGS_UPDATE_ORGANIZATION_SETTINGS`
**Arguments:** `organization_id`, `name`, `description`, `email`, `website`, `logo`, `logo_base64`, `content_type`, `instagram`, `linkedin`, `facebook`, `twitter`, `youtube`, `github`, `crunchbase`
Create an organization role with a name and privilege set. **Write**
**Required privilege:** `ROLES_CREATE_ROLES`
**Arguments:** `organization_id`, **`name`**, **`privileges`**, `description`
Update an existing organization role. **Destructive write**
**Required privilege:** `ROLES_UPDATE_ROLES`
**Arguments:** `organization_id`, **`role_id`**, `name`, `privileges`, `status`
## Bundles, media, and calendars List bundles available in the organization. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, `space_id`, `status`
Get one bundle by ID. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`bundle_id`**
Create a bundle that combines experiences, sessions, and a price. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`name`**, `description`, **`experience_ids`**, **`session_count`**, **`price`**, **`currency`**, `validity`, `ticket_tiers`, `space_id`
Update selected fields on a bundle. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`bundle_id`**, `name`, `description`, `experience_ids`, `session_count`, `price`, `currency`, `validity`, `ticket_tiers`, `status`, `hide_on_public_page`, `max_quantity`
Delete a bundle using a soft delete. Existing bookings and purchases are not refunded or removed. **Write**
**Required privilege:** `EVENTS_UPDATE_EVENTS`
**Arguments:** `organization_id`, **`bundle_id`**
Upload an image and return its hosted URL. The image is not attached to an event or experience automatically. **Write**
**Required privilege:** `EVENTS_CREATE_EVENTS`
**Arguments:** `organization_id`, **`image_base64`**, **`content_type`**
List the organization’s calendars, including their names, slugs, and entry counts. **Read**
**Required privilege:** `BOARDS_READ_BOARDS`
**Arguments:** `organization_id`, `search`
Update a calendar’s name, branding, location, links, or automatic event settings. `links` replaces the existing links object. **Write**
**Required privilege:** `BOARDS_UPDATE_BOARDS`
**Arguments:** `organization_id`, **`calendar`**, `name`, `description`, `slug`, `cover_image`, `logo`, `tint_color`, `font_family`, `background_pattern_id`, `location`, `links`, `automatically_add_events`, `automatically_remove_past_events`
Add an event, experience, or external event to a calendar. Use `type` to identify the kind of entry. **Write**
**Required privilege:** `BOARDS_UPDATE_BOARDS`
**Arguments:** `organization_id`, **`calendar`**, **`id`**, **`type`**, `is_featured`
Remove an event, experience, or external event from a calendar without changing the underlying item. **Write**
**Required privilege:** `BOARDS_UPDATE_BOARDS`
**Arguments:** `organization_id`, **`calendar`**, **`id`**, **`type`**
## Reporting and analytics Summarize unique attendees across the organization’s events for a date range, including event counts, ticket counts, and spend by currency. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, `from`, `to`, `status`
Return sales and attendance trends across events, grouped by day, week, or month. Revenue remains separated by currency. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, `from`, `to`, `interval`
Aggregate ticket or payment data by a selected dimension such as event, date, ticket class, or status. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, `source`, **`group_by`**, `metric`, `from`, `to`
Compare two event-performance periods, including revenue, refunds, tickets, events, and attendee counts. The baseline defaults to the same-length period one year earlier. **Read**
**Required privilege:** `TICKETS_READ_TICKETS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, `from`, `to`, `years_back`, `baseline_from`, `baseline_to`
Return page-view totals or a time series for one event, with an option to count unique visitors. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, `interval`, `group_by`, `unique_visitors`
Break down one event’s page visitors by country, city, region, or continent. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, `group_by`, `interval`
Break down one event’s page views by device, browser, or operating system. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, `group_by`, `interval`
Break down one event’s page traffic by referring domain, URL, or UTM source, medium, or campaign. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`event_id`**, `group_by`, `interval`
Return daily registrations for one event over a recent number of days. Days without registrations are omitted. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `TICKETS_READ_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, `days`
Return page views, visitors, registrations, and conversion percentages for one event. If page-view data is unavailable, the response identifies that limitation separately from registration totals. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `TICKETS_READ_TICKETS`
**Arguments:** `organization_id`, **`event_id`**, `interval`
Return conversion metrics for up to 10 events in one request. Events outside the organization are reported as rejected while valid IDs continue processing. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `TICKETS_READ_TICKETS`
**Arguments:** `organization_id`, **`event_ids`**, `interval`
Return an event’s sponsorship revenue, impressions, cashback earnings, and placement performance. Unavailable sections may be returned as null. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, **`event_id`**
Return a daily sponsorship-revenue time series and period totals for one event. The requested range is capped at 365 days. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, **`event_id`**, `days`
List sponsors that have served ads on an event, with revenue, impressions, clicks, and click-through performance. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, **`event_id`**
Read an event’s sponsorship and cashback settings, including enabled ad placements. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`, `PAYMENTS_READ_PAYMENTS`
**Arguments:** `organization_id`, **`event_id`**
Generate HTML for embedding an event or event calendar on an external website. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, `event_id`, **`mode`**, `button_text`, `iframe_width`, `iframe_height`, `bg_color`
Generate HTML for embedding an experience on an external website. **Read**
**Required privilege:** `EVENTS_READ_EVENTS`
**Arguments:** `organization_id`, **`experience_id`**, **`mode`**, `button_text`, `iframe_width`, `iframe_height`, `bg_color`
## Feature requests Record a feature request with a summary, category, user intent, and supporting quote. Clients call this when an organizer asks for a capability no tool covers. **Write**
**Required privilege:** None
**Arguments:** **`featureSummary`**, **`category`**, **`userIntent`**, **`userQuote`**
## Common patterns ### "Analyze submissions to form X" 1. `get_form` with `form_id` → returns the question schema (each question's `id`, `title`, `type`). 2. `list_applications` with `form_id` → returns the matching submissions, each carrying `answers` keyed by `question_id`. 3. Correlate `answers[].question_id` with `questions[].id` to render grouped output. ### "Find paying attendees of event Y" 1. `list_attendees` with `event_id` → list of tickets. 2. `list_payments` with `event_id` and `status: succeeded` → list of payments. 3. Join the results on `customer_email` or `contact_user_id`. ### "Group last month's submissions by status" 1. `list_applications` with `status: Pending,Accepted,Rejected` (or omit for all). 2. Bucket the results client-side. Status is on every application. ## Limits * **Rate limit:** 120 requests / 60 seconds per user (sliding window). Burstable up to 120 in a single window. * **Page size:** max `200` for organizer read tools, max `50` for contacts tools. * **Access tokens:** OAuth access tokens live for 1 hour and are refreshed automatically by the client; disconnecting the connector stops refreshes. Personal tokens (legacy) are cached server-side for 5 minutes — revocation propagates within that window. * **User context cache:** RBAC privileges are cached for 5 minutes — privilege changes take effect within that window. # Pagination Source: https://docs.gomry.com/pagination Navigate through large result sets # Pagination List endpoints that may return large result sets support page-based pagination. ## Query Parameters | Parameter | Type | Default | Range | Description | | ----------- | ------- | ------- | ----- | ------------------------- | | `page` | integer | `1` | 1+ | The page number to return | | `page_size` | integer | `50` | 1–200 | Number of items per page | ## Response Format Paginated endpoints wrap results in a standard envelope: ```json theme={null} { "data": [ ... ], "pagination": { "total": 142, "page": 1, "page_size": 50, "total_pages": 3 } } ``` | Field | Description | | ------------- | ---------------------------------------- | | `total` | Total number of items matching the query | | `page` | Current page number | | `page_size` | Items per page | | `total_pages` | Total number of pages | ## Example: Iterating Through All Pages ```javascript theme={null} async function fetchAllAttendees(eventId, apiKey) { const attendees = []; let page = 1; let totalPages = 1; while (page <= totalPages) { const response = await fetch( `https://www.gomry.com/api/v1/events/${eventId}/attendees?page=${page}&page_size=200`, { headers: { "X-API-KEY": apiKey } } ); const result = await response.json(); attendees.push(...result.data); totalPages = result.pagination.total_pages; page++; } return attendees; } ``` Not all list endpoints are paginated. Endpoints that always return a bounded number of items (like ticket classes for an event) return a flat `{ "data": [...] }` response without pagination metadata. # Rate Limits Source: https://docs.gomry.com/rate-limits Understand API rate limiting and best practices # Rate Limits The Gomry API enforces rate limits to ensure fair usage and service stability. ## Current Limits | Scope | Limit | | -------------- | ------------------------------ | | Per IP address | **60 requests per 10 seconds** | When you exceed the rate limit, the API returns a `429 Too Many Requests` response: ```json theme={null} { "error": "Rate limit exceeded. Try again shortly." } ``` ## Best Practices Use list endpoints with pagination instead of fetching resources one by one. Cache API responses on your end to reduce the number of requests, especially for data that doesn't change frequently (events, ticket classes). When you receive a `429`, wait before retrying. Double the wait time on each subsequent retry. If syncing large datasets, spread requests evenly rather than bursting all at once. ## Retry Strategy Example ```javascript theme={null} async function fetchWithRetry(url, headers, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, { headers }); if (response.status !== 429) return response; const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s await new Promise((r) => setTimeout(r, delay)); } throw new Error("Rate limit exceeded after retries"); } ``` # Scopes Source: https://docs.gomry.com/scopes Per-resource read/write permissions on API keys # Scopes Each Gomry API key carries a **scope map** that controls which resources it can access and at what level. Scopes are configured per-resource and per-action, letting you create keys with exactly the access an integration needs. ## Available Actions | Action | Description | | ------- | ---------------------------------------------------------------------- | | `none` | No access. Requests to this resource return `403 insufficient_scope`. | | `read` | Read-only access (`GET` requests). | | `write` | Full access (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`). Implies `read`. | ## Available Resources `contacts`, `events`, `experiences`, `attendees`, `ticket_classes`, `applications`, and `spaces` support `write`. `checkout` is the one resource that supports `write` but not `read` — see [Commerce scopes](#commerce-scopes). Every other resource is read-only, and selecting `read` is sufficient to use each endpoint listed. | Resource | Endpoints | Supported actions | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `spaces` | `GET/POST /v1/spaces`, `GET/PATCH /v1/spaces/:identifier`, `PUT /v1/spaces/:identifier/tax-details` | `none`, `read`, `write` | | `events` | `GET/POST /v1/events`, `GET/PATCH/DELETE /v1/events/:id` | `none`, `read`, `write` | | `experiences` | `GET/POST /v1/experiences`, `GET/PATCH/DELETE /v1/experiences/:id`, `GET/POST /v1/experiences/:id/recurrences`, `GET/PATCH/DELETE /v1/experiences/:id/recurrences/:id` | `none`, `read`, `write` | | `attendees` | `GET/POST /v1/events/:id/attendees`, `GET/PATCH/DELETE /v1/events/:id/attendees/:id` | `none`, `read`, `write` | | `ticket_classes` | `GET/POST /v1/events/:id/ticket-classes`, `GET/PATCH/DELETE /v1/events/:id/ticket-classes/:id` | `none`, `read`, `write` | | `contacts` | `GET/POST /v1/contacts`, `GET/PATCH/DELETE /v1/contacts/:id` | `none`, `read`, `write` | | `applications` | `GET /v1/applications`, `GET/PATCH /v1/applications/:id`, `GET /v1/contacts/:id/applications` | `none`, `read`, `write` | | `forms` | `GET /v1/forms`, `GET /v1/forms/:id` | `none`, `read` | | `lists` | `GET /v1/lists`, `GET /v1/lists/:id`, `GET /v1/lists/:id/contacts` | `none`, `read` | | `custom_fields` | `GET /v1/custom-fields`, `GET /v1/custom-fields/:id` | `none`, `read` | | `payments` | `GET /v1/payments`, `GET /v1/payments/:id` | `none`, `read` | | `catalog` | `GET /v1/catalog/events`, `GET /v1/catalog/events/:id`, `GET /v1/catalog/feed` | `none`, `read` | | `checkout` | `POST /v1/checkout_sessions`, `GET/POST /v1/checkout_sessions/:id`, `POST /v1/checkout_sessions/:id/complete`, `POST /v1/checkout_sessions/:id/cancel` | `none`, `write` | ## Commerce scopes `catalog` and `checkout` power the [Agentic Commerce](/commerce/introduction) surface and behave differently from every other scope on this page. Both are **off by default on every key**, including keys created before scopes existed — the legacy full-access rule does not extend to them, and they must be granted explicitly. These two are not organization-scoped. `catalog` is a **cross-organization read**: one key returns live public events from every organizer on Gomry. Granting it to an organizer's own integration would silently hand that integration a platform-wide catalog. `checkout` goes further — it **sells an arbitrary organizer's tickets** and takes payment against their Stripe account. It is the most privileged scope in this API. `checkout` supports `write` and not `read`: reading a session is part of the same capability, and a read-only checkout scope would grant access to buyer details without any ability to act on them. The `checkout` scope alone is **not sufficient**. A request must also come from a key on Gomry's ACP partner allowlist and carry a valid request signature. A scope can be granted by anyone with settings access; a surface that can sell tickets and charge cards needs a gate a misconfigured dashboard cannot open. See [Access](/commerce/introduction#access). Some endpoints depend on multiple resources. For example, `GET /v1/lists/:id/contacts` requires both `lists:read` and `contacts:read` because the response exposes contact records. Application responses include a resolved `applicant` object (email, first/last name) — this is part of `applications:read`, there is no separate scope for it. Each field is `null` when no linked record (application, user, contact, or a name/email-tagged answer) supplies it. See [List Applications](/api-reference/applications/list-applications). API keys created before `events:write` shipped are capped at `events:read` for backwards compatibility — to enable event creation on a legacy key, edit it in the dashboard and select `write`. The same cap applies to `experiences:write`, `attendees:write`, `ticket_classes:write`, `applications:write`, and `spaces:write`: legacy keys must be opted in explicitly so a key sitting in someone's `.env` cannot silently start mutating tickets, ticket classes, applications, or — most sensitively — creating spaces and setting their tax details when the write endpoints ship. ## Configuring Scopes on a Key When creating a key in **Organization Settings → API Keys**, the scope picker lets you set each resource to `none`, `read`, or `write`. Scopes can be updated later from the same screen without rotating the key. API keys created **before scopes existed** are treated as having full access (`write` on every resource) for backwards compatibility. We recommend tightening these legacy keys to the minimum scopes their integration actually needs. ## Insufficient Scope Errors If a request hits an endpoint the key isn't authorized for, the API returns: ```json theme={null} { "error": "insufficient_scope", "message": "This API key does not have 'read' access to 'payments'.", "required_scope": "payments:read" } ``` Status: `403 Forbidden`. The fix is to grant the missing scope from the dashboard or use a different key. ## Recommendations * **Principle of least privilege.** Start with `none` everywhere, then enable only what your integration needs. * **Use separate keys per integration.** A CRM sync needs `contacts:read`, `lists:read`, `custom_fields:read`. A finance reconciliation job needs `payments:read`. Don't share keys. * **Rotate periodically.** Create a new key with the desired scopes, switch over, then revoke the old one. # Webhooks Source: https://docs.gomry.com/webhooks Receive real-time POST notifications when tickets are purchased and forms are submitted # Webhooks Webhooks let Gomry notify your own systems the moment something happens in your organization — a ticket is purchased, a subscription changes, or a form is submitted. Instead of polling the API, you register one or more HTTPS URLs and Gomry sends an HTTP `POST` request with a JSON payload to each of them as events occur. ## Configuring webhook URLs Webhook endpoints are configured **per organization**. Go to **Organization Settings → Integrations** in your Gomry dashboard. Enter one or more endpoint URLs, **one per line**. Every URL you add receives a copy of every webhook event for your organization. Save your settings. New events will start being delivered to your endpoints. Use an `https://` endpoint that is publicly reachable and responds quickly (see [Delivery & reliability](#delivery--reliability)). Webhooks are scoped to the whole organization — there is currently no per-event subscription. ## How delivery works Each event is sent as a separate HTTP request to every configured URL: | Property | Value | | ------------ | -------------------------------------------------------------- | | Method | `POST` | | Content-Type | `application/json` | | Body | A single JSON event object (see [Payload](#payload-structure)) | | Timeout | 5 seconds | | Retries | None — delivery is best-effort | Your endpoint should respond with a `2xx` status code as quickly as possible. Do any heavy processing **after** acknowledging the request (for example, by pushing the payload onto a queue), so you don't hit the delivery timeout. Webhook payloads are **not currently signed**, and there is no shared secret or signature header on this delivery path. Treat the data as advisory: keep your endpoint URL private, accept requests only over HTTPS, and confirm anything sensitive (such as payment amounts) against the [Payments API](/api-reference/payments/get-payment) using the `payment.id` from the payload before acting on it. ## Event types The `type` field identifies what happened. The most common event is `payment.succeeded`, which fires when an attendee **buys a ticket**. | `type` | When it fires | | ----------------------- | ------------------------------------------------------------------------------------------------------------------- | | `payment.succeeded` | A one-time payment completes — most commonly a **ticket purchase**. `payment.billing_reason` is `one_time_payment`. | | `subscription.created` | A recurring subscription is created. `payment.billing_reason` is `subscription_create`. | | `subscription.deleted` | A subscription is cancelled or ends. | | `form_response.created` | A form / application is submitted for the first time. | | `form_response.updated` | An existing form / application response is updated (e.g. status change). | Some legacy form-response payloads omit the `type` field but always include `formID` and `applicationID`. Use the presence of those fields to identify a form-response event. ## Payload structure All events share one envelope. Fields are populated based on the event `type`, so most are optional — always guard for missing values. | Field | Type | Description | | -------------------- | ----------------- | -------------------------------------------------------------------------- | | `type` | string | The event type (see table above). | | `spaceID` | string | The space the event belongs to, when applicable. | | `organizationID` | string | The organization the event belongs to. | | `date` | string (ISO 8601) | When the event occurred. | | `user` | object | The person who triggered the event (see below). | | `payment` | object | Payment details — present on `payment.*` and `subscription.*` events. | | `subscription` | object | Subscription details — present on `subscription.*` events. | | `lineItems` | array | Line items for the payment. Ticket line items have `type: "ticket"`. | | `formID` | string | The form ID — present on `form_response.*` events. | | `applicationID` | string | The application/response ID — present on `form_response.*` events. | | `submission_answers` | object | Submitted answers keyed by question — present on `form_response.*` events. | | `responseStatus` | string | `Pending`, `Accepted`, `Rejected`, `Draft`, or `Deleted` — form responses. | | `isNewApplication` | boolean | Whether this is the first submission — form responses. | ### `user` object | Field | Type | Description | | ------------------------------------------------------- | ---------------- | ----------------------------------------------- | | `id` | string | The user's ID. | | `contactID` | string | The contact record ID in your organization. | | `name` | string | Full name. | | `firstName` / `lastName` | string | Name parts (populated on form-response events). | | `email` | string | Email address. | | `phoneNumber` | string | Phone number. | | `linkedin`, `instagram`, `github`, `twitter`, `website` | string | Social / web profiles, when provided. | | `bio` | string | Short biography, when provided. | | `location` | string \| object | Location, when provided. | | `country` | string | Country, when provided. | ### `payment` object | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------- | | `id` | string | The payment ID. Use this to look up the payment via the API. | | `grossAmount` | number | Total amount charged, in the smallest currency unit. | | `netAmount` | number | Amount after fees, in the smallest currency unit. | | `currency` | string | ISO 4217 currency code (e.g. `usd`). | | `productID` | string | The product ID. | | `priceID` | string | The price ID. | | `billing_reason` | string | `one_time_payment`, `subscription_create`, or `subscription_cycle`. | ## Example: ticket purchase When an attendee buys a ticket, your endpoint receives a `payment.succeeded` event: ```json theme={null} { "type": "payment.succeeded", "spaceID": "AbCdEfGhIjKlMnOpQrSt", "user": { "id": "user_123", "contactID": "contact_456", "name": "Jordan Rivera", "email": "jordan@example.com", "phoneNumber": "+15551234567", "linkedin": "https://linkedin.com/in/jordanrivera", "country": "US" }, "date": "2026-06-11T18:30:00.000Z", "payment": { "id": "pay_789", "grossAmount": 5000, "netAmount": 4650, "currency": "usd", "productID": "prod_abc", "priceID": "price_def", "billing_reason": "one_time_payment" }, "lineItems": [ { "type": "ticket", "ticket_owner_email": "jordan@example.com", "quantity": 1 } ] } ``` ## Example: form submission ```json theme={null} { "type": "form_response.created", "formID": "form_123", "applicationID": "app_456", "user": { "id": "user_123", "contactID": "contact_456", "name": "Jordan Rivera", "firstName": "Jordan", "lastName": "Rivera", "email": "jordan@example.com" }, "submission_answers": { "What's your company?": "Acme Inc.", "Dietary restrictions": "Vegetarian" }, "responseStatus": "Pending", "isNewApplication": true, "date": "2026-06-11T18:30:00.000Z" } ``` ## Handling webhooks ```javascript Node.js (Express) theme={null} import express from "express"; const app = express(); app.use(express.json()); app.post("/webhooks/gomry", (req, res) => { const event = req.body; // Acknowledge immediately, then process asynchronously. res.status(200).send("ok"); switch (event.type) { case "payment.succeeded": // A ticket was purchased — event.payment.id, event.user.email, … handleTicketPurchase(event); break; case "form_response.created": case "form_response.updated": handleFormResponse(event); break; default: // form-response events may arrive without a `type` if (event.formID) handleFormResponse(event); } }); app.listen(3000); ``` ```python Python (Flask) theme={null} from flask import Flask, request app = Flask(__name__) @app.post("/webhooks/gomry") def gomry_webhook(): event = request.get_json() event_type = event.get("type") if event_type == "payment.succeeded": # A ticket was purchased handle_ticket_purchase(event) elif event_type in ("form_response.created", "form_response.updated") or event.get("formID"): handle_form_response(event) # Acknowledge quickly; process heavy work out of band. return "ok", 200 ``` ## Best practices Return a `2xx` within the 5-second timeout. Offload anything slow (emails, database writes, third-party calls) to a background job or queue. Because there are no delivery guarantees and multiple endpoints can be configured, design your handler so processing the same event twice is safe — key off `payment.id` or `applicationID`. Payloads are unsigned. Before acting on money-related data, re-fetch it from the [Payments API](/api-reference/payments/get-payment) using the `payment.id` from the payload. Most fields are optional and vary by event type. Always check for existence before reading nested values.