Skip to main content

Webhooks

When an order's status changes, the API sends an HTTP POST to your notify_url. This is how you know when a payment is confirmed.

Setting Your Notify URL​

You can specify notify_url on each order creation request. If omitted, the system uses the default URL configured on your merchant profile. Ask your administrator to confirm that URL before live testing.

Payload Format​

{
"event": "order.PAID",
"ref_code": "ORD-260601103045-A1B2",
"merchant_order_id": "your-internal-order-id",
"amount": 50000,
"status": "PAID",
"paid_at": "2026-06-01T10:30:45Z"
}

Fields​

FieldTypeDescription
eventstringorder.<STATUS> — e.g., order.PAID, order.FAILED, order.EXPIRED
ref_codestringFlyPay internal order reference
merchant_order_idstringYour original order ID
amountnumberOrder amount in Rupiah
statusstringNew order status
paid_atstring (ISO 8601)Timestamp of payment; null if not yet paid

Status Values​

StatusMeaning
PENDINGAwaiting payment
PAIDPayment confirmed
EXPIREDOrder expired before payment
FAILEDPayment failed or was rejected
CANCELLEDOrder was cancelled
REFUNDEDFully refunded
PARTIALLY_REFUNDEDPartially refunded

Responding to Webhooks​

Your endpoint must return any HTTP 2xx response within 10 seconds. Non-2xx responses and timeouts trigger a retry.

tip

Immediately return 200 OK and process the event asynchronously. Do not block the response on database writes or downstream API calls.

PHP — minimal webhook handler
<?php
// Acknowledge immediately
http_response_code(200);
echo json_encode(['received' => true]);

// Close the connection so PHP can continue processing
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}

// Now process safely
$payload = json_decode(file_get_contents('php://input'), true);
if ($payload['status'] === 'PAID') {
// update your order in DB
}

Verifying Webhook Signatures​

If your merchant has a Webhook Secret configured, every V1 order webhook delivery includes X-Signature: sha256=<lowercase-hex-HMAC> over the exact raw request body. Verify it before acknowledging the event. Without a configured secret, no signature is sent; provision a secret before using webhooks for fulfillment. Legacy API callbacks use their separate legacy signature format.

PHP — signature verification
$rawBody = file_get_contents('php://input');
$receivedSig = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expectedSig = 'sha256=' . hash_hmac('sha256', $rawBody, $webhookSecret);

if (!hash_equals($expectedSig, $receivedSig)) {
http_response_code(401);
exit('Invalid signature');
}
Node.js — signature verification
const crypto = require('crypto');

function verifySignature(rawBody, receivedSig, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const actual = Buffer.from(receivedSig);
const wanted = Buffer.from(expected);
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
caution

Always use a constant-time comparison function (hash_equals in PHP, timingSafeEqual in Node.js) to prevent timing attacks.

Retry Schedule​

If your endpoint does not return HTTP 2xx, delivery is retried on this schedule:

AttemptDelay after previous
1 (initial)Immediate
2+30 seconds
3+2 minutes
4+10 minutes
5+30 minutes
6 (final)+2 hours

After the final attempt, the webhook is marked as permanently failed. You can still poll the order status via GET /v1/orders/:ref_code.

Idempotency​

Your system may receive the same webhook event more than once (network failures, retries). Always check the order status in your own database before updating — use ref_code or merchant_order_id as the deduplication key.