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
- Include a
webhook_urlparameter when creating a Quick Pay payment. - Wonderful sends a
POSTrequest to that URL at key moments during the payment lifecycle. - Your server responds with
200 OKto acknowledge receipt. Retryable failures (408,429and5xx) are retried with a backoff for up to 6 hours; permanent client errors (all other4xx) are not retried. - 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
- Customer selects their bank
- Customer consents to the payment
- Customer is redirected to their banking app
- Customer returns from their banking app
- Wonderful begins processing the payment
- Payment succeeds
- Payment fails
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:
- Validate that
order_idis present. - Call
GET /v2/orders/{order_id}to get the authoritative status. - Update your local records based on the authoritative status.
- Return
200 OKto 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
- Always verify via the API. The webhook payload status may be stale — fetch
GET /v2/orders/{order_id}for the source of truth. - Respond quickly. Return
200 OKas soon as you have validated and stored the notification. Heavy processing can be deferred to a queue. - Return a retryable status to trigger a retry. Return
408,429or any5xxstatus 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. - Do not return other
4xxstatuses unless the request is genuinely invalid. Permanent client errors (for example400,401,403,404,422) are not retried, so a webhook rejected with one of these statuses will be dropped. - Idempotency. Your webhook handler should be safe to run multiple times. A given payment may send multiple webhook notifications.
- 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.