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, or order.failed without 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 from GET /orders/{id} after order.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

  1. In the CardPlusPay Dashboard, open API Keys.
  2. Create or edit a key and set Webhook URL (optional).
  3. The URL must be HTTPS. Private or loopback hosts are rejected.
  4. 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-Typeapplication/json
X-Webhook-Signaturesha256=<hex> HMAC-SHA256 of the raw JSON body bytes actually POSTed
X-Webhook-EventEvent name (order.created, order.completed, or order.failed)
X-Webhook-IdStable id for this delivery. Retries of the same delivery reuse it.
X-Webhook-TimestampUnix 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 →