Webhooks
Get an HMAC-signed HTTP POST the moment a payment completes, with manual re-delivery and a delivery log in the console.
How they work#
When a transaction confirms, U.CASH Pay POSTs the full transaction record as JSON to your webhook URL. One call is sent per completed payment - there is no event-type field to switch on; the presence of the delivery is the event.
POST /your-endpoint HTTP/1.1
Host: yoursite.com
Content-Type: application/json
User-Agent: U.CASH-Pay
X-Webhook-Event-Id: 6f9c2b1e8a4d... (32-hex, unique per delivery)
X-Webhook-Signature: t=1771900800,v1=... (only if a secret is set)
The body is the transaction row plus delivery metadata:
{
"transaction": {
"id": "12345",
"amount": "0.0025",
"amount_fiat": "100.00",
"cryptocurrency": "btc",
"currency": "usd",
"status": "C",
"hash": "on-chain transaction hash",
"title": "Order #1024",
"external_reference": "your-order-ref",
"vat": "20.00",
"vat_details": "{...}",
"creation_time": "...",
"confirmed_time": "...",
"description": "notes and custom-field values",
"billing": "...",
"type": "1"
},
"event_id": "6f9c2b1e8a4d...",
"test": false
}
Parse unknown fields tolerantly - new columns are added over time and are not considered breaking.
Verifying the signature#
If you set a webhook secret, every delivery is signed with HMAC-SHA256 over the string <timestamp>.<raw-body>, delivered as X-Webhook-Signature: t=<unix>,v1=<hex>. Without a secret configured, deliveries are sent unsigned.
Verify with the raw request body - re-serialized JSON will not match:
$parts = explode(',', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '');
$ts = substr($parts[0] ?? '', 2);
$sig = substr($parts[1] ?? '', 3);
$expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) {
http_response_code(403);
exit;
}
Recommended: allow a 5-minute replay window, and deduplicate on event_id so a manual re-delivery never double-fulfills an order.
Configuration#
Webhook URLs are resolved in order, so different stores can target different endpoints:
- Per store - set in Account → Stores for the transaction's store
- Agent settlements - for transactions whose external reference starts with
res_(AI-agent resources) - Global - Settings → Webhooks:
webhook-urlpluswebhook-secret(masked, with copy and rotate actions)
URL requirements are enforced on save: HTTPS on a public host. Loopback, private-range, link-local, and metadata addresses are rejected.
To test end to end, call the API's test-connection function with your Cloud Token - it sends a real signed delivery to your URL and reports the HTTP status and whether the signature verified. See Embed, API & SDKs.
Delivery behavior#
- Timeouts: 5 s connect, 7 s total - acknowledge fast and process asynchronously
- No automatic retry. Failed deliveries are not re-sent on a schedule; use the Resend webhook action on the transaction's detail panel (or the API/agent endpoint) to re-deliver on demand
- Delivery log: every attempt is recorded with HTTP status and response body (first 1000 chars), visible on the transaction in the console
- Underpayments never fire. Only transactions that complete (
status = C) deliver; rejected underpayments (Underpayments) and manual payout completions do not
Best practices
- Always verify the signature and reject deliveries older than 5 minutes
- Deduplicate on
event_id- manual re-sends intentionally reuse the same event semantics - Return 200 quickly, then fulfill - slow endpoints will time out and count as failures in the log
- Key off
external_referenceto map deliveries to your orders