Skip to main content
A payment keeps changing after the create answered. A processing charge is confirmed by the processor later, and a bank debit stays processing until it settles in the processor’s next batch; a succeeded one comes back as returned on a bank return or a chargeback days after; a void in the dashboard turns it canceled; a refund of it succeeds or fails. Rather than polling GET /payments/{id}, register a webhook endpoint and Payra posts each change to it. Endpoints can also be added, paused and deleted from the dashboard, in Settings → Integrations → Webhooks, which shows the delivery log and lets you resend any finished delivery to an endpoint that is still enabled.
1

Register an endpoint

Response
secret is in this response only. Store it; it is what you verify deliveries with, and it is never shown again. enabled_events is a list of types, or ["*"] for every type, present and future. The URL must be https on a publicly reachable host. A hostname that resolves to a private address is accepted when you save it, but every delivery to it fails with last_error: "host not allowed".
2

Receive the delivery

Payra POSTs the event to your URL:
Body
Answer any 2xx within 15 seconds. Redirects are not followed.
3

Verify the signature, then act

Recompute the signature over the raw body and the signed time, compare in constant time, and refuse a delivery more than five minutes old. A header you cannot parse is a forgery, not an error: answer 400 and move on. Then deduplicate on id before doing anything: a retry and a resend carry the same event.
To rotate a secret, register a new endpoint and delete the old one once the new one receives events.Check your signature step against this vector before going live: secret whsec_test_vector_secret_do_not_use (the whole value is the HMAC key, prefix included), t 1758315851, body {"object":"event","id":"evt_test_1"} → v1 is e5ca22ae17f9d57b866e45d25890fca17410ce3eb1ad6a7b5b565e2c54398c19. That t is long past, so verify() above refuses the vector for its age: test the HMAC without the five-minute check.

The event object

Event types

One per status a payment or a refund reaches, starting with the one the create answered, plus the two a checkout session reaches. A card charge approved synchronously emits payment.succeeded alone, and a refund confirmed synchronously emits refund.succeeded alone: the create’s own status is announced once, and there is no processing or pending event for a state nobody observed. A bank debit is born processing, so it emits payment.processing right after the create and payment.succeeded when it settles. For a refund.* event, data.object is the refund, and data.object.payment names the payment it belongs to. A ["*"] subscription receives these without being changed.
Only payments made through this API, and refunds of those payments, produce events; a refund made in the dashboard on an API payment is announced like one made through POST /refunds. Payments taken in the dashboard, the customer portal or a payment link have no pay_ and are not announced, and neither are their refunds.

Retries

A delivery that does not get a 2xx within 15 seconds — a timeout, a 4xx, a 5xx, a redirect — is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. After the eighth attempt the delivery is exhausted and stops. Deliveries are at least once and in no guaranteed order: two events for the same payment can arrive out of sequence, and the same event can arrive twice. Deduplicate on id, and read type rather than assuming the order. Deliveries to one workspace run in parallel, up to ten at a time; a slow endpoint queues your own deliveries, never another merchant’s. Answer fast and do the work after.

An endpoint that stops answering

Payra pauses an endpoint when, for three days, no delivery to it got a 2xx and nobody edited or resumed it, and at least one delivery created before those three days ran its whole schedule against it. Its status becomes disabled, deliveries still being retried are canceled, and no event is sent to it until you resume it. Every event stays in the delivery log. Whoever registered the endpoint in the dashboard is emailed; the Webhook Endpoint Paused rule in Settings → Internal Notifications adds recipients. An endpoint registered with an API key emails no one unless that rule is on, and it is off by default. Once your server answers again, set the endpoint back to enabled (POST /webhook-endpoints/{id} with { "status": "enabled" }, or Resume in the dashboard) and resend what it missed with POST /events/{id}/resend. Events filed while it was paused have no delivery to it, so the dashboard log offers no Resend for them: use the API.

The delivery log

GET /events lists every event your payments produced in this environment, newest first, whether or not an endpoint was listening at the time; type filters, and limit and starting_after page it like every other list. GET /events/{id} adds deliveries, oldest first: one entry per delivery of the event, with endpoint (the we_… it went to), origin (emit or resend), status (pending, succeeded, exhausted, canceled), attempt_count, created_at, last_attempt_at, next_attempt_at, last_response_status and last_error in Payra’s words (timeout, http 503, redirect).

Resending

POST /events/{id}/resend with { "endpoint": "we_…" } delivers the event to that endpoint again, with the same id and the same body. Nothing is charged and nothing about the payment changes; only the delivery is repeated. While an earlier delivery to that endpoint is still being retried, the resend is 409 conflict; a disabled endpoint is 400 parameter_invalid naming endpoint.

Managing endpoints

Up to 16 endpoints per workspace and environment (400 webhook_endpoint_limit_reached past that). POST /webhook-endpoints/{id} changes the url, enabled_events, description or status (enabled / disabled; a disabled endpoint keeps its secret and receives nothing, and disabling it cancels the deliveries to it still being retried). DELETE /webhook-endpoints/{id} stops it for good; its past deliveries stay readable on their events. GET and the list never include the secret.

Errors