Webhooks
We send a signed POST request to your endpoint whenever a payment or payout changes state. Webhooks are how you should learn that a payment is completed.
Setting up an endpoint
Add endpoints in Developers → Webhooks. Each endpoint belongs to one mode, has its own signing secret and can subscribe to all events or a chosen list. Live endpoints must use HTTPS, and every endpoint must resolve to a public internet address.
Events
| Payments | Payouts |
|---|---|
payment.createdpayment.pendingpayment.processingpayment.completedpayment.underpaidpayment.failedpayment.expiredpayment.cancelledpayment.on_holdpayment.refunded |
payout.requestedpayout.approvedpayout.processingpayout.completedpayout.failedpayout.rejectedpayout.cancelled |
The Send test event button next to each endpoint in the dashboard sends a webhook.ping event.
Request format
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-Gateway-Event: payment.completed
X-Gateway-Event-Id: evt_01j9x6...
X-Gateway-Timestamp: 1760616000
X-Gateway-Signature: t=1760616000,v1=5f8c2b...
{
"id": "evt_01j9x6...",
"type": "payment.completed",
"mode": "live",
"created_at": "2026-10-16T12:00:00+00:00",
"data": { "id": "pay_01j9x4...", "object": "payment", "status": "completed", ... }
}data is the full payment or payout object, in the same format the API returns.
Verifying the signature
- Read the raw request body exactly as received, before any JSON parsing.
- Split
X-Gateway-Signatureon,to gett(Unix timestamp) andv1(signature). - Compute
HMAC-SHA256(secret, t + "." + raw_body)as lowercase hex. - Compare it with
v1using a constant-time comparison. - Reject the request if
tis more than 5 minutes from your current time. This blocks replayed requests.
// PHP
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts); // ['t' => ..., 'v1' => ...]
$expected = hash_hmac('sha256', $parts['t'] . '.' . $payload, getenv('GATEWAY_WEBHOOK_SECRET'));
if (! hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) $parts['t']) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);More languages are in Code examples.
Responding and retries
- Return any
2xxstatus within 10 seconds. Do slow work (emails, fulfilment) after responding, or in a queue. - Any other status, a timeout or a connection error counts as a failure. We retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. That is 8 attempts in total.
- After the last attempt the delivery is marked failed. You can see every attempt and retry it manually under Developers → Webhooks.
Duplicates and ordering
- We create each event once per status change, but a retry can deliver it again. Store
X-Gateway-Event-Idand skip IDs you have already handled. - Events can arrive out of order. Check
data.statusrather than assuming order. If unsure, read the latest state withGET /payments/{id}.
Rolling the secret
Rolling an endpoint's secret in the dashboard replaces it immediately, and the old secret stops working. Update your server first, then roll.