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 Answer any
POSTs the event to your URL:Body
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 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
400 and move on. Then deduplicate on id before doing anything: a retry
and a resend carry the same event.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 perstatus 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.
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 a2xx 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 a2xx 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.