BillKit/DocsConsole
Operations guide

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.

13 Oct 2026·Intermediate·8 min read

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 switch on status, method, error code or reason needs 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 on reason, never on message. Messages are reworded freely. For an unknown code, fall back on the HTTP status: a 4xx is your request, a 5xx or a network failure is retryable with the same Idempotency-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.

  1. 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 v1 entry matches, and ignore entries with any other name: a rotated secret produces two v1 entries, and a future scheme will add a new name beside v1 rather than change it. Webhooks describes the scheme.
  2. Return 2xx for any event type you do not handle. New event types are added without notice. A 4xx or 5xx for 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.
  3. 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 the BillKit-Event-Id header.
  4. 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.

PatternHow
Compare timestampsStore 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 stateTreat 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.