Webhooks
Webhooks deliver payment, order, bridge, payout, and seller dispute updates from Peer Pay to your server.
Each delivery is a signed POST with a JSON body. The event family determines the snapshot
in data; see events and payloads.
Recommended flow:
- Create a webhook and store the signing secret returned once on creation.
- Subscribe to the events your fulfillment logic needs.
- Verify the signature and timestamp on every delivery. See verification and handler checklist.
- Store
X-Webhook-Idand skip deliveries you have already processed. - For order-family events, reconcile your merchant order record from
data.order.idanddata.payment.id. Do not cancel an order solely becausePAYMENT_EXPIREDarrived, and keep chargeback state in its own field.
Create a webhook
curl -X POST https://api.pay.peer.xyz/api/v1/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-d '{
"url": "https://yoursite.com/webhooks/zkp2p",
"events": ["PAYMENT_SETTLED", "PAYMENT_FAILED", "PAYMENT_EXPIRED", "ORDER_FULFILLED"]
}'
The response is 201 and carries the signing secret once:
{
"success": true,
"message": "Webhook created",
"responseObject": {
"webhook": { "id": "...", "url": "...", "events": ["..."], "active": true, "...": "..." },
"secret": "whsec_..."
},
"statusCode": 201
}
Store responseObject.secret immediately. It is never returned again; list and update
responses omit it.
Request fields:
| Field | Required | Description |
|---|---|---|
url | Yes | HTTPS endpoint that resolves to a public address |
events | Yes | One or more event names. The full list is on events and payloads |
customHeaders | No | Up to 10 { "key", "value" } pairs sent on every delivery, for example a shared bearer token |
- Plain HTTP, private, and loopback destinations are rejected with
400. - URLs with embedded credentials, such as
https://user:pass@example.com/hook, are rejected. - The hostnames
localhostandmetadata.google.internal, and any hostname ending in.internal,.local, or.localhost, are rejected whatever they resolve to. - Every delivery re-resolves and re-validates the host. A destination that stops resolving to a public address fails that delivery immediately, with no retries.
- Keys are trimmed, lower-cased, and must be valid HTTP header tokens.
- Values cannot contain CR, LF, or NUL.
- Duplicate keys are rejected.
- Reserved keys are rejected:
x-webhook-id,x-webhook-timestamp,x-webhook-signature,host,content-type,content-length,connection,expect,forwarded,proxy-authenticate,proxy-authorization,te,trailer,transfer-encoding,upgrade, and any key starting withproxy-orx-forwarded-.
Webhooks are registered per environment. A registration made with the sandbox key only receives sandbox deliveries, and one made with the live key only receives live deliveries, each with its own signing secret.
Subscriptions
Add DISPUTE_OPENED, DISPUTE_PAID and DISPUTE_ESCALATED to learn when a seller dispute on your order asks you to pay the seller.
Subscribe to ORDER_FULFILLED, PAYMENT_SETTLED, PAYMENT_FAILED, PAYMENT_EXPIRED,
ORDER_CANCELLED, and ORDER_RESIZED. Add PAYMENT_CHARGEBACKED,
ORDER_PARTIALLY_CHARGEBACKED, and ORDER_CHARGEBACKED if you accept Venmo or PayPal; Cash
App is non-chargebackable. Add the bridge events if your payout flow crosses chains.
If you create open-amount orders, add ORDER_AMOUNT_SET to learn the
amount the customer chose before they pay. Existing webhooks do not receive it until you add it.
ORDER_FULFILLED is the release signal. The others tell you why an order is not progressing,
or that its amount moved. The full event list, what each one carries, and the payload shapes
are on events and payloads.
Endpoint requirements
- Accept
POSTwith a JSON body. - Verify the signature before acting on the payload. See verification and handler checklist.
- Return 2xx within 30 seconds.
- Process asynchronously.
Retry policy
A delivery is attempted up to 7 times. Each attempt waits 30 seconds for a response; a non-2xx status, a timeout, or a connection error schedules the next attempt with growing delays:
| After attempt | Next attempt in |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 8 hours |
| 6 | 24 hours |
After the seventh failure the delivery is marked FAILED and not retried. Redirects count as
failures, so register the final URL. Every attempt carries the same X-Webhook-Id and a
fresh timestamp and signature.
Delivery statuses
| Status | Meaning |
|---|---|
PENDING | Awaiting delivery or retry |
DELIVERED | Delivered with a 2xx response |
FAILED | All 7 attempts exhausted, or delivery aborted because the webhook was inactive, its URL no longer resolves to a public HTTPS host, or it was a test delivery whose single attempt failed |
These are also listed under webhook delivery status.
Manage webhooks
Authenticate with X-API-Key, or with a dashboard session token in Authorization: Bearer,
but never both; sending both is rejected. With a dashboard session, creating, updating,
deleting, and testing webhooks needs the Owner or Manager role.
List
curl https://api.pay.peer.xyz/api/v1/webhooks \
-H "X-API-Key: your_api_key"
Update
curl -X PATCH https://api.pay.peer.xyz/api/v1/webhooks/{webhookId} \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-d '{"active": true}'
PATCH accepts any of url, active, events, and customHeaders.
Delete
curl -X DELETE https://api.pay.peer.xyz/api/v1/webhooks/{webhookId} \
-H "X-API-Key: your_api_key"
Test
curl -X POST https://api.pay.peer.xyz/api/v1/webhooks/{webhookId}/test \
-H "X-API-Key: your_api_key"
Sends one signed, synthetic ORDER_CREATED delivery with data.test set to true and every
snapshot null, so your handler can be exercised before a real order exists. The delivery is
not retried. responseObject carries success, deliveryId, responseCode, and error.
Limited to 5 calls per 10 minutes per merchant.
Deliveries
curl https://api.pay.peer.xyz/api/v1/webhooks/{webhookId}/deliveries \
-H "X-API-Key: your_api_key"
Returns the 50 most recent deliveries, newest first, as { deliveries, hasMore }. Each
delivery carries payload, status, attempts, lastAttemptAt, nextRetryAt, and
responseCode.