Docs Open Console

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:

  1. Per store - set in Account → Stores for the transaction's store
  2. Agent settlements - for transactions whose external reference starts with res_ (AI-agent resources)
  3. Global - Settings → Webhooks: webhook-url plus webhook-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_reference to map deliveries to your orders