Skip to content

Best practices

A well-built webhook receiver withstands three things that will happen: the same delivery arriving twice, events arriving out of order, and your server being down for a while.

An attempt has 15 seconds. Past that, it counts as a failure and the event comes back later, even if your system finished the work.

So, in the receiver, do only the minimum:

  1. check the signature;
  2. store the event (in a queue, in a table);
  3. answer 200.

The real work (updating the ERP, sending an email, calling another system) runs later, outside the request.

The same delivery may arrive more than once: your response got lost on the network, went past 15 seconds, or someone resent it manually. In all these cases the webhook-id (and the body id) is the same.

Store the webhook-id of each event you processed and, before processing, check whether it is already there. If it is, answer 200 and do nothing. A uniqueness constraint in the database solves this without a race between two simultaneous receptions.

Events do not necessarily arrive in the order they happened. A delivery that failed comes back minutes or hours later, when others for the same record have already arrived; up to 5 deliveries to the same endpoint can be in flight at the same time; and two events from the same write have no order between them.

To decide which information is newest:

  • compare the body’s timestamp, which is when the event happened (not the header’s webhook-timestamp, which is the send time);
  • or compare the object’s updated_at with what you already have stored.

If the event that arrived is older than what you already have, ignore its data. A client.updated that arrives after a client.deleted for the same client, for example, is from before the deletion.

When object_truncated comes, fetch the object

Section titled “When object_truncated comes, fetch the object”

With data.object_truncated: true, the object came with only object and id: either because it went past 256 KB, or because the write came from a path that sends the event summarized (WhatsApp, automations, Open Finance, automatic routines). Fetch the object through the API, with the resource’s GET. It comes as it is now.

Fetching through the API is also the right approach when the object is large or changes a lot: treat the event as a “this record changed” notice and read the current state.

Some changes do not produce an event (see What does not send an event), a disabled endpoint does not keep what it missed, and your server may be down for longer than the 3 days of retries. For data that must be exact:

  • run a synchronization with updated_after every so often, which brings what changed since last time, in the order it changed. See Pagination;
  • after an outage on your side, use GET /public/v1/events to see what happened in the last 30 days, and manual resend for the deliveries that failed.
  • Check the signature on every delivery, and reject a timestamp more than 5 minutes off.
  • Keep the secret the way you keep a password: out of the code, out of the repository, out of the logs.
  • Do not return sensitive data in the response body: up to 2 KB of it stays in the delivery log, which the owner and admins read.
  • Rotate the secret when someone who knew it leaves, or when it went through a place you do not control. See Rotate the secret.