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 withPOST /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
httpson port 443 with a publicly trusted certificate, and publicly reachable. Otherwise you get422 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}/testto 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 sendsPOST with a application/json; charset=utf-8 body containing one event, and these headers:
Envelope
data.objectis the full API resource, the same JSON itsGETendpoint returns, as it was when the event happened, not as it is when the request is sent.previous_attributesholds the old values of the fields that changed, on change events only.livemodeisfalsefor sandbox events.- Money-movement objects carry
response_codeandresponse_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
2xxwithin 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 inPanoFlow-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_atand the resource’s ownupdated_atorstatusto 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
failedand 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: sameid, same body, new signature. - Events are kept for 30 days and are readable at
GET /v1/eventsandGET /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.