That's the whole loop
Create → settle → signed callback → fulfil. Swap your DEV- key for the PROD-
key and point customers at real transfers, and the same code runs in production.
This page takes you through the entire payment loop in the sandbox: create a
transaction, settle it on the fake BCA with simulate-pay, and verify the signed
callback. Nothing here touches real money.
You need three things from your merchant account (ask your Incognito Payments administrator, or read them from your merchant dashboard):
| Credential | Example | Used for |
|---|---|---|
| Merchant code | M0001 |
Identifies you; part of the request signature. |
| Sandbox API key | DEV-xxxxxxxx… |
Authorization: Bearer header. Starts with DEV-. |
| Sandbox private key | devsk-xxxxxxxx… |
Signs requests and verifies callbacks. Secret. |
# Private install from GitHub Packages — see the Node.js SDK page for the .npmrcnpm install @incognitoasia/payments# Private install from your GitHub VCS repo — see the PHP/Laravel SDK pagecomposer require incognitoasia/payments# No install needed — just curl + a way to compute HMAC-SHA256.Send your order reference and the amount you want to charge. We add a unique
3-digit code to the amount so the transfer can be matched automatically, and
return the exact pay_amount the customer must transfer.
import { IncognitoPayments } from "@incognitoasia/payments";
const payments = new IncognitoPayments({ apiKey: process.env.INCOGNITO_PAYMENTS_API_KEY!, // DEV-… sandbox key privateKey: process.env.INCOGNITO_PAYMENTS_PRIVATE_KEY!, merchantCode: "M0001", // baseUrl defaults to https://payment.incognito.asia});
const tx = await payments.createTransaction({ merchantRef: "ORDER-1001", amount: 150000, // what you want to receive, in IDR customerName: "Budi Santoso", customerEmail: "budi@example.com", callbackUrl: "https://yourapp.com/webhooks/incognito",});
console.log(tx.reference); // e.g. DEV-T... (sandbox references are DEV-prefixed)console.log(tx.pay_amount); // 150076 ← customer transfers EXACTLY thisconsole.log(tx.pay_code); // "076"console.log(tx.checkout_url); // hosted payment page you can redirect touse Incognito\Payments\PaymentClient;
$payments = new PaymentClient([ 'api_key' => env('INCOGNITO_PAYMENTS_API_KEY'), // DEV-… sandbox key 'private_key' => env('INCOGNITO_PAYMENTS_PRIVATE_KEY'), 'merchant_code' => 'M0001', // 'base_url' defaults to https://payment.incognito.asia]);
$tx = $payments->createTransaction([ 'merchant_ref' => 'ORDER-1001', 'amount' => 150000, 'customer_name' => 'Budi Santoso', 'customer_email'=> 'budi@example.com', 'callback_url' => 'https://yourapp.com/webhooks/incognito',]);
echo $tx['reference']; // DEV-T...echo $tx['pay_amount']; // 150076 ← customer transfers EXACTLY thisecho $tx['pay_code']; // "076"echo $tx['checkout_url']; // hosted payment page# signature = HMAC_SHA256( merchant_code + merchant_ref + amount ) keyed with the private keySIGNATURE=$(printf '%s' "M0001ORDER-1001150000" \ | openssl dgst -sha256 -hmac "$INCOGNITO_PAYMENTS_PRIVATE_KEY" -r | cut -d' ' -f1)
curl -s https://payment.incognito.asia/api/v1/transactions \ -H "Authorization: Bearer $INCOGNITO_PAYMENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "method": "BCA", "merchant_ref": "ORDER-1001", "amount": 150000, "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "callback_url": "https://yourapp.com/webhooks/incognito", "signature": "'"$SIGNATURE"'" }'The response is the standard envelope; data holds the transaction:
{ "success": true, "message": "", "data": { "reference": "DEV-T8K2P9ABC123", "merchant_ref": "ORDER-1001", "payment_method": "BCA", "amount": 150000, "pay_amount": 150076, "amount_total": 150076, "total_amount": 150076, "pay_code": "076", "unique_code": 76, "bank": { "name": "BCA", "account_number": "6485415487", "account_name": "…" }, "checkout_url": "https://payment.incognito.asia/pay/…", "status": "UNPAID", "expired_time": 1731600000 }}Instead of actually transferring money, tell the sandbox to pretend the customer paid. This runs the same match engine the real robot uses and fires your callback.
const settled = await payments.simulatePayment(tx.reference);console.log(settled.status); // "PAID"console.log(settled.paid); // true$settled = $payments->simulatePayment($tx['reference']);echo $settled['status']; // "PAID"echo $settled['paid']; // truecurl -s -X POST \ "https://payment.incognito.asia/api/v1/transactions/$REFERENCE/simulate-pay" \ -H "Authorization: Bearer $INCOGNITO_PAYMENTS_API_KEY"Moments later, we POST a signed JSON body to your callback_url. Verify the
X-Callback-Signature header against the raw request body using your private
key, then reply {"success": true}.
import express from "express";
const app = express();
// Capture the RAW body — signature is computed over exact bytes.app.post( "/webhooks/incognito", express.raw({ type: "application/json" }), (req, res) => { const raw = req.body.toString("utf8"); const ok = payments.verifyCallback(raw, req.header("X-Callback-Signature") ?? ""); if (!ok) return res.status(403).json({ success: false });
const event = JSON.parse(raw); if (event.status === "PAID") { // ✅ mark event.merchant_ref as paid } return res.json({ success: true }); },);// routes/web.php — protect the route with the bundled middlewareuse Illuminate\Support\Facades\Route;use Incognito\Payments\Laravel\VerifyCallbackSignature;
Route::post('/webhooks/incognito', function (Illuminate\Http\Request $request) { $event = $request->json()->all(); if (($event['status'] ?? null) === 'PAID') { // ✅ mark $event['merchant_ref'] as paid } return response()->json(['success' => true]);})->middleware(VerifyCallbackSignature::class);POST /webhooks/incognito HTTP/1.1X-Callback-Event: payment_statusX-Callback-Signature: 9f1c… ← HMAC_SHA256(raw_body) keyed with your private keyContent-Type: application/json
{"reference":"DEV-T8K2P9ABC123","merchant_ref":"ORDER-1001","status":"PAID","total_amount":150076,"amount_received":150076,"paid_at":1731500000,"is_closed_payment":1,"...":"..."}Your endpoint must respond HTTP 200 with the JSON body {"success": true}
(nothing else printed around it), or we retry with backoff.
That's the whole loop
Create → settle → signed callback → fulfil. Swap your DEV- key for the PROD-
key and point customers at real transfers, and the same code runs in production.