Skip to main content
Every webhook request body is a JSON event envelope. Route the event using X-SFPY-EVENT-TYPE or webhook_event_type, deduplicate it using X-SFPY-EVENT-ID or identifier, and then inspect subject.payment for the affected resource.

Event envelope

JSON property names use snake_case (the protobuf field names). Protobuf int64 values, including payment amounts and internal numeric IDs, are serialized as JSON strings. Timestamps use RFC 3339 and enum values use their string names.
Treat payload evolution as additive. Ignore fields and enum values you do not recognize, and use webhook_event_type rather than relying only on kind or code.

Complete payment.completed example

The following is a representative complete body generated by the current V1_0_0 serializer. Empty strings, empty arrays, and null nested resources are included because the serializer emits unpopulated fields.
txn_logs is the full status history for the payment, oldest first; the entry matching the current event’s code/status is the last one in the array, and its token matches the envelope’s identifier. payment_parties holds the debitor (payer) and creditor (payee) legs, each with a nested bank_account. bank_account.iban is always empty — Safepay never returns a raw IBAN — and redacted_iban/hashed_iban are the only account-number representations exposed. bank_account.vault_token references the underlying account in the vault service; treat it as a sensitive credential-like value, since it can be used to retrieve full account details from the vault API. qr_code, aggregator_merchant, refunds, settlement, payment_authorization, and aggregator are also part of the same snapshot and populate depending on the payment’s type and lifecycle stage. Persist the entire event, but select only the fields your integration needs.

Refund event example

Refund lifecycle events use kind: REFUND_EVENT and include the payment plus its refund records. This focused example omits unrelated empty payment fields for readability; the delivered body uses the same complete envelope shown above.
Payment-level events payment.refunded and payment.refund_partial can describe the resulting aggregate payment status. Events in the refund.* family describe the individual refund lifecycle. Make refund processing idempotent using the event identifier and identify the refund by subject.payment.refunds[].token.

Payout event example

Payouts use the payment.* event family. Identify a payout through subject.payment.type; there are no payout.* webhook names.

Failed events

For payment.failed, payment.rejected, or refund.failed, use the resource snapshot and its transaction logs to determine the latest known state. Do not confuse a business failure inside the event with last_error from the Webhook Delivery API: last_error describes Safepay’s HTTP delivery attempt, such as status 422, not why the payment or refund failed.

Match headers to the body

Before processing, verify that the routing headers agree with the signed body: The HMAC covers the body, not these routing headers individually. Verify the body signature first, then reject the request if a routing header does not match its signed body field or your expected aggregator and subscription.

See also