Connect a fulfilment webhook
Beluga has no app ecosystem. What it has instead is a signed POST to a URL you own
whenever something happens, retried until you answer. This tutorial wires one up:
a receiver that gets order.paid, verifies it, and posts a pick list to a Slack
channel. Swap the last step for your warehouse, your ledger, or your printer.
You need a Beluga store that can take a test payment and somewhere to run twenty lines of Node that Beluga can reach over HTTPS. A tunnel is fine for development.
1. Write the receiver
Section titled “1. Write the receiver”The whole thing is in A webhook receiver. The parts that matter:
- It uses
express.raw, notexpress.json, on the webhook route. The signature is an HMAC over the raw bytes, and a body parser that has already turned the request into an object cannot reproduce them. - It rejects a timestamp more than five minutes old. The timestamp is inside the signed material so a replay is detectable.
- It answers
200before doing the work. Beluga waits ten seconds and then treats the delivery as failed. - It deduplicates on the event id. Delivery is at-least-once.
Run it, and expose it:
node receiver.jsngrok http 30002. Register the endpoint
Section titled “2. Register the endpoint”Admin → Webhooks → Add an endpoint. Paste the tunnel’s https:// URL with the
/hooks/beluga path, tick order.paid, save.
You are shown the signing secret exactly once. Put it in the receiver’s
environment as BELUGA_WEBHOOK_SECRET. Nothing reads it back afterwards; lose it
and you roll a new one, which invalidates the old one immediately.
The URL must be https:// and its hostname must resolve to a public address. A
tunnel satisfies both. For a receiver that genuinely lives on localhost, and only on
a machine nobody else can reach, WEBHOOK_ALLOW_INSECURE_TARGETS=true lifts both
rules.
3. Trigger it
Section titled “3. Trigger it”Complete a test payment. checkout.session.completed arrives at Beluga, the order
is marked paid, and an order.paid event is queued. Delivery is a background pass
every ten seconds, never inline, so a slow subscriber can never delay Beluga’s
answer to Stripe.
Your receiver logs the order. The Slack message says what to pick.
4. Read the delivery log
Section titled “4. Read the delivery log”The endpoint’s row in the admin expands into its recent deliveries: event type, attempt count, response status, the error if there was one. Redeliver puts one back at the front of the queue with its attempts reset, which is the fastest loop while you are debugging the handler.
Stop the receiver and pay again. The delivery fails, and retries after 1 minute, 5, 25, 2 hours, 10 hours, then gives up. Five give-ups in a row disable the endpoint; re-enabling clears the count and sends what is still queued.
5. Make it idempotent for real
Section titled “5. Make it idempotent for real”Resend a delivery with the receiver running. The same event id arrives twice. If your dedup store is in memory, that survives a restart badly; use the database you already have. Two subscribers to the same event receive the same id, so key the store per receiver.
What to build on this
Section titled “What to build on this”order.updatedfires on fulfilment status, carrier or tracking changes; a cancellation sendsorder.cancelledinstead, so you do not diff statuses.inventory.lowfires per variant, once per qualifying sale, at five units or fewer. It is not latched; filter onremainingfor a different threshold.product.publishedcarrieskind, so a fulfilment receiver can branch on physical versus digital before raising a pick list.
Build a receiver is the reference: every payload, verification in Node and Python, the truncation rule for very large orders, and a troubleshooting table.