Skip to content

Webhooks

Webhooks push events to your server as HTTPS POST requests, signed so you can check they come from Status302. Use them for things you react to; use the stream for continuous market data.

Webhooks need the Elite plan or higher. Manage endpoints in Settings → Developers → Webhooks or over the API with the webhooks:write scope. An organization can have up to 50 endpoints.

Type Sent when data
alert.fired One of your organization’s alert rules fires. The firing, as in GET /v1/alerts/events
onsale.announced A new event, on-sale or presale is announced. The onsales stream payload
event.status_changed An event in the endpoint’s list changes status: on sale, off sale, cancelled, postponed or rescheduled. {eventId, discoveryId, name, from, to, startsAt}
list.sale A ticket sale on an event in the endpoint’s list. The Sale schema with its event summary
inventory.restocked Primary (face value) seats become available on an event in the endpoint’s list: new releases, returns, GA pools growing. Market activity
inventory.sold Primary seats on an event in the endpoint’s list are sold. Market activity
resale.listed Seats on an event in the endpoint’s list are listed on Ticketmaster resale. Market activity
resale.sold Ticketmaster resale listings on an event in the endpoint’s list disappear in a way that looks like a sale. Market activity

All types except alert.fired and onsale.announced follow a saved list: set the endpoint’s listId, and only events in that list trigger them. The unfiltered sales firehose is only on the stream.

To send different events to different places, register one endpoint per destination with the types it needs: for example restocks to one URL and resale activity to another.

markets limits list.sale and the four market-activity types to some markets:

Market Covers
primary Ticketmaster face-value inventory: inventory.restocked, inventory.sold and primary list.sale
tm_resale Ticketmaster verified resale: resale.listed, resale.sold and resale list.sale

Leave markets empty (the default) for every market. More markets, such as secondary marketplaces as secondary:<marketplace>, will be added to this list; treat values you don’t know as other markets. PATCH the endpoint with "markets": [] to remove the filter.

Status302 polls each event’s Ticketmaster inventory and compares consecutive polls. Each of the four market-activity types is delivered at most once per poll of an event: one delivery carries every change of that type the poll found, grouped by section, row and price.

{
"id": "5d1c0e8a-77f3-5b0e-9a4c-2f61d0c3b8e4",
"type": "inventory.restocked",
"createdAt": "2026-11-05T00:00:02.114Z",
"test": false,
"data": {
"event": {
"id": 46, "name": "…", "startsAt": "…", "segment": "Music", "genre": "Rock", "venue": "…", "city": "…", "country": "US",
"imageUrl": "…", "url": "https://www.ticketmaster.com/…/event/…"
},
"market": "primary",
"at": "2026-11-05T00:00:01Z",
"quantity": 6,
"truncated": false,
"changes": [
{ "kind": "restock", "section": "101", "row": "A", "seats": ["1", "2"], "quantity": 2,
"priceMinor": 13510, "listMinor": 11500, "currency": "USD", "confidence": null },
{ "kind": "ga_restock", "section": "GA Floor", "row": "", "seats": [], "quantity": 4,
"priceMinor": 8900, "listMinor": 7500, "currency": "USD", "confidence": null }
]
}
}
  • event is the event summary, with url its Ticketmaster page when known.
  • market is primary for inventory.* and tm_resale for resale.*. at is the time of the poll.
  • changes lists the seats that changed. kind is restock or ga_restock for inventory.restocked, sale or ga_sale for inventory.sold, resale_listed for resale.listed and resale_removed for resale.sold. General admission pools have an empty row and no seats. priceMinor is the all-in price per ticket in minor units, listMinor the face price before fees; both are null when the seats have no known price.
  • quantity counts tickets over all changes. A poll with more than 200 changes delivers the first 200 and sets truncated.

The envelope id is derived from the poll and the type, so a poll is never delivered twice, even across retries and Status302 restarts.

