Security
Signature or token
Both are set on the service side, independently of each other, and can be required together: a delivery must then satisfy both. With neither, the receiver accepts any delivery.
- The signature (
secret): the service sends an HMAC SHA-256 digest of the body inx-lfp-signature-256. The secret never travels, and the signature proves the body was not changed on the way. It is the choice to make as soon as the delivery crosses the network. - The token (
token): expected inAuthorization: Bearer <token>. Simpler, but it travels with every request: keep it for a local or encrypted link.
createReceiver({
secret: process.env.LFP_WEBHOOKS_SECRET,
token: process.env.LFP_WEBHOOKS_TOKEN,
})
Both comparisons run in constant time. A rejected delivery gets 401, an unreadable body 400.
The signature covers the bytes received: the body must reach the receiver raw, never parsed then re-serialized (see Express and Fastify).
Answering before processing
The service gives up on a delivery after ten seconds and retries it. The receiver therefore answers 200 as soon as the delivery is verified, without waiting for your handlers, which run afterwards.
Duplicates
A retried delivery keeps the same x-lfp-delivery ID. The receiver remembers the last 512 (dedupeSize): a delivery already seen gets an acknowledgment, without calling the handlers again. dedupeSize: 0 disables this filter.
That memory lives in the process: after a restart, or with several instances behind a load balancer, use context.id as an idempotency key in your own storage if a duplicate is a problem.
Order
Handlers run in a queue, one after another and in the order deliveries arrive, even when they are asynchronous. An error in a handler goes to onError and does not stop the queue.
createReceiver({
secret: process.env.LFP_WEBHOOKS_SECRET,
onError: (error, context) => logger.error({ error, event: context.event, id: context.id }),
})