Skip to main content
PanoFlow sends an HTTPS POST to your webhook endpoint when something happens in a business, such as a payment settling or money arriving. Each request carries one signed event. Verify the signature, answer with a 2xx quickly, and process the event idempotently.

Set up an endpoint

Add endpoints in the web app (Developers → Webhooks) or with POST /v1/webhooks, choosing the event types to send. The response includes the endpoint’s signing secret (whsec_...), shown once when the endpoint is created or its secret is rolled.
  • The URL must be https on port 443 with a publicly trusted certificate, and publicly reachable. Otherwise you get 422 webhook_url_invalid.
  • Endpoints belong to one mode. Sandbox (pk_test_) and live endpoints are separate and have separate signing secrets.
  • Send a test event with POST /v1/webhooks/{webhook_id}/test to check your endpoint.

Event types

The catalogue is also available from GET /v1/event-types. New event types can be added; ignore types you don’t handle.

The request

PanoFlow sends POST with a application/json; charset=utf-8 body containing one event, and these headers:

Envelope

  • data.object is the full API resource, the same JSON its GET endpoint returns, as it was when the event happened, not as it is when the request is sent.
  • previous_attributes holds the old values of the fields that changed, on change events only.
  • livemode is false for sandbox events.
  • Money-movement objects carry response_code and response_message (see Payment statuses). Invoices, businesses and accounts carry none.

Verify the signature

Every request is signed with your endpoint’s signing secret:
1

Parse the header

Split the header on ,, then split each part on the first =. t is the Unix time in seconds when this attempt was signed. Each v1 is a lowercase hex HMAC-SHA256. There can be two v1 values, one per active secret, while a rolled secret overlaps. Ignore schemes you don’t know, such as a future v2.
2

Build the signed payload

Concatenate the decimal t, a literal ., and the raw request body bytes exactly as received. Don’t parse and re-serialise the JSON first: whitespace and key order matter.
3

Compute the HMAC

Compute HMAC-SHA256 with the secret string’s UTF-8 bytes as the key, including the whsec_ prefix, over the signed payload. Hex-encode it in lowercase.
4

Compare

Compare your value with each v1 using a constant-time comparison. Accept if any matches.
5

Check the timestamp

Reject the request if t is more than 300 seconds (5 minutes) away from your current time, even if the HMAC matches. This stops replays of a captured request. Keep your server clock in sync with NTP.

Test your verification

Check your code against this reproducible example before you go live: Pass now = 1793176920 when you test, or the timestamp check rejects it.

Respond

  • Answer with any 2xx within 10 seconds. The response body is ignored. Do slow work after you answer.
  • Anything else is a failed attempt: a non-2xx status, a redirect, a TLS error, a timeout or a refused connection. Redirects are not followed.

Delivery

  • At least once. The same event can arrive more than once, through retries, replays or rare duplicates. Deduplicate on the event id (also in PanoFlow-Event-Id) and make your handler idempotent.
  • Ordered per endpoint. Events for one endpoint are sent one at a time in the order they were created. While an event is being retried, later events for that endpoint wait behind it. Replays are the exception: a replayed event is delivered when it is requested, out of order. Use created_at and the resource’s own updated_at or status to ignore stale updates.
  • Retries. After a failed attempt, PanoFlow retries 5 minutes, 30 minutes and 2 hours after the first attempt, then doubles the gap, up to 12 hours between attempts, until the retry window ends. After that the delivery is marked failed and the business’s developers are emailed.
  • Failing endpoints. Three failed deliveries in a row flag the endpoint as failing and send a “webhook failing” email. A failing endpoint keeps receiving retries.
  • Re-signing. Every retry and replay is signed again with a fresh t. The body bytes are identical across attempts.

Replay and inspect

  • Resend any event, delivered or failed, from the web app or with POST /v1/events/{event_id}/replay. A replay is a new delivery of the same event: same id, same body, new signature.
  • Events are kept for 30 days and are readable at GET /v1/events and GET /v1/events/{event_id}.
  • Each delivery attempt is kept for 90 days with its response code, latency and the first 4 KB of your response body. List them with GET /v1/webhooks/{webhook_id}/deliveries.