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 falls back to the default notify_url configured in your merchant profile (set in the dashboard under Settings → Notifications).

Payload Format

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

Fields

FieldTypeDescription
eventstringorder.<STATUS> — e.g., order.PAID, order.FAILED, order.EXPIRED
ref_codestringPGA internal order reference
merchant_order_idstringYour original order ID
amount_minorintegerOrder amount in minor units
currencystringAlways IDR
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 HTTP 200 within 10 seconds. Any other response code (or a timeout) is treated as a delivery failure and triggers 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 you have configured a Webhook Secret in the dashboard, every webhook delivery includes an X-Signature header. Verify it to ensure the request is genuinely from Nusio Saka:

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');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(receivedSig)
);
}
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 200, 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.