# frequentix developer documentation frequentix is an API-first event registration and ticketing platform. Third-party integrations get machine-to-machine OAuth clients, a read-only REST surface over events and orders, and HMAC-signed outbound webhooks. API access and webhooks are available on the Pro and Enterprise plans. Human-readable documentation: https://frequentix.com/developers/docs ## Authentication (OAuth 2.0 client credentials) - Token endpoint: POST https://api.frequentix.com/oauth/token (domain root — NOT under /v1) - Body: grant_type=client_credentials, client_id, client_secret, scope (space-separated) - Response: {token_type, expires_in, access_token}. Cache the token until expires_in lapses; on 401 mint a fresh one and retry once. - Clients are created in the organiser console (Settings → Integrations; MFA required). The secret is shown once; there is no rotation — create a replacement client and revoke the old one. - A client covers every event of its organiser or a selected list; out-of-scope events return 404. Re-scoping applies immediately to live tokens. - Scopes: events:read (Read events and ticket types); orders:read (Read orders) ## Stability & versioning Within /v1, documented fields are never removed or renamed and their meaning does not change. Evolution is additive — new fields, endpoints and webhook event names may appear at any time, so tolerate unknown keys. Every webhook delivery declares the api_version its payload was rendered under. A breaking change, if ever needed, would ship opt-in, pinned per integration client — never as a surprise to existing integrations. ## REST API - Base URL: https://api.frequentix.com/v1 - Auth: Authorization: Bearer - Rate limit: 120 requests/min per integration client (429 + Retry-After beyond it) - List endpoints return a Laravel paginator under "data" at a fixed 25 per page; page with ?page=N and follow next_page_url until null. - Money is integer minor units with an uppercase ISO 4217 currency field. REST timestamps are UTC like 2026-10-02T18:30:00.000000Z; webhook timestamps are offset ISO 8601. - References are opaque lowercase ULIDs with a type prefix: evt_ (Event), tt_ (Ticket type), adn_ (Add-on), ord_ (Order), tkt_ (Ticket), ref_ (Refund), ses_ (Event session), loc_ (Scanner location), wle_ (Waitlist entry), whk_ (Webhook subscription), whd_ (Webhook delivery). - Errors: 401 invalid/expired token; 403 missing scope; 404 unknown key OR event outside the client's scope (deliberately indistinguishable); 429 rate limited. ### GET /v1/integration/events (scope: events:read) Your organiser's events, newest first. An event-scoped client sees only the events it was granted. Event object: - reference (string): Event reference (`evt_…`). - slug (string): URL slug — accepted interchangeably with the reference in paths. - name (string): Event name. - status (string): `draft` or `published`. - starts_at (string | null): Event start, UTC. - ends_at (string | null): Event end, UTC. - timezone (string): IANA timezone the event runs in, e.g. `Europe/London`. - currency (string): Uppercase ISO 4217 currency code. Example request: ``` curl "https://api.frequentix.com/v1/integration/events" \ -H "Authorization: Bearer $FREQUENTIX_TOKEN" ``` ### GET /v1/integration/events/{eventKey} (scope: events:read) One event with its ticket types, including live availability counters. Event object: - reference (string): Event reference (`evt_…`). - slug (string): URL slug — accepted interchangeably with the reference in paths. - name (string): Event name. - status (string): `draft` or `published`. - starts_at (string | null): Event start, UTC. - ends_at (string | null): Event end, UTC. - timezone (string): IANA timezone the event runs in, e.g. `Europe/London`. - currency (string): Uppercase ISO 4217 currency code. ticket_types[] — Ticket type object: - reference (string): Ticket type reference (`tt_…`). - name (string): Ticket type name. - price (integer): Price in minor units (pence, cents). - currency (string): Uppercase ISO 4217 currency code. - is_active (boolean): Whether it is on sale. - total (integer | null): Capacity — null when inventory is unlimited. - sold (integer | null): Tickets sold so far. Example request: ``` curl "https://api.frequentix.com/v1/integration/events/evt_01k0zqj4d8v6snb2x9e7g3m5cr" \ -H "Authorization: Bearer $FREQUENTIX_TOKEN" ``` ### GET /v1/integration/events/{eventKey}/orders (scope: orders:read) The event's orders, newest first, with buyer contact details. Order object: - reference (string): Order reference (`ord_…`). - status (string): `draft`, `pending_payment`, `awaiting_approval`, `paid`, `free_confirmed`, `cancelled` or `refunded`. - currency (string): Uppercase ISO 4217 currency code. - subtotal (integer): Line-item total before discounts and tax, in minor units. - discount_total (integer): Discounts applied, in minor units. - tax_total (integer): Tax charged, in minor units. - total (integer): Amount paid, in minor units. - contact (object | null): Buyer contact — `{email, first_name, last_name}` — or null when none is attached. - confirmed_at (string | null): When the order confirmed, UTC. - created_at (string): When the order was created, UTC. Example request: ``` curl "https://api.frequentix.com/v1/integration/events/northlight-sessions-autumn-run/orders?page=1" \ -H "Authorization: Bearer $FREQUENTIX_TOKEN" ``` ## Webhooks Subscriptions are managed in the console (Delivery → Webhooks): an HTTPS endpoint plus a chosen set of events, organisation-wide or scoped to a single event. The signing secret (whsec_…) is shown once. Endpoint URLs must resolve to a public address; redirects are not followed. Deliveries are POSTs with envelope {"event", "created_at", "api_version", "data"} (api_version, currently 2026-08-01, names the payload generation; JSON key order is not significant and varies) and headers X-Frequentix-Signature (t=,v1=), X-Frequentix-Event, X-Frequentix-Delivery (stable across retries — use it to deduplicate). Signature = hex(HMAC-SHA256(secret, "{t}.{raw body}")). Verify over the raw request bytes, tolerate ~5 minutes of clock skew, compare in constant time. Respond 2xx within 15 seconds. Failures retry up to 8 attempts (10s, 30s, 2m, 5m, 15m, 1h, 2h — ~3.5h total); after the final failure the subscription is auto-disabled and the organiser emailed. Missed events are not replayed — reconcile over the REST API. Worked signature example (verified by the site's test suite): ``` secret = whsec_9fJq2mX7RtA4LcW8zN3pYv6KdB5hUgS1eT0aQrHx t = 1786730651 body = {"data":{"event":"evt_01k0zqj4d8v6snb2x9e7g3m5cr","order":"ord_01k1p3v9qe6rwyx0m5h8t2c4nf","total":8250,"status":"paid","tickets":3,"currency":"GBP","contact_email":"amelia.hart@example.com","application_fee":165},"event":"order.created","created_at":"2026-08-14T19:04:11+01:00","api_version":"2026-08-01"} X-Frequentix-Signature: t=1786730651,v1=722e85f919583b00ee5a48ee2f406a3f7ff7ca45978c26c44b5c0ccea4a5ad84 ``` ## Webhook event catalogue ### Orders #### order.created Fires once an order confirms — for paid and free orders alike — after the transaction has committed, so the order and its tickets are readable by the time the delivery arrives. Payload fields: - order (string): Order reference. - status (string): `paid` for a paid order, `free_confirmed` for a free one. - currency (string): Uppercase ISO 4217 currency code. - total (integer): Order total in minor units (pence, cents). - application_fee (integer): Platform fee taken from the total, in minor units. - event (string | null): Reference of the event the order belongs to. - contact_email (string | null): The buyer's email address, when a contact is attached. - tickets (integer): Number of tickets issued on the order. Example delivery: ```json { "event": "order.created", "created_at": "2026-08-14T19:04:11+01:00", "api_version": "2026-08-01", "data": { "order": "ord_01k1p3v9qe6rwyx0m5h8t2c4nf", "status": "paid", "currency": "GBP", "total": 8250, "application_fee": 165, "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "contact_email": "amelia.hart@example.com", "tickets": 3 } } ``` #### order.refunded Fires for every refund against an order, whether issued from the console or made directly in the Stripe dashboard and reconciled afterwards. `refund` and `fee_reversed` are present only for refunds issued through frequentix; a dashboard-made refund is reconciled from Stripe's records, which carry neither. Payload fields: - order (string): Order reference. - refund (string): Refund reference. Omitted when the refund was made in the Stripe dashboard. - amount (integer): Amount returned by this refund, in minor units. - fee_reversed (integer): Platform fee returned with this refund, in minor units. Omitted for dashboard-made refunds. - fully_refunded (boolean): Whether the whole order is now settled as refunded. Example delivery: ```json { "event": "order.refunded", "created_at": "2026-08-20T11:30:02+01:00", "api_version": "2026-08-01", "data": { "order": "ord_01k1p3v9qe6rwyx0m5h8t2c4nf", "refund": "ref_01k2e0qcsg6vy9x4n7f3d8w5jm", "amount": 2750, "fee_reversed": 55, "fully_refunded": false } } ``` #### order.cancelled Fires when a confirmed order — paid or free — is cancelled, voiding its tickets and returning capacity to the pool. A cancellation never moves money; listen for `order.refunded` to know when funds are returned. Payload fields: - order (string): Order reference. - reason (string | null): The organiser-supplied cancellation reason, when given. Example delivery: ```json { "event": "order.cancelled", "created_at": "2026-08-18T16:45:29+01:00", "api_version": "2026-08-01", "data": { "order": "ord_01k1p3v9qe6rwyx0m5h8t2c4nf", "reason": "Duplicate booking" } } ``` ### Tickets #### ticket.scanned Fires on every scan verdict, from the scanner app and box-office check-in alike — denials included, so your systems see the same stream the door does. `reason` is a stable key: `scanning.allowed` on success, or a denial key such as `scanning.already_scanned`, `scanning.wrong_entry_point`, `scanning.outside_validity_window`, `scanning.outside_session_window`, `scanning.session_cancelled`, `scanning.ticket_cancelled` or `scanning.ticket_refunded`. Payload fields: - ticket (string): Ticket reference. - event (string): Event reference. - result (string): `allowed` or `denied`. - reason (string): Stable verdict key, e.g. `scanning.allowed`. - scanner_location (string | null): Reference of the entry point that scanned, when one was set. - scanned_at (string): When the scan happened, ISO 8601. Example delivery: ```json { "event": "ticket.scanned", "created_at": "2026-10-02T19:12:44+01:00", "api_version": "2026-08-01", "data": { "ticket": "tkt_01k1p3vazh5cw9x2r7m4e8t0bd", "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "result": "allowed", "reason": "scanning.allowed", "scanner_location": "loc_01k1p2tafd3vw6x8m5e9c2b7hs", "scanned_at": "2026-10-02T19:12:44+01:00" } } ``` #### ticket.updated Fires when an existing ticket is edited — a status change, reassignment or detail update. Each change increments `revision`, so out-of-order deliveries can be sequenced. There is deliberately no `ticket.deleted` event. Payload fields: - ticket (string): Ticket reference. - event (string): Event reference. - status (string): `valid`, `cancelled`, `refunded` or `checked_in`. - ticket_type (string | null): Reference of the ticket's type. - revision (integer): Monotonic edit counter for this ticket. - updated_at (string | null): When the change was made, ISO 8601. Example delivery: ```json { "event": "ticket.updated", "created_at": "2026-09-30T09:21:07+01:00", "api_version": "2026-08-01", "data": { "ticket": "tkt_01k1p3vazh5cw9x2r7m4e8t0bd", "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "status": "cancelled", "ticket_type": "tt_01k0zqj5ae8txcv3m7d9e2b4hs", "revision": 3, "updated_at": "2026-09-30T09:21:07+01:00" } } ``` ### Add-ons #### add_on.redeemed Fires when a unit of a purchased add-on — a drink token claimed, merch collected — is redeemed at the door or box office. A replayed redemption (the same `client_reference` retried) does not fire again. Payload fields: - order_line (string): Reference of the order line being redeemed (`oln_…`). - add_on (string | null): Reference of the add-on (`adn_…`). - event (string): Event reference. - quantity (integer): Units redeemed in this action. - remaining (integer): Units still unredeemed on the line afterwards. - scanner_location (string | null): Reference of the entry point that redeemed, when one was set. - redeemed_at (string): When the redemption happened, ISO 8601. Example delivery: ```json { "event": "add_on.redeemed", "created_at": "2026-10-02T20:05:31+01:00", "api_version": "2026-08-01", "data": { "order_line": "oln_01k1p4wbvj6dy1z6n9h5f3e8ta", "add_on": "adn_01k0zqj6gbn04x9w2e7t5c3d8m", "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "quantity": 1, "remaining": 1, "scanner_location": "loc_01k1p2tafd3vw6x8m5e9c2b7hs", "redeemed_at": "2026-10-02T20:05:31+01:00" } } ``` ### Payments #### charge.disputed Fires when Stripe reports a dispute against one of your charges. The identifiers are Stripe's — a dispute and charge id, not frequentix references — and every field can be null depending on what Stripe sends. Note: A dispute is account-level, not tied to a frequentix event, so it is delivered to organiser-wide subscriptions only — an event-scoped subscription never receives it. Payload fields: - dispute (string | null): Stripe dispute id (`dp_…`). - charge (string | null): Stripe charge id (`ch_…`). - amount (integer | null): Disputed amount in Stripe's minor units. - reason (string | null): Stripe's dispute reason, e.g. `fraudulent`. Example delivery: ```json { "event": "charge.disputed", "created_at": "2026-08-27T08:14:52+01:00", "api_version": "2026-08-01", "data": { "dispute": "dp_1Q8XkT2c9aWv5RhM0nB3pDfE", "charge": "ch_3Q8XkR2c9aWv5RhM1kV9tYzL", "amount": 5500, "reason": "fraudulent" } } ``` ### Waitlists #### waitlist.joined Fires when a new entry joins a waitlist for a sold-out ticket type or add-on. Payload fields: - waitlist_entry (string): Waitlist entry reference. - status (string): The entry's status — `waiting` at the moment this event fires. - email (string): The waiter's email address. - quantity (integer): Number of places requested. - event (string | null): Reference of the event the waitlist belongs to. - sellable (string | null): Reference of what is waited on — a ticket type (`tt_…`) or add-on (`adn_…`). - order (string | null): Reference of the order the entry converted into — null until `waitlist.converted`. Example delivery: ```json { "event": "waitlist.joined", "created_at": "2026-08-16T12:08:19+01:00", "api_version": "2026-08-01", "data": { "waitlist_entry": "wle_01k2d9pbrf5tx8w3m6e2c7v4hn", "status": "waiting", "email": "theo.marsh@example.com", "quantity": 2, "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "sellable": "tt_01k0zqj5famz3w8p6t2v0c9d4e", "order": null } } ``` #### waitlist.offered Fires when freed capacity is offered to an entry — the buyer has a limited window to complete their purchase before the offer expires. Payload fields: - waitlist_entry (string): Waitlist entry reference. - status (string): The entry's status — `offered` at the moment this event fires. - email (string): The waiter's email address. - quantity (integer): Number of places requested. - event (string | null): Reference of the event the waitlist belongs to. - sellable (string | null): Reference of what is waited on — a ticket type (`tt_…`) or add-on (`adn_…`). - order (string | null): Reference of the order the entry converted into — null until `waitlist.converted`. Example delivery: ```json { "event": "waitlist.offered", "created_at": "2026-08-19T09:00:03+01:00", "api_version": "2026-08-01", "data": { "waitlist_entry": "wle_01k2d9pbrf5tx8w3m6e2c7v4hn", "status": "offered", "email": "theo.marsh@example.com", "quantity": 2, "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "sellable": "tt_01k0zqj5famz3w8p6t2v0c9d4e", "order": null } } ``` #### waitlist.converted Fires when an offered entry completes checkout. `order` carries the resulting order's reference — expect a matching `order.created` delivery too. Payload fields: - waitlist_entry (string): Waitlist entry reference. - status (string): The entry's status — `converted` at the moment this event fires. - email (string): The waiter's email address. - quantity (integer): Number of places requested. - event (string | null): Reference of the event the waitlist belongs to. - sellable (string | null): Reference of what is waited on — a ticket type (`tt_…`) or add-on (`adn_…`). - order (string | null): Reference of the order the entry converted into — null until `waitlist.converted`. Example delivery: ```json { "event": "waitlist.converted", "created_at": "2026-08-19T09:41:57+01:00", "api_version": "2026-08-01", "data": { "waitlist_entry": "wle_01k2d9pbrf5tx8w3m6e2c7v4hn", "status": "converted", "email": "theo.marsh@example.com", "quantity": 2, "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "sellable": "tt_01k0zqj5famz3w8p6t2v0c9d4e", "order": "ord_01k2f1rdth8wz1y6p9h5f0d3ka" } } ``` ### Sessions #### session.cancelled Fires when one dated occurrence of a recurring event is cancelled — whether or not anything was sold, since an unsold night still changes a published schedule an integration may be mirroring. Sold tickets deliberately stay valid and refundable; `tickets` counts the still-valid tickets outstanding, so don't assume zero when reconciling. Payload fields: - session (string): Session reference. - event (string): Event reference. - name (string | null): The session's display name. - starts_at (string): Session start, ISO 8601. - ends_at (string): Session end, ISO 8601. - tickets (integer): Still-valid tickets outstanding for the session. Example delivery: ```json { "event": "session.cancelled", "created_at": "2026-11-01T10:15:36+00:00", "api_version": "2026-08-01", "data": { "session": "ses_01k2c8ndqe4vw7x9m2t5b3h8fa", "event": "evt_01k0zqj4d8v6snb2x9e7g3m5cr", "name": "Evening show — 14 November", "starts_at": "2026-11-14T19:30:00+00:00", "ends_at": "2026-11-14T22:00:00+00:00", "tickets": 118 } } ``` ## Verifying a signature (Node.js reference implementation) ```javascript import crypto from "node:crypto"; import express from "express"; const app = express(); const secret = process.env.FREQUENTIX_WEBHOOK_SECRET; // whsec_… // express.raw keeps the body as the exact bytes that were signed — // re-serialising parsed JSON would break the signature. app.post( "/webhooks/frequentix", express.raw({ type: "application/json" }), (req, res) => { const header = req.header("X-Frequentix-Signature") ?? ""; const parts = Object.fromEntries( header.split(",").map((pair) => pair.split("=", 2)), ); const timestamp = Number(parts.t); const expected = String(parts.v1 ?? ""); // Each attempt is signed freshly, so a 5-minute tolerance blocks replays. if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) { return res.status(400).send("Stale signature"); } const digest = crypto .createHmac("sha256", secret) .update(`${timestamp}.`) .update(req.body) // Buffer of the raw body .digest("hex"); const valid = expected.length === digest.length && crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(expected)); if (!valid) return res.status(400).send("Invalid signature"); const event = JSON.parse(req.body.toString("utf8")); const deliveryId = req.header("X-Frequentix-Delivery"); // stable across retries res.status(204).end(); // acknowledge fast… handleAsync(event, deliveryId); // …then do the real work asynchronously }, ); ```