Skip to main content

Webhook best practices

The following guide describes some best practices for working with webhooks.

Verify every request​

Your endpoint is a publicly reachable URL that accepts unauthenticated POST requests, so anyone who finds it can send it anything. The X-Kavitro-Signature header is what separates a real delivery from a forged one.

Check it before you do anything else, and reject the request if it does not match. See verifying requests for how, including the two details that most often make a correct-looking implementation fail.

Respond quickly​

After receiving a webhook, respond with a 2xx status code as quickly as you can. You have 3 seconds: if no 2xx arrives in that window the delivery counts as failed and goes into the retry schedule, even if your handler eventually finishes its work.

A common pattern is to store the payload in a queue and return immediately, letting a background worker do the real processing. Acknowledge first, work afterwards.

Speed matters beyond the timeout, too. Kavitro delivers to each endpoint one event at a time, so an endpoint that takes a second to answer receives at most one event a second, and a large import reaches it that much later.

Subscribe only to what you handle​

By default an endpoint receives every event the account produces. Choose the events it needs instead, per resource or per action, so the rest are never sent to it. That saves your endpoint the work of acknowledging them, and it keeps bursts of irrelevant changes from delaying the events you care about, since an endpoint receives one event at a time.

Remember to extend the subscriptions when your integration starts handling a new event type: events you are not subscribed to are not stored anywhere for you to collect later.

Acknowledge what you ignore​

Even a narrowed endpoint can receive something it does not handle: an endpoint that has been failing for days gets a system.ping whatever it is subscribed to, and one left on every event receives types you may never have seen. Answer all of them with a 2xx, including the types you do not handle.

An error for an unfamiliar type counts as a failed delivery. Enough of them in a row pause the endpoint, and three days of failures switch it off. Rejecting system.ping is the costly case. On a quiet account the ping can be the only request that shows a recovered endpoint is working again, so rejecting it can get a working endpoint switched off.

Fetch the record, don't trust the payload​

An event carries only data.object.id and a type. It tells you what changed, never what it changed to.

Query the API for the record when you handle the event. Besides being the only way to get field values, it means you act on the record's current state rather than on whatever was true when the event fired, which matters more than it sounds - see the next two sections.

Ignore duplicates​

Delivery is at least once, so the same event can arrive more than once. Handlers should be idempotent: processing an event twice should leave the same result as processing it once.

The id on each event is stable across retries, so recording the ones you have seen is enough:

on event:
if store.has(event.id):
return 200 # already handled, nothing to do
store.add(event.id)
process(event)

Record the ID only after the work succeeds, or in the same transaction as it, so a crash midway does not leave you having skipped an event you never actually processed.

Do not rely on the order​

Events are not guaranteed to arrive in the order they occurred, and retries make reordering more likely: a delivery that failed once can land minutes after events that came later.

Every event carries an occurredAt timestamp. Use it rather than arrival order when the sequence matters, and discard an event that is older than the state you already hold.

Monitor your deliveries​

Check the webhook's delivery health periodically rather than waiting for someone to notice missing data. A failingSince means your endpoint is failing, a future circuitOpenUntil that deliveries are paused, and pendingDeliveries how many events are waiting. The warning email only reaches the account owner, and only after two days of failures, so do not rely on it as your alert.

Every attempt is also recorded in the delivery log. Query it for non-2xx responses to see what your endpoint answered:

logs(first: 25, where: { column: STATUS_CODE, operator: GTE, value: 400 }) {
edges {
node {
type
attempt
statusCode
message
createdAt
}
}
}

A rising attempt on recent rows means deliveries are being retried, which is the earliest signal that something on your side is failing.

Implement reconciliation jobs​

Your app shouldn't rely solely on receiving data from webhooks. Delivery is not guaranteed: an endpoint that stays down for three days is switched off and the events waiting for it are discarded, and some changes never raise an event at all - editing a rich-text description is one, as noted under event types.

Run a periodic job that queries Kavitro for records changed since your last sync, and run it once more after switching a webhook back on, to pick up what it missed while it was off. Webhooks make you fast; reconciliation makes you correct. You want both.