Skip to main content
Consuming webhooks correctly keeps your systems in sync with Pocketsflow without losing events or doing work twice. These patterns apply regardless of your stack.

Core responsibilities

Your endpoint should:
  1. Receive the HTTP POST from Pocketsflow.
  2. Verify the X-Pocketsflow-Signature-V2 against the raw body, within a 5-minute tolerance (see Authentication & security).
  3. Deduplicate on the event id (X-Pocketsflow-Event-Id, also the body’s id).
  4. Acknowledge with a 2xx immediately.
  5. Parse the JSON and route on the X-Pocketsflow-Event header (or the body’s type).
  6. Process the event idempotently in the background.

Delivery guarantees

Each attempt has a 5-second timeout. If your endpoint answers 5xx or 429, or can’t be reached at all (connection refused, DNS failure), Pocketsflow retries up to 3 attempts in total, about 1 second and then 4 seconds apart. A timeout or reset is not retried — your endpoint may already have processed the event. 307/308 redirects are followed (up to 3 hops, each re-checked); other answers — 2xx (success), other 3xx, or any other 4xx — are final. Every attempt is recorded in the delivery log (GET /delivered-webhooks), and you can re-send any logged delivery with POST /delivered-webhooks/{id}/redeliver. Retries are best-effort, not a durable queue: design for an event occasionally arriving more than once — or not at all.
Because of this, two habits matter most:
  • Acknowledge fast. Verify the signature, enqueue the event, and return 200 — all well under 5 seconds. Never do slow work (emails, third-party calls, heavy DB writes) before responding.
  • Reconcile. Periodically pull the source of truth from the API so a missed delivery self-heals:

Idempotency

Handlers must be safe to run more than once for the same logical event. The first line of defense is the event id: a retry or a redelivery of an event carries the same X-Pocketsflow-Event-Id (and body id) as the original, so record each id you process and skip ones you’ve seen. The event id is per delivery of an event, though — reconciliation via the API or two separate events about the same object won’t share it. For business-level idempotency, also key on a stable value from the payload: Before processing, check whether you’ve already handled that key; if so, skip. If not, record it and proceed. This protects you from re-processing during reconciliation and from any duplicate delivery.
Do not dedupe customer.subscription.updated on the customer id. It is a state-change signal and fires repeatedly over a subscription’s life — past due, recovery, cancel-scheduled, pause, resume, cancellation. Keying it on subscription.customerId alone would make you drop every change after the first. Treat it as a state sync instead: upsert from the payload’s subscriptionCustomer.status / cancelAtPeriodEnd / paused fields, which are safe to apply more than once. The same applies to invoice.upcoming — key it on the billing period (renewalAt), not the customer.

Ordering

Events are not guaranteed to arrive in the order they occurred. Don’t assume, for example, that payment_intent.succeeded always lands before the first invoice.payment_succeeded. Make each handler tolerant of out-of-order arrival (upsert state; use the payload’s own fields and the API rather than relying on sequence).

Delivery log and redelivery

Every attempt is logged with its HTTP status, durationMs, a short error (for example Timed out, Connection refused, HTTP 503, Blocked destination), the attempt number, the eventId, and the exact body that was sent. Logs are kept for 30 days.
  • GET /delivered-webhooks?page=1&pageSize=50&webhookId=…&event=order.completed lists attempts, newest first (pageSize max 100), with a pagination block.
  • GET /delivered-webhooks/{id} returns one attempt.
  • POST /delivered-webhooks/{id}/redeliver re-sends that attempt’s body to its endpoint with fresh signature headers (same event id) and logs the result as a new attempt.
All three accept an API key or a dashboard session.

Error handling

Plan for invalid payloads, downstream outages, and transient connectivity issues:
  • Log every delivery with enough context to debug (event type, event id, idempotency key, timestamp, and webhookId).
  • Return 2xx only after you’ve safely recorded the event for processing — not after the full downstream work completes.
  • Alert when your handler’s error rate crosses a threshold.
1

Read the raw body

Capture raw bytes before parsing so signature verification is exact.
2

Verify the signature

Check the V2 timestamp is within 5 minutes, recompute the HMAC-SHA256 and compare in constant time. Reject on mismatch.
3

Enqueue and acknowledge

Persist the raw event keyed by its event id (ignore an id you already have) and return 200.
4

Process asynchronously

A worker routes on event.type, skips already-processed keys, performs the action, and marks the key done.
For concrete language examples, see Webhook examples.