Skip to main content
Webhook callback sent by Headout when a subscribed event occurs. Sent once per event, per matching event subscription.
product.listing_status_changed, product.variant_listing_status_changed, product.data_changed, and product.variant_pricing_changed are the supported event types.

Headers

Every delivery includes: Apart from the headers above, a delivery carries only standard HTTP transport headers (Host, Connection, Accept-Encoding). No other headers are sent — in particular no Accept, Accept-Language, Referer, Cookie, or tracing headers. Deliveries are server-to-server requests from Headout’s infrastructure, not browser traffic: they carry no cookies, no Referer, and no browser identity. If a bot-protection or WAF layer sits in front of your endpoint, exempt the webhook path from browser heuristics (optionally scoping the exemption to the User-Agent prefix, which is easy to spoof and is not a security control), and verify every delivery’s X-Headout-Signature in your application as described below. A blocked request that is held open rather than rejected counts as a timeout on our side and is retried on the schedule below.

Signature verification vs. delivery authentication

These headers carry two independent security mechanisms — one is how Headout proves itself to you, the other is how Headout authenticates against your server:
  • Signature verification (X-Headout-Signature) proves the payload came from Headout and was not modified in transit. It is always present, computed with the subscription’s signingSecret.
  • Delivery authentication (Authorization) is a value you configure on the subscription and Headout sends back verbatim, so your server (or a gateway in front of it) can apply its own access control.
Always verify the signature, even when an Authorization header is configured — the Authorization value only controls access to your endpoint; it does not prove the payload is authentic or unmodified.

Verifying the signature

X-Headout-Signature has the form:
To verify a delivery:
  1. Extract t and v1 from the header.
  2. Compute HMAC-SHA256(signingSecret, "<t>.<raw request body>") using the subscription’s signingSecret (returned once from Create event subscription or Rotate signing secret), over the exact raw bytes of the request body — not a re-serialized version of it.
  3. Hex-encode the result (lowercase) and compare it to v1 using a constant-time comparison.
Verify against the raw request body bytes exactly as received. Parsing the JSON and re-serializing it (for example JSON.stringify) produces different bytes, and the signature will not match.
Secret rotation — after you rotate the signing secret, deliveries (including retries of earlier failed deliveries) are signed with the new secret. Accept a delivery that verifies against either your current or previous secret for at least the next 24 hours after rotating, then discard the previous one.The signature also carries a timestamp t, so you can optionally add replay protection by rejecting deliveries whose t is too old.

Response & retry behavior

Your endpoint should return a 2xx status as soon as the event is durably received — do heavier processing asynchronously if needed.
  • 2xx — Delivery successful, no retry.
  • Any other status, or a timeout — Treated as a failed delivery and retried. Non-2xx responses are not treated differently by status code today (a 4xx retries the same as a 5xx).
  • Retries run on a backoff schedule of 5m → 30m → 3h → 18h → 24h, up to 6 attempts total. After the 6th failed attempt, the delivery is not retried again.
  • Each delivery attempt allows roughly 15 seconds end-to-end for your endpoint to respond.

Ordering and deduplication

Deliveries are not ordered. Events are dispatched independently of each other, and a failed delivery is retried on the backoff above — so an event retried hours later can arrive after a newer event for the same product. Reconcile on the payload rather than on arrival order: The payload carries no version or sequence number, and id is opaque — don’t derive order from it. Delivery is at-least-once, so the same id can arrive more than once: an attempt whose response was lost or timed out is retried even though you processed it. Make your handler idempotent on id.
Each event carries the state Headout observed at createdAt, not a transition. Two changes in quick succession can collapse into a single event describing the net result, so apply the payload as the current state instead of replaying a sequence of flips.

Reading a product.listing_status_changed event

listingStatus is the product’s status as of createdAt: LISTED means it is available to sell, UNLISTED means it is not — stop offering it until a later LISTED event arrives. A product can flip either way any number of times, so apply each event as the current state rather than as a one-off transition. If you subscribe to both event types, a product being listed or delisted also produces a product.variant_listing_status_changed event for the same change. They are independent deliveries — neither their order nor their arrival together is guaranteed, so apply each on its own.

Reading a product.variant_listing_status_changed event

data carries only the variants that changed — listedVariantIds for newly listed, delistedVariantIds for newly unlisted. Variants that didn’t change are omitted, so apply both arrays against the variants you already hold rather than treating them as the full list. For example, if product 3492 has variants 5397, 16552, 20770 and this event lists 52632, 83740 and unlists 5397, 16552, then 20770 is unchanged — leaving 20770, 52632, 83740 live.

Reading a product.data_changed event

Sent when the content of a listed product changes — the product was listed at both the previous and current sync and at least one tracked field now differs. A product being listed or unlisted is reported by product.listing_status_changed, and variants being added or removed by product.variant_listing_status_changed — not here. data.changedFields lists the top-level fields that changed; it is never empty. Nested changes roll up to their top-level field, and any variant-level change collapses into a single variants entry. The event names what changed, not the new value — re-fetch the product with Get a product to read the updated data. Product-level fields Variant fields — any of the below, on a variant present at both syncs, adds a single variants entry (variant changes never appear individually):
Ordering within a list is ignored — reordering points of interest, operating schedules, or input fields without changing their content does not emit an event. Adding or removing a whole variant is not reported here either; subscribe to product.variant_listing_status_changed for that.

Reading a product.variant_pricing_changed event

Sent when the price of a variant changes for one of the next few dates — currently today, tomorrow, or the day after. It is a notification, not a price feed — the payload tells you which variant and date moved, never the new amounts. When one arrives, re-fetch the current price from the List availabilities endpoint and update your side. data carries three fields: Which dates are watched. Only the nearest few dates per variant trigger events — currently today, tomorrow, and the day after, in the timezone of the experience’s city. Price changes further out do not emit; you’ll pick those up the next time you fetch availability. Each event covers exactly one date, so if a variant’s price changes for two of those days in the same sync, you receive two separate events. When it fires — and when it doesn’t. Only changes on the watched dates above (today, tomorrow, or the day after) are considered, and among those a change triggers an event only in these cases: The threshold is measured against the last price we notified you about, not the last price we happened to observe. So a run of tiny sub-threshold moves still produces an event once they add up past the threshold — slow drift can’t slip through unnoticed.
Because prices move often, treat this event as a prompt to re-fetch rather than a complete record of every change. Always read the live price from the API before relying on it.

Body

application/json

Delivered webhook body. Event metadata wraps the event-specific data, whose shape is determined by type.

id
string
required

Unique identifier for this event, matching the X-Headout-Event-Id header. Stable across retries of the same delivery — use it to deduplicate.

Example:

"evt_019fdcf4-e289-72f6-b324-488d531377ab"

type
enum<string>
required

The event type. Determines the shape of data.

Available options:
product.listing_status_changed,
product.variant_listing_status_changed,
product.data_changed,
product.variant_pricing_changed
createdAt
string<date-time>
required

When the event was created (ISO 8601, UTC). Stable across retries of the same event.

Example:

"2026-08-07T16:01:04.136037Z"

livemode
boolean
required

true for real production events; false for test/sandbox traffic.

data
product.listing_status_changed · object
required

The event-specific payload. Its shape is determined by type.

Response

Delivery successful. No retry.