> ## Documentation Index
> Fetch the complete documentation index at: https://docs.interchange.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Register a webhook to receive push events instead of polling

Webhooks push events to a URL you control instead of you polling for them — the server-to-server equivalent of the [discovery event stream](/v2/buyer/discovery/tasks/events) for callers that run batch jobs or don't hold a connection open. Register a subscription once; every matching event after that is delivered as an HTTP `POST` to your endpoint, HMAC-signed so you can verify it came from us.

## Register a subscription

`POST /api/v2/buyer/webhook-subscriptions`

```bash curl theme={null}
curl -X POST "https://api.interchange.io/api/v2/buyer/webhook-subscriptions" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/scope3",
    "secret": "a-secret-at-least-16-characters-long",
    "eventTypes": ["discovery.revision"]
  }'
```

| Field        | Type   | Required | Notes                                                                                 |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------- |
| `url`        | string | Yes      | HTTPS endpoint we `POST` events to                                                    |
| `secret`     | string | Yes      | 16–256 characters. Used to compute the `X-Webhook-Signature` header — keep it private |
| `eventTypes` | array  | Yes      | One or more of the [event catalog](#event-catalog) below                              |

Response (`201`):

```json theme={null}
{
  "data": {
    "subscription": {
      "id": "wh_1751000000000_a1b2c3d4",
      "url": "https://example.com/webhooks/scope3",
      "eventTypes": ["discovery.revision"],
      "status": "active",
      "failureCount": 0,
      "lastSuccess": null,
      "lastFailure": null,
      "createdAt": "2026-07-06T00:00:00.000Z",
      "updatedAt": "2026-07-06T00:00:00.000Z"
    }
  }
}
```

The response never echoes your `secret` back — store it when you register.

## List subscriptions

`GET /api/v2/buyer/webhook-subscriptions`

Returns every subscription for your account, newest first. Same subscription shape as above, minus the secret.

## Delete a subscription

`DELETE /api/v2/buyer/webhook-subscriptions/{id}`

Deliveries stop immediately. Returns `404` if the id doesn't exist or belongs to another account.

## Verifying deliveries

Every delivery carries an `X-Webhook-Signature` header: the hex-encoded HMAC-SHA256 of the exact request body, keyed with your subscription's `secret`. Recompute it and compare before trusting the payload:

```javascript theme={null}
import crypto from 'node:crypto'

function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
}
```

Compute the HMAC over the raw request body, not a re-serialized copy — key order or whitespace differences will change the signature.

## Retry semantics

A delivery is a single `POST` attempt per event. If your endpoint doesn't respond with a `2xx` status, the attempt counts as a failure — it is **not** retried inline, since push events like `discovery.revision` are time-sensitive and a delayed retry would deliver stale state. Instead:

* Each failure increments `failureCount` on the subscription.
* After 10 consecutive failures, the subscription's `status` flips to `failed` and delivery stops. List your subscriptions to check `status`, `failureCount`, and `lastFailure`, then delete and re-register once your endpoint is healthy again.
* A successful delivery resets `failureCount` to zero.

Because delivery isn't retried, treat webhooks as an optimization, not your only source of truth: an endpoint that's been down can catch up by polling the equivalent REST resource (for `discovery.revision`, that's [Browse products](/v2/buyer/discovery/tasks/browse-products) with `sinceRevision`).

## Event catalog

| Event                | Fires when                                                                                                                                                                                                                          | Payload                                                                                                        |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `discovery.revision` | A [progressive discovery](/v2/buyer/discovery/tasks/discover-products#progressive-delivery) session's snapshot advances — the same moment the [SSE stream](/v2/buyer/discovery/tasks/events) emits a `revision` or `complete` frame | `{ event, discoveryId, revision, resultsComplete, productGroups, pendingAgents, incompleteAgents, timestamp }` |

`discovery.revision` is one payload contract delivered over two transports: hold a connection open and use the [SSE stream](/v2/buyer/discovery/tasks/events), or register a webhook if you'd rather not. The payload shape is identical either way — `productGroups` contains only groups that landed since the previous revision (replace-by-`groupId` semantics), `resultsComplete: true` marks the final event for that discovery session.

```json theme={null}
{
  "event": "discovery.revision",
  "discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
  "revision": 2,
  "resultsComplete": false,
  "productGroups": [
    { "groupId": "ctx_disc-group-2", "groupName": "CTV Network", "products": ["..."] }
  ],
  "pendingAgents": [
    { "agentId": "agent_slow_media", "agentName": "Slow Media Storefront" }
  ],
  "timestamp": "2026-07-06T00:00:12.500Z"
}
```

Only subscriptions active *before* a discovery session starts receive that session's events — there's no replay of revisions that landed before you registered.

## Errors

* `401 UNAUTHORIZED` — missing or invalid auth.
* `400 VALIDATION_ERROR` — invalid `url`, `secret` shorter than 16 characters, or empty `eventTypes`.
* `404 NOT_FOUND` — (delete only) no subscription with this id for your account.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Stream discovery events" href="/v2/buyer/discovery/tasks/events" icon="tower-broadcast">
    The SSE-equivalent of discovery.revision for a held-open connection
  </Card>

  <Card title="Browse products" href="/v2/buyer/discovery/tasks/browse-products" icon="layer-group">
    Poll for the same revisions if you'd rather not use push at all
  </Card>
</CardGroup>
