frequentix
Developer docs

Build on the same API the product runs on.

The storefront, console and scanner are all clients of the frequentix API. These pages document the surface you can use too — OAuth clients, REST reads and signed webhooks — with worked examples our test suite verifies.

Documentation menu

Webhooks

Webhooks push what happens on the platform — orders, scans, refunds, disputes, waitlists, cancelled sessions — to your endpoint as it happens. Every delivery is HMAC-signed, retried with backoff, and recorded in a delivery log you can inspect in the console.

Subscribing

In the console, open Delivery → Webhooks (multi-factor-verified session required). A subscription is an endpoint URL plus the set of events it wants, and it can cover your whole organisation or be scoped to a single event.

  • The signing secret (whsec_…) is shown once, at creation — store it like a password.
  • An event-scoped subscription only ever receives deliveries belonging to its event. Account-level events with no event attached — like charge.disputed — go to organisation-wide subscriptions only.
  • Event matching is exact — there are no wildcards; pick each event explicitly.

Endpoint requirements

  • A publicly resolvable URL, at most 2048 characters, with no credentials in it (user:pass@ is rejected). HTTPS is strongly recommended; plain HTTP is accepted.
  • The host must resolve to a public address — loopback, private-range and cloud-metadata targets are rejected, both when the subscription is created and again at every delivery.
  • Redirects are not followed — the endpoint must answer directly.

The delivery

A delivery is an HTTP POST with a JSON envelope: the event name, when it was created, the api_version the payload was rendered under, and the event’s payload under data. JSON key order is not significant and varies between deliveries — read by key, never by position.

Envelope · order.created
{
  "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
  }
}
X-Frequentix-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-Frequentix-EventThe event name, e.g. order.created
X-Frequentix-DeliveryThe delivery reference (whd_…) — stable across retries
Content-Typeapplication/json

Your endpoint has 15 seconds to respond. Any 2xx counts as delivered; anything else — including a timeout — counts as a failed attempt.

Verifying the signature

The signature header carries a unix timestamp and a hex HMAC: t=<seconds>,v1=<signature>. The signature is HMAC-SHA256 over the timestamp, a literal dot, and the raw request body, keyed with your subscription’s secret. A complete worked example — which this site’s test suite recomputes and verifies on every build:

Worked example
# The signing secret — shown once when the subscription was created
secret = whsec_9fJq2mX7RtA4LcW8zN3pYv6KdB5hUgS1eT0aQrHx

# Timestamp and raw body of the delivery
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"}

# signature = hex( HMAC-SHA256( secret, "{t}.{body}" ) )
X-Frequentix-Signature: t=1786730651,v1=722e85f919583b00ee5a48ee2f406a3f7ff7ca45978c26c44b5c0ccea4a5ad84
  • Verify over the raw bytes. Parse the JSON for your handler, but compute the HMAC against the body exactly as received — a re-serialisation can differ in whitespace, key order or escaping, and the signature covers exact bytes. Read the body before any JSON middleware touches it.
  • Reject stale timestamps. Each attempt is signed freshly, so tolerate a small window — we recommend 5 minutes — and refuse anything older; that closes replay.
  • Compare in constant time timingSafeEqual, hash_equals, compare_digest, fixed_length_secure_compare — never ==.
Verify a delivery · Node.js
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
  },
);

Retries and failure

A failed delivery is retried up to 8 attempts, with the gap growing each time: 10 s, 30 s, 2 min, 5 min, 15 min, 1 h, then 2 h — about three and a half hours end to end. Every attempt is recorded on the delivery: its status, the response code, and the first 2,000 characters of the response body, all visible in the console’s delivery log.

When the final attempt fails, the subscription is auto-disabled with the reason stored, and the organiser is emailed. Re-enabling it in the console clears the reason and resumes delivery for new events — missed events are not replayed, so reconcile over the REST API after an outage.

Consuming reliably

  • Acknowledge fast, process async. Verify, enqueue, return 2xx — well inside the 15-second window. Slow handlers become failed attempts, and repeated failure disables the subscription.
  • Deduplicate on X-Frequentix-Delivery. The reference is stable across retries of the same delivery, so a retry after a timeout you actually processed is detectable. (The timestamp and signature are fresh on each attempt.)
  • Don’t assume order. Deliveries can arrive out of sequence; use the payload’s own markers — revision on ticket.updated, status on waitlist events — rather than arrival order.
  • Return 2xx even for events you ignore. Filtering belongs in your handler, not in refusing deliveries.