Webhooks

Webhooks provide real-time, server-to-server notifications when the status of a payment changes. Instead of polling the API, your application receives a POST request at a URL you configure when creating a Quick Pay payment.

Why use webhooks?

  • Speed: The payment succeeded webhook often arrives seconds before the customer is redirected back to your site, allowing you to begin fulfillment immediately.
  • Reliability: If a customer closes their browser before reaching your redirect_url, the webhook ensures you still receive the final payment status.
  • Security: Webhooks are sent directly from Wonderful's servers to yours — they are not visible to the client and cannot be tampered with.

How it works

  1. Include a webhook_url parameter when creating a Quick Pay payment.
  2. Wonderful sends a POST request to that URL at key moments during the payment lifecycle.
  3. Your server responds with 200 OK to acknowledge receipt. Retryable failures (408, 429 and 5xx) are retried with a backoff for up to 6 hours; permanent client errors (all other 4xx) are not retried.
  4. Always verify the status by calling GET /v2/orders/{order_id} — the webhook payload is informative, but the API is the single source of truth.

Webhook payload

{
    "order_id": "ed6d3016",
    "status": "paid",
    "updated_at": "2024-03-05T11:55:00.000000Z"
}
Field Type Description
order_id string The public API hash ID of the Order. Use this to fetch the authoritative status from GET /v2/orders/{order_id}.
status string The status of the latest payment on the Order at the moment the webhook was sent. Possible values: created, pending, paid, failed, cancelled, part-refunded, refunded, expired, errored, aborted.
updated_at string ISO 8601 timestamp of when the payment was last updated.

The payload does not contain the Wonderful payment ID. The order_id is the hash ID of the Order, which is the same ID returned by POST /v2/quick-pay and GET /v2/orders.

Events that trigger a webhook


Creating a payment with a webhook URL

Add the webhook_url field when calling POST /v2/quick-pay.

curl --request POST \
    "https://api.wonderful.one/v2/quick-pay" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data '{
        "amount": 1000,
        "merchant_payment_reference": "ORDER-1234",
        "redirect_url": "https://mysite.com/success",
        "webhook_url": "https://mysite.com/webhooks/wonderful"
    }'
fetch("https://api.wonderful.one/v2/quick-pay", {
    method: "POST",
    headers: {
        "Authorization": "Bearer {YOUR_AUTH_KEY}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    body: JSON.stringify({
        amount: 1000,
        merchant_payment_reference: "ORDER-1234",
        redirect_url: "https://mysite.com/success",
        webhook_url: "https://mysite.com/webhooks/wonderful",
    }),
});
$client = new \GuzzleHttp\Client();
$response = $client->post('https://api.wonderful.one/v2/quick-pay', [
    'headers' => [
        'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
    ],
    'json' => [
        'amount' => 1000,
        'merchant_payment_reference' => 'ORDER-1234',
        'redirect_url' => 'https://mysite.com/success',
        'webhook_url' => 'https://mysite.com/webhooks/wonderful',
    ],
]);

Verifying the order status

When your webhook endpoint receives a notification, fetch the authoritative status from the API:

curl --request GET \
    --get "https://api.wonderful.one/v2/orders/ed6d3016" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$orderId = $payload['order_id'];
$response = $client->get("https://api.wonderful.one/v2/orders/{$orderId}", [
    'headers' => [
        'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
    ],
]);
$body = json_decode((string) $response->getBody());
$authoritativeStatus = $body->data->order_status;

Only act on paid status — intermediate statuses (created, pending) may arrive before the payment is final.


Building the webhook listener (generic)

POST /webhooks/wonderful

Request body:
{
    "order_id": "ed6d3016",
    "status": "paid",
    "updated_at": "2024-03-05T11:55:00.000000Z"
}

Your endpoint should:

  1. Validate that order_id is present.
  2. Call GET /v2/orders/{order_id} to get the authoritative status.
  3. Update your local records based on the authoritative status.
  4. Return 200 OK to acknowledge receipt (prevents retries).

Node.js (Express) example

app.post("/webhooks/wonderful", async (req, res) => {
    const orderId = req.body.order_id;

    if (!orderId) {
        return res.status(200).json({ status: "error" });
    }

    const response = await fetch(
        `https://api.wonderful.one/v2/orders/${orderId}`,
        {
            headers: {
                Authorization: "Bearer {YOUR_AUTH_KEY}",
                "Content-Type": "application/json",
                Accept: "application/json",
            },
        }
    );
    const data = await response.json();
    const status = data.data.order_status;

    if (status === "paid") {
        // Update order status in your database
        // Trigger fulfillment
    }

    res.status(200).json({ status: "ok" });
});

PHP example (no framework)

<?php
$payload = json_decode(file_get_contents('php://input'), true);
$orderId = $payload['order_id'] ?? null;

if (!$orderId) {
    http_response_code(200);
    echo json_encode(['status' => 'error']);
    exit;
}

$client = new \GuzzleHttp\Client();
$response = $client->get(
    "https://api.wonderful.one/v2/orders/{$orderId}",
    [
        'headers' => [
            'Authorization' => 'Bearer {YOUR_AUTH_KEY}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = json_decode((string) $response->getBody());

if ($body->data->order_status === 'paid') {
    // Update order, trigger fulfillment
}

http_response_code(200);
echo json_encode(['status' => 'ok']);

Webhook best practices

  1. Always verify via the API. The webhook payload status may be stale — fetch GET /v2/orders/{order_id} for the source of truth.
  2. Respond quickly. Return 200 OK as soon as you have validated and stored the notification. Heavy processing can be deferred to a queue.
  3. Return a retryable status to trigger a retry. Return 408, 429 or any 5xx status if the delivery should be retried (for example, your database or the API call failed). Wonderful retries these with a backoff for up to 6 hours.
  4. Do not return other 4xx statuses unless the request is genuinely invalid. Permanent client errors (for example 400, 401, 403, 404, 422) are not retried, so a webhook rejected with one of these statuses will be dropped.
  5. Idempotency. Your webhook handler should be safe to run multiple times. A given payment may send multiple webhook notifications.
  6. Log everything. Record the webhook receipt, the API verification call, and any resulting actions for debugging.

Limitations

Webhooks are currently available only for payments created via the Quick Pay endpoint (POST /v2/quick-pay). Payments made through reusable QR code links do not currently support webhooks.