Webhook endpoints and deliveries
The full reference for webhooks. Endpoints and their modes, event types and filters, thin and fat payloads, burst windows, versions, limits, the delivery log, the retry schedule, automatic disabling, recovery, resending, secrets and the dashboard's controls.
A webhook endpoint (whe_…) is a URL FurniOps pushes events to as they happen, signed and
retried. This page is the reference; Your first webhook walks through
a first endpoint, and Handle webhooks securely covers verifying them.
Endpoints
Create one with POST /v1/webhook-endpoints
(webhooks:write: owners, admins and developers, or a key that holds it). The answer carries the
signing secret once: store it.
const endpoint = await furniops.webhookEndpoints.create({
url: "https://example.com/hooks/furniops",
description: "Price and stock sync",
event_types: ["offer.changed", "offer.created", "product.discontinued"],
payload: "thin",
});
// The signing secret is returned once, on creation (and on rotation). Store it as a secret.
console.log(endpoint.id, endpoint.secret.slice(0, 6));| Field | Meaning |
|---|---|
url | Where events are POSTed. HTTPS in live mode; a test-mode endpoint may use plain HTTP (a local tunnel). It must resolve to a public address, and redirects are not followed. |
event_types | The event types to send, up to 50, or ["*"] for every public type, present and future. |
filters | What narrows them (below). |
payload | thin (default) or fat (below). |
coalesce_seconds | A burst window, 0 to 300 seconds (below). |
api_version | The API version whose shape the events have. Defaults to the version of the request that creates it. See versions. |
description | Your note, up to 500 characters. |
PATCH /v1/webhook-endpoints/{id} changes any of
them, and status; send the ETag you read as If-Match.
DELETE stops every delivery and forgets the
secrets.
Each mode apart
An endpoint belongs to the mode of the key that created it: a test-mode endpoint receives only test events, a live one only live events. Each mode lists, changes and counts only its own endpoints.
Limits
- 20 endpoints per organisation in each mode.
- 50 event types per endpoint (or
*). - Up to 100 sources and 100 categories in
filters.
Filters
| Filter | Narrows |
|---|---|
filters.sources | Events about these sources' offers, and their source health events. Product and variant events are not narrowed by source. |
filters.categories | Events about products filed under these categories or below them, and their variants and offers. Source health events are not narrowed by category. |
Omit a filter for everything.
Payloads
thinsends what changed: the changed fields, by ID. Read the object back when you need all of it.fatalso sendsobject: the product, variant or offer as of the event's version, so you need no read back. Fat payloads are a plan feature (fat_webhooks); without it, asking for one answers403feature_unavailable, and an endpoint that is already fat is sent thin events.
Burst windows
With coalesce_seconds above 0, changes to one product, variant or offer that are still waiting to
be sent are combined into one event: each field's first old and last new, under the latest
version and event ID. Each delivery waits the window before it is sent. 0, the default, sends
every change as it happens. Use a window for a receiver that only needs the latest state of a busy
offer.
Deliveries
A delivery is one event for one endpoint. Every delivery and every attempt is kept for 30 days (fewer when your plan keeps fewer delivery days).
GET /v1/webhook-endpoints/{id}/deliveries
lists them newest first, filtered by status, event type or one event.
GET …/deliveries/{delivery} shows the body exactly
as sent and every attempt: when, the status, how long it took, why it failed without an answer
(a timeout, a refused address, TLS), and the first 1 KiB of your answer.
status | Meaning |
|---|---|
pending | Waiting for its next attempt, or for an earlier event of its subject. |
sending | Being sent. |
succeeded | Your endpoint answered 2xx. |
failed | No more attempts: the schedule ran out, your endpoint asked to stop, or it was disabled. |
Retries
- Only a
2xxanswer within 15 seconds is a success. Redirects are not followed. - A failed attempt is retried: at once, then after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h, 14 h, 14 h and 14 h. That is eleven attempts over about 2.9 days.
429,502and504answers that sendRetry-Afterare retried when it says.- A
webhook-delivery: abort-messageheader in your answer stops retrying that one event. - Events of one subject (one product, variant or offer) arrive in order. A slow subject never holds up the others, and a slow endpoint never slows anyone else's.
Automatic disabling
FurniOps disables an endpoint that:
- answers
410 Gone(at once), or - has had no successful delivery for 72 hours.
status becomes disabled and status_reason.code says why: gone or failing. A
delivery.disabled event goes to your other endpoints that listen for it, so you hear about it.
While an endpoint is disabled, no events are stored for it.
Recover after an outage
- Fix the receiver.
- Set the endpoint's
statusback toenabled. - Send the failed deliveries of the outage again with
POST /v1/webhook-endpoints/{id}/recoverand a time range (since, anduntil, now when omitted). They go oldest first, each with a fresh schedule, and the order per subject is kept.
// Once a broken receiver is fixed: send every failed delivery of the range again, oldest first.
const recovery = await furniops.webhookEndpoints.recoverDeliveries(endpoint.id, {
since: "2026-10-03T00:00:00Z",
});
console.log(`${recovery.requeued} deliveries queued again`);Events never stored while the endpoint was disabled are not recovered: read them from the change feed, which keeps 30 days.
Resend one delivery
POST …/deliveries/{delivery}/resend sends a
finished delivery (failed or succeeded) again at once, with the same webhook-id, so a receiver
that already has it can ignore it. A delivery still pending or being sent is answered as it is.
The endpoint must be enabled, or it answers 409.
Test an endpoint
POST /v1/webhook-endpoints/{id}/test sends a
webhook.test event now, signed like any other, whatever the endpoint subscribes to. In test mode,
POST /v1/test/events emits a sample of a real event type
to every test endpoint that wants it. To receive events on your machine, use
furniops listen.
Secrets
Every delivery is signed to the Standard Webhooks spec with the endpoint's secret.
POST /v1/webhook-endpoints/{id}/rotate-secret
makes a new one, shown once; the current one keeps signing beside it for overlap_hours (0 to 72,
24 by default), so deliveries carry both signatures until your receiver has switched. The endpoint
lists its secrets by their last characters only. See
Rotate a secret without downtime.
In the dashboard
Developers › Webhooks lists the endpoints of the mode you are in, with what each listens to, its last 24 hours of activity, error rate and response time. Opening one shows its details, its signing secrets and its latest deliveries, with these actions:
| Action | Does |
|---|---|
| Send test event | Sends webhook.test. |
| Rotate secret | Makes a new secret; the old one signs for 24 more hours. |
| Change payload | Switches between changed fields (thin) and full objects (fat). |
| Retry failed | Sends again the failed deliveries of the last 24 hours, 3 days, 7 days or 30 days. |
| Resend | Sends one finished delivery again. |
Creating, editing and deleting endpoints is done through the API or the SDK.
React to price and stock changes
Use offer.changed to update prices and stock within seconds, tell price moves from stock moves, and check stock at the moment that matters.
Events reference
Every event FurniOps publishes, generated from the API's own contract. What each one means, which channels deliver it (webhooks, the change feed, the stream), whether test mode can emit a sample, and its payload.