Skip to main content

How do I receive payment notifications (webhooks)?

As soon as a payment is confirmed paid, SekaliPay sends a JSON POST to your callback_url with an X-Signature header: an HMAC-SHA256 over the raw body keyed with your secret key. Verify the signature, process the order, then answer 2xx.

This page was checked against the API code as of October 16, 2026.

When are webhooks sent?#

  • Production: once, when the payment becomes paid, including payments confirmed shortly after they expired.
  • There is no webhook for expired payments. The canceled status is only visible through a status check.
  • Sandbox: when you press a simulation button, with status completed or failed.
  • The destination is callback_url from the request, or the URL under API Settings, Webhook & Return URL. Without either, no webhook is sent.

What does a webhook request look like?#

HTTP
POST /webhook/sekalipay HTTP/1.1
Host: toko-anda.id
Content-Type: application/json
User-Agent: SekalipayGateway/1.0
X-Signature: 2101dd6c3812f2bcc9908337098fbded3c573aa3943d89abca71527840af0d76

{"merchant_ref_id":"ORDER-2026-00184","invoice":"SPY1791446400K3QZ","amount":150000,"fee":1500,"total":151500,"status":"paid","paid_at":"2026-10-08 15:05:12"}
merchant_ref_idstring
Your order ref.
invoicestring
SekaliPay invoice number.
amountinteger
The amount you receive.
feeinteger
Transaction fee.
totalinteger
What the buyer paid.
statusstring
paid in production; completed or failed in sandbox.
paid_atstring
Paid time as Y-m-d H:i:s in WIB (UTC+7) without a time zone. Different from the ISO format in API responses.

How do I verify the signature?#

Compute an HMAC-SHA256 over the raw body you received, keyed with your secret key, and compare it with X-Signature in constant time. Do not parse and re-encode the body: it is produced by PHP json_encode, which escapes / as \/, so a re-encoded body in another language differs byte for byte and the signature never matches.

$raw       = file_get_contents('php://input');
$expected  = hash_hmac('sha256', $raw, getenv('SEKALIPAY_SECRET_KEY'));
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);

http_response_code(200);
echo 'OK';
Test vector
secret_key  : sk_test_contoh_saja
body        : {"merchant_ref_id":"ORDER-2026-00184","invoice":"SPY1791446400K3QZ","amount":150000,"fee":1500,"total":151500,"status":"paid","paid_at":"2026-10-08 15:05:12"}
X-Signature : 2101dd6c3812f2bcc9908337098fbded3c573aa3943d89abca71527840af0d76

How should I answer a webhook?#

  • Answer with any 2xx status as soon as possible. The response body is not read.
  • Each attempt times out after 120 seconds. Run heavy work in a queue after answering.
  • The endpoint must be reachable from the internet. Internal addresses are rejected and not retried.

What happens when my endpoint fails?#

AttemptWhen
1Right after the payment
2About 30 seconds after the first attempt fails
3About 5 minutes after the second attempt fails

After 3 failed attempts, the webhook is marked failed. Fix your endpoint, then resend it from the transaction detail in the dashboard.

Delivery can be paused temporarily

If your endpoint fails many times in a row, deliveries to your account are paused so a struggling server is not flooded. Skipped transactions are marked failed and can be resent from the dashboard. Delivery resumes on its own after the pause, or immediately after one delivery succeeds.

How do I handle duplicate webhooks?#

The same webhook can arrive more than once: a manual resend, or a retry after your server processed it but answered too late. Record each processed invoice and ignore later deliveries. Also match amount against your order.

Ready to use your own keys?

Sign in to the merchant dashboard to copy your API key and secret key, set the access mode and webhook URL, and download a Postman collection with your account's keys.

Sign in to the dashboard