Build a robust integration
The habits that keep a BillKit integration working as the API grows, a webhook arrives twice, or two events arrive in the wrong order.
BillKit’s v1 API only grows, under the compatibility contract. New fields,
event types and enum values arrive without notice, and webhook delivery is at least once and
unordered. None of that breaks an integration written with the rules on this page. Each one
names the failure it prevents, because each of these failures has happened to someone.
Read the API tolerantly
- Ignore fields you do not know. Parse responses into types that allow extra keys. A strict schema validator, or a deserialiser configured to reject unknown properties, turns the next additive release into an outage. The official SDKs already pass unknown fields through.
- Give every enum a default branch. A
switchonstatus,method, errorcodeorreasonneeds a case for a value you have never seen. Log it, and take the most cautious path: for a subscription status you do not recognise, that usually means “do not grant access you were not already granting, and do not revoke access either; ask a human”. - Branch on
code, then onreason, never onmessage. Messages are reworded freely. For an unknowncode, fall back on the HTTP status: a4xxis your request, a5xxor a network failure is retryable with the sameIdempotency-Key. Errors lists the codes. - Treat ids as opaque strings. Do not parse them, and do not size a database column to the ids you have seen so far. Only the prefix is stable.
Write a webhook handler that survives redelivery
A handler does four things, in this order.
- Verify the signature against the raw body, before parsing it. The SDK helpers do this.
If you write your own, accept the header when any
v1entry matches, and ignore entries with any other name: a rotated secret produces twov1entries, and a future scheme will add a new name besidev1rather than change it. Webhooks describes the scheme. - Return
2xxfor any event type you do not handle. New event types are added without notice. A4xxor5xxfor one you have not heard of makes BillKit retry it for days and marks it failed at the end, which buries real failures in your delivery log. - Record the event id, and stop if you have seen it. Delivery is at least once. Store the
evt_id with a unique constraint, in the same transaction as the work the event triggers, so a redelivery that races the first attempt fails the insert instead of doing the work twice. The id is in the body and in theBillKit-Event-Idheader. - Acknowledge, then work. Write the event to a queue or a table and return
200. The delivery times out after 10 seconds, and a timed-out delivery is retried even when your code finished the work.
Expect events out of order
Deliveries are independent and retried independently, so subscription.updated can arrive after
the subscription.canceled that followed it, and a retried delivery can arrive hours after a
later event. Two patterns handle this, and the second is simpler.
| Pattern | How |
|---|---|
| Compare timestamps | Store the created of the last event you applied to each object, and skip an event that is older. Event data is a snapshot from that moment, so applying an older one rolls your copy back. |
| Fetch the current state | Treat the event as a signal that something changed, and read the object from the API, for example GET /v1/subscriptions/{id}. The response is always current, whatever order the events came in. |
Fetching costs one read per event and removes the ordering problem entirely. It is the right default for anything that grants or removes access.
Plan for an outage on your side
BillKit retries a failed delivery for about three days, on the schedule in Webhooks, and never disables an endpoint for failing. A receiver that is down for an afternoon catches up on its own when it comes back.
After a longer outage, some deliveries will have run out of attempts. List the failed ones from
the delivery log and redeliver them, or page
through GET /v1/events until you reach the last event you
recorded. Both are safe to
replay into a handler that deduplicates by event id, which is the reason to build step 3 first.
Make your own writes retry-safe
Every POST that creates or charges something should carry an Idempotency-Key derived from your
own record, such as your order id, so a retry after a timeout cannot charge twice. The official
SDKs and the CLI generate one when you do not, and reuse it across their own retries; a key you
choose yourself also survives a restart of your process. Idempotency has the
rules.
Test the unhappy paths before you go live. Feed your handler the same signed event twice, then an older event after a newer one, and make it fail for a while before recovering: each run should leave your records exactly as one clean delivery would.
BillKit