Create a webhook endpoint
1
Choose the events you need
Subscribe to exact event names, such as
payment.completed, or use a trailing wildcard to subscribe to a category, such as payment.*, refund.*, or settlement.*. A wildcard is supported only at the end of a filter.2
Create the webhook
Call
POST /v1/aggregators/{{aggregator_id}}/webhooks with your URL, event filters, and whether the endpoint is enabled.3
Store the webhook secret
The response contains a base64-encoded signing secret. Store it in a secret manager and base64-decode it before using the resulting bytes as the HMAC key.
Treat every
secret field as sensitive signing material. Do not expose it in client-side code, logs, support tickets, or source control.HTTP request contract
Safepay sends an HTTPPOST with Content-Type: application/json. Header names are case-insensitive according to HTTP, but Safepay emits the following names:
The JSON body is the raw event envelope. The values in
X-SFPY-EVENT-ID, X-SFPY-EVENT-TYPE, and X-SFPY-AGGREGATOR-ID correspond to the body’s identifier, webhook_event_type, and aggregator_id fields.
Verify the signature
Safepay signs each individual delivery attempt. The signed message is the exactX-SFPY-TIMESTAMP value, a literal period (.), and the raw HTTP request body bytes:
1
Read the request without changing it
Capture
X-SFPY-SIGNATURE, X-SFPY-TIMESTAMP, and the raw request body. Do not parse or re-serialize the body before verification.2
Check timestamp freshness
Parse the timestamp as RFC 3339 with optional fractional seconds. Reject timestamps outside a tolerance appropriate for your environment; five minutes is a reasonable starting point. Safepay does not enforce this tolerance on your behalf.
3
Compute the expected signature
Base64-decode the stored webhook secret, calculate HMAC-SHA256 over
timestamp + '.' + raw_body, then prefix the lowercase hexadecimal digest with sha256=.4
Compare in constant time
Compare the expected signature with
X-SFPY-SIGNATURE using a constant-time comparison. Parse the JSON only after verification succeeds.Whitespace and property order are part of the signed bytes. Parsing, prettifying, or re-serializing the body before verification will invalidate the signature.
Acknowledge and process safely
Return any2xx response after you have durably stored the event. Safepay treats the delivery as successful and does not retry it. A 200 OK response is recommended.
Do not wait for downstream business logic before acknowledging. A safe handler performs these operations in order:
- Read the raw body and required headers.
- Verify the timestamp and signature.
- Atomically persist the event using
X-SFPY-EVENT-IDas a unique key. - Return
200 OK. - Process the stored event asynchronously.
2xx promptly and do not depend on a long-running connection remaining open.
Delivery and retry semantics
Webhook delivery is at least once. Your endpoint can receive the same event more than once, for example when Safepay sends a request successfully but does not receive your acknowledgement. Safepay does not guarantee that separate events arrive in creation order, especially when one event is being retried. Always make processing idempotent and compare the resource’s current state before applying a transition. Safepay makes one initial delivery attempt. Transport errors and5xx responses are retried up to five more times using exponential backoff:
Any
2xx response completes the delivery. A terminal non-2xx response below 500, including a 4xx, is not retried and moves the delivery to WD_DEAD. After the final failed retry, the delivery also moves to WD_DEAD.
Automatic redirects should not be part of your integration contract. Register the final HTTPS URL and return a
2xx response directly from it.Delivery statuses
Use the List Webhook Delivery endpoint to filter by status, event type, event ID, or delivery ID. Use Find Webhook Delivery to inspect the stored payload and last error.
There is currently no public API for manually replaying a
WD_DEAD delivery. Reconcile the resource through the corresponding Payments, Refunds, or Settlement API and process it idempotently in your system.
Rotate the signing secret
CallPUT /v1/aggregators/{{aggregator_id}}/webhooks/{{webhook_id}}/rotate to generate a new base64-encoded secret. Rotation atomically replaces the stored secret; Safepay does not maintain a server-side overlap period with both secrets.
New delivery attempts, including retries of older events, load the current secret and are signed with it. A request already in progress at the moment rotation completes may have been signed with the previous secret. To rotate without rejecting an in-flight request:
- Retain the previous secret in your verifier.
- Call the rotation endpoint and securely store the returned secret.
- Try the new secret first and the previous secret second for a short operational grace period.
- Remove the previous secret after in-flight requests have cleared.
X-SFPY-TIMESTAMP and signature. The webhook request does not include a secret-version header.
Endpoint changes
- Disabling an endpoint causes pending deliveries selected afterward to become
WD_DEAD. - Deleting an endpoint also deletes its stored delivery records.
- Updating event filters affects future events; it does not create deliveries for past events.
Event catalog
Payment events
Payouts use the
payment.* event family because payouts are represented as payments with subject.payment.type set to PAYOUT or SETTLEMENT_PAYOUT. There is no separate payout.* event family.
Refund events
Payment-level
payment.refunded and payment.refund_partial events describe the resulting payment state. Refund-level refund.* events describe the lifecycle of an individual refund. Use subject.payment.refunds to identify the refund concerned.