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.
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
Every attempt is recorded in the delivery log. Query it for non-2xx responses periodically rather than waiting for someone to notice missing data:
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 long enough will have events dropped after the retry schedule is exhausted, 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. Webhooks make you fast; reconciliation makes you correct. You want both.