Resale price changes on a listing that stays listed are not detected.

  1. Expose an HTTPS URL that accepts POST and answers 2xx quickly. The samples below listen on /webhooks/status302.

  2. Register it with the event types it wants. The response contains the endpoint’s signing secret (whsec_…) once; store it with your receiver.

    samples/curl/webhooks-create.sh
    #!/usr/bin/env bash
    # Register an endpoint for alert firings and new on-sales. The answer holds the
    # signing secret (whsec_…) exactly once: store it with your receiver.
    set -euo pipefail
    API="${STATUS302_API_URL:-https://api.status302.com}"
    curl --fail-with-body -sS "$API/v1/webhooks" \
    -H "Authorization: Bearer $STATUS302_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"$WEBHOOK_URL\", \"description\": \"alerts and on-sales\", \"events\": [\"alert.fired\", \"onsale.announced\"]}"
  3. Send a test event and check the delivery log:

    samples/curl/webhooks-test.sh
    #!/usr/bin/env bash
    # Queue a signed test delivery ("test": true) to an endpoint, then read its
    # delivery log: status, attempts, response status and latency.
    set -euo pipefail
    API="${STATUS302_API_URL:-https://api.status302.com}"
    AUTH="Authorization: Bearer $STATUS302_API_KEY"
    curl --fail-with-body -sS -X POST "$API/v1/webhooks/$WEBHOOK_ID/test" -H "$AUTH" \
    -H "Content-Type: application/json" -d '{"type": "alert.fired"}' | jq '{id, eventId, status}'
    sleep 2
    curl --fail-with-body -sS "$API/v1/webhooks/$WEBHOOK_ID/deliveries?limit=5" -H "$AUTH" \
    | jq '.items[] | {id, event, test, status, attempts, responseStatus, latencyMs}'

Endpoint URLs must use https, must not contain credentials, and must resolve to public addresses: private, loopback and link-local networks are refused when you register the URL and again when Status302 connects. Redirects aren’t followed.

Every delivery has the same envelope:

{
"id": "0b9d7c1e-2f4a-4d6b-9e8f-1a2b3c4d5e6f",
"type": "list.sale",
"createdAt": "2026-11-05T00:00:00Z",
"test": false,
"data": {
"ts": "2026-11-04T23:59:41Z", "eventId": 46, "scope": "primary", "kind": "sale", "section": "101", "row": "A",
"seats": ["1", "2"], "qty": 2, "priceMinor": 13510, "listMinor": 11500, "currency": "USD", "confidence": "inferred_high",
"event": { "id": 46, "name": "…", "startsAt": "…", "segment": "Music", "genre": "Rock", "venue": "…", "city": "…", "country": "US" }
}
}
  • id identifies the event. Retries and redeliveries repeat it: process each id once.
  • type is one of the events above. test is true for test deliveries, whose data is a realistic sample.
  • createdAt is when the event happened, in UTC with millisecond precision.

Each request also carries these headers:

Header Value
Status302-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>, see verifying signatures
Status302-Event The event type, e.g. alert.fired
Status302-Delivery-Id The delivery’s id, the same on every attempt; a redelivery gets a new one
Status302-Attempt 1 for the first attempt, then 2, 3, …
User-Agent Status302-Webhooks/1
Content-Type application/json

The signature is an HMAC-SHA256 over the timestamp, a dot and the raw request body, keyed with the whole signing secret (including whsec_):

signed_payload = t + "." + raw_body
v1 = hex(HMAC_SHA256(key = secret, message = signed_payload))

To verify a delivery:

  1. Parse t and every v1 from Status302-Signature.
  2. Reject it if t is more than 5 minutes from your clock: this stops replays of captured requests.
  3. Compute the HMAC of t + "." + body over the bytes you received, before any JSON parsing or re-encoding.
  4. Compare it with each v1 in constant time. Accept if any matches.

Answer 2xx once verified (and stored), and do the actual work afterwards; answer 4xx to a failed verification.

