How to receive order webhooks
Optionally, CardPlusPay can POST a signed JSON notification to a URL you set on your API key when an order is created or becomes terminal. Polling GET /orders/{id} stays fully supported. You do not need webhooks to complete an integration.
When to use this
- Optional — If you never set a webhook URL, CardPlusPay does not POST to you. Use GET /orders/{id} as today.
- Faster notification — Receive
order.created,order.completed, ororder.failedwithout waiting for your next poll. - Still poll when needed — If you do not use webhooks, you must poll while an order is
pending. Even with webhooks, poll after timeouts, missed POSTs, or as a backup. Webhook payloads do not include gift card codes; load codes fromGET /orders/{id}afterorder.completed.
A failed or slow webhook does not change the order or your API wallet. Fulfillment continues independently of your receiver.
Step 1: Set a webhook URL on your API key
- In the CardPlusPay Dashboard, open API Keys.
- Create or edit a key and set Webhook URL (optional).
- The URL must be HTTPS. Private or loopback hosts are rejected.
- When you first save a URL (or rotate the secret), CardPlusPay shows a signing secret once. Store it in your secrets manager. It is not shown again on edit or view.
Clear the URL to stop sending. Empty URL does not mint a new secret.
Step 2: Understand events
There is no order.pending event. Pending is data.status on order.created.
| Event | When |
|---|---|
order.created |
An order exists after POST /orders. data.status may be pending or completed. |
order.completed |
Status becomes completed. If POST /orders already returns completed, you also receive this as a second POST (two deliveries, two ids). Later settlement of a pending order sends only this event (not created again). |
order.failed |
Status becomes failed (including a pending order that later fails). |
| Purchase result | Webhooks |
|---|---|
Sync completed in POST /orders |
order.created then order.completed (two POSTs, two ids) |
| Pending then later success | order.created (status: pending) then later order.completed |
| Pending then later fail | order.created (status: pending) then later order.failed |
Step 3: Headers and body
CardPlusPay POSTs to your HTTPS URL. This is not a path on api.cardpluspay.com.
| Header | Meaning |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | sha256=<hex> HMAC-SHA256 of the raw JSON body bytes actually POSTed |
X-Webhook-Event | Event name (order.created, order.completed, or order.failed) |
X-Webhook-Id | Stable id for this delivery. Retries of the same delivery reuse it. |
X-Webhook-Timestamp | Unix seconds. Reject the request if |now − timestamp| > 300 (5 minutes). |
{
"event": "order.created",
"id": "wh_...",
"created_at": "2026-09-13T00:00:00+00:00",
"data": {
"order_id": 123,
"order_no": "ORD-...",
"status": "pending",
"amount": 5.5
}
}
Treat JSON id (and X-Webhook-Id) as idempotent: process each id once; retries may repeat the same id. Do not expect codes, PINs, or supplier names in this payload.
Step 4: Verify the signature
Compute HMAC-SHA256 over the raw request body (do not re-encode JSON). Compare with X-Webhook-Signature using a constant-time comparison. Also check X-Webhook-Timestamp.
<?php
$secret = getenv('CARDPLUSPAY_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = (int) ($_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? 0);
if (abs(time() - $timestamp) > 300) {
http_response_code(400);
exit;
}
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
// Store $payload['id'] and skip if you already processed it.
http_response_code(200);
Step 5: Load codes via GET
After order.completed, call GET /orders/{order_id} to retrieve gift card codes (or confirm delivery.status for direct top-up). See order status & polling.
← Order status · Integration Guide · List orders & balance →