samples/typescript/webhook-receiver.ts
// A webhook receiver on node:http. Verify the signature over the raw body
// before parsing, answer 2xx fast, and process each event `id` once (retries
// and redeliveries repeat it).
import { createServer } from "node:http";
import { verifyWebhook, WebhookVerificationError } from "@status302/sdk";
const secret = process.env.STATUS302_WEBHOOK_SECRET ?? "";
const processed = new Set<string>(); // use your database in production
const server = createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/status302") {
res.writeHead(404).end();
return;
}
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const body = Buffer.concat(chunks);
try {
const event = await verifyWebhook(body, req.headers["status302-signature"]?.toString(), secret);
if (processed.has(event.id)) {
console.log(`duplicate ${event.type} ${event.id} (delivery ${req.headers["status302-delivery-id"]})`);
} else {
processed.add(event.id);
console.log(`verified ${event.type} ${event.id}${event.test ? " (test)" : ""}`);
}
res.writeHead(204).end();
} catch (err) {
if (!(err instanceof WebhookVerificationError)) throw err;
console.log(`rejected: ${err.message}`);
res.writeHead(400).end(err.message);
}
});
server.listen(Number(process.env.PORT ?? 3000), () => console.log(`listening on :${process.env.PORT ?? 3000}`));

The TypeScript and Python receivers use the SDKs’ verifyWebhook / verify_webhook; the Go receiver shows the algorithm with the standard library only. All three are tested against real signed deliveries on every change.

A delivery succeeds when your endpoint answers any 2xx status within 10 seconds. Anything else (another status, a timeout, a connection error, a redirect) is a failure, and Status302 retries with exponential backoff: after 30 seconds, then 1, 2, 4, 8, 16, 32 and 64 minutes, then every 2 hours, for up to 24 hours after the event. Each attempt is signed afresh with a new t. Test deliveries are attempted once and not retried.

Because of retries and redeliveries, the same event can arrive more than once: deduplicate on the envelope id. Deliveries can also arrive out of order; use createdAt or the payload’s own timestamps to order them.

An endpoint whose deliveries have all failed for 24 hours is disabled: Status302 stops sending to it, fails its pending deliveries and emails the organization’s owners and admins. The endpoint shows the reason in Settings. Fix the receiver, then re-enable the endpoint (PATCH /v1/webhooks/{webhookId} with "status": "active"); test deliveries don’t count towards disabling.

GET /v1/webhooks/{webhookId}/deliveries lists the last 30 days of deliveries, newest first, with their status (pending, succeeded, failed), attempt count, your response’s status and first 512 bytes, latency and next attempt time. The same log is in Settings.

To send a delivery again, for example after fixing a bug in your receiver:

samples/curl/webhooks-redeliver.sh
#!/usr/bin/env bash
# Send a delivery again (e.g. after fixing your receiver). The envelope keeps its
# `id`, so a receiver that deduplicates on it processes the event once.
set -euo pipefail
API="${STATUS302_API_URL:-https://api.status302.com}"
curl --fail-with-body -sS -X POST \
"$API/v1/webhooks/$WEBHOOK_ID/deliveries/$DELIVERY_ID/redeliver" \
-H "Authorization: Bearer $STATUS302_API_KEY" | jq '{id, eventId, status, redeliveryOf}'

A redelivery is a new delivery (new Status302-Delivery-Id, redeliveryOf pointing at the original) of the same envelope, id included, so a receiver that deduplicates processes it once.

An endpoint can have at most 5 pending test deliveries and redeliveries at a time; further requests answer 429 with "limit": "concurrent" and a Retry-After until some of them finish.

POST /v1/webhooks/{webhookId}/rotate-secret (or Rotate secret in Settings) returns a new secret, which signs every delivery from that moment; the old secret stops being used immediately. To rotate without failed deliveries, let your receiver accept a list of secrets:

  1. Rotate, and add the new secret to your receiver’s list next to the old one.
  2. Deploy. Deliveries that failed in between are retried and now verify.
  3. Remove the old secret.

Both SDKs’ verify functions accept an array of secrets for this.