Sidigi Payment Gateway — Integration Guide (IT)
Base URL: https://qris.ag1it.fun
Browser docs: https://qris.ag1it.fun/docs
Register (PIN): https://qris.ag1it.fun/portal/register.html
Merchant portal: https://qris.ag1it.fun/portal/
Health: https://qris.ag1it.fun/health
Ketentuan Layanan (Bahasa): https://qris.ag1it.fun/ketentuan.html
Settlement / pencairan: 2×24 jam (H+2) per transaksi setelah pembayaran berhasil.
Xendit Create QR Code reference: Create QR Code
Callback chain (important)
Merchant app Sidigi PG Xendit
| | |
| POST /v1/payments/qris | |
| (+ optional callback_url) | |
|------------------------------>| POST /qr_codes |
| | callback_url = |
| | https://qris.ag1it.fun/ |
| | webhooks/xendit |
| |------------------------------->|
| qr_string (Tag 59 branded) | qr_string |
|<------------------------------|<-------------------------------|
| | |
| | payer scans & pays |
| | |
| | POST qr.payment |
| | (x-callback-token) |
| |<-------------------------------|
| POST merchant callback_url | verify → mark paid |
| (HMAC signed) | |
|<------------------------------| |
- Merchant → Sidigi — create QRIS; optionally pass your
callback_url(or use the one saved on the merchant profile / portal). - Sidigi → Xendit — always sets Create QR Code
callback_urlto our webhook:https://qris.ag1it.fun/webhooks/xendit. - Xendit → Sidigi —
event: qr.paymentwhen paid; we verifyx-callback-token. - Sidigi → Merchant — after status is
paid, we POST to your callback URL withX-Sidigi-Signature.
Merchants never talk to Xendit webhooks directly.
1. Roles
| Role | How you authenticate |
|---|---|
| Register (IT / merchant) | Registration PIN on Register |
| Admin (Sidigi ops) | X-Admin-Key: <ADMIN_API_KEY> |
| Merchant (backend) | Authorization: Bearer sk_sidigi_... |
| Xendit | POST /webhooks/xendit + x-callback-token |
Self-register (PIN)
Open: https://qris.ag1it.fun/portal/register.html
Default PIN (change via REGISTER_PIN env): ask Sidigi ops (Sidigi123 on initial deploy).
What register does (only this):
- Validates PIN
- Creates merchant (
code, name, password, callback_url) - Returns
api_key+callback_secretonce
It does not set bank / disbursement details. Bank verification stays admin-only.
POST https://qris.ag1it.fun/register/merchants
Content-Type: application/json
{
"pin": "Sidigi123",
"code": "toko_rafsan",
"name": "Toko Rafsan",
"email": "[email protected]",
"password": "your-portal-password",
"callback_url": "https://merchant.example.com/webhooks/sidigi"
}
Merchant Portal (browser)
https://qris.ag1it.fun/portal/
Login with merchant code + portal password.
2. Admin: create a merchant (optional)
Admin can still create merchants without the PIN page:
POST https://qris.ag1it.fun/admin/merchants
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json
{
"code": "toko_rafsan",
"name": "Toko Rafsan",
"email": "[email protected]",
"callback_url": "https://merchant.example.com/webhooks/sidigi"
}
Response (credentials shown once):
{
"success": true,
"merchant": {
"id": "uuid",
"code": "toko_rafsan",
"name": "Toko Rafsan",
"callback_url": "https://merchant.example.com/webhooks/sidigi"
},
"credentials": {
"api_key": "sk_sidigi_...",
"callback_secret": "whsec_...",
"portal_password": "..."
}
}
Update default callback URL
PATCH https://qris.ag1it.fun/admin/merchants/{id}/callback
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json
{ "callback_url": "https://merchant.example.com/hooks/sidigi" }
Merchants can also change it in the portal.
Set / verify bank (once)
PUT https://qris.ag1it.fun/admin/merchants/{id}/bank
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json
{
"bank_code": "BCA",
"account_number": "1234567890",
"account_name": "TOKO RAFSAN",
"verify": true
}
3. Merchant: create a unique QRIS payment
Uses Xendit QR Codes V1 (POST /qr_codes, type DYNAMIC).external_id must be unique per merchant.
POST https://qris.ag1it.fun/v1/payments/qris
Authorization: Bearer sk_sidigi_...
Content-Type: application/json
{
"external_id": "ORDER-1001",
"amount": 25000,
"description": "Order 1001",
"customer_name": "Budi",
"callback_url": "https://merchant.example.com/webhooks/sidigi",
"metadata": { "table": "A3" }
}
| Field | Required | Notes |
|---|---|---|
external_id |
yes | Your order id |
amount |
yes | Min 1,500 / max 10,000,000 IDR |
callback_url |
no | Your webhook for this payment; else merchant profile callback |
description / customer_name / metadata |
no |
Success 201:
{
"success": true,
"payment": {
"id": "uuid",
"external_id": "ORDER-1001",
"amount": 25000,
"status": "requires_action",
"merchant_display_name": "Toko Rafsan",
"callback_url": "https://merchant.example.com/webhooks/sidigi",
"qr_string": "000201...",
"expires_at": "...",
"xendit_qr_id": "qr_..."
}
}
How to show the QR
- Render
payment.qr_stringas a QR image - Do not alter the EMV string (Tag 59 is already set by Sidigi)
- Poll
GET /v1/payments/{id}or wait for your callback
Status values
| Status | Meaning |
|---|---|
pending / requires_action |
Waiting for payer |
paid |
Confirmed via Xendit qr.payment |
expired / failed |
Unpaid / failed |
4. Merchant: get payment status
GET https://qris.ag1it.fun/v1/payments/{payment_id}
Authorization: Bearer sk_sidigi_...
GET https://qris.ag1it.fun/v1/payments
Authorization: Bearer sk_sidigi_...
5. Merchant callback (you implement this)
When Sidigi verifies Xendit’s qr.payment, we POST to your URL
(per-payment callback_url or merchant default).
Headers
| Header | Meaning |
|---|---|
Content-Type |
application/json |
X-Sidigi-Signature |
hex(HMAC-SHA256(callback_secret, raw_body)) |
X-Sidigi-Event |
e.g. payment.paid |
Body example
{
"event": "payment.paid",
"merchant_code": "toko_rafsan",
"merchant_name": "Toko Rafsan",
"payment": {
"id": "uuid",
"external_id": "ORDER-1001",
"amount": 25000,
"status": "paid",
"merchant_display_name": "Toko Rafsan",
"paid_at": "..."
},
"sent_at": "..."
}
Verify signature (Node)
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || "");
return a.length === b.length && timingSafeEqual(a, b);
}
Rules
- Verify signature before trusting the event
- Idempotent on
payment.id/external_id - Respond
2xxquickly - Fulfill only when
event === "payment.paid"andstatus === "paid" - Retry:
POST /v1/payments/{id}/retry-callback
6. Payouts / disbursement
Access: only merchants with payout_enabled = true (admin-activated) can request disbursement.
POST https://qris.ag1it.fun/admin/merchants/{id}/payout-access
X-Admin-Key: ...
Content-Type: application/json
{ "enabled": true }
Settlement hold: each paid transaction becomes withdrawable 48 hours (H+2) after its own paid_at.
Until then it shows as settling in the balance; next_release_at tells you when the oldest one matures.
See Ketentuan Layanan.
Flow:
- Admin verifies the merchant bank account once, then activates disbursement (
payout-access) - Each transaction matures 48h after payment → moves from
settlingtoavailable - Merchant requests withdraw manually (
POST /v1/payouts/ portal) or enables auto disbursement - Admin marks
approved→processing→paid(orrejected)
Auto disbursement (every 3 days)
Toggle in the portal (Auto disbursement card), or:
PATCH https://qris.ag1it.fun/portal/api/auto-payout
Authorization: Bearer <portal session token>
Content-Type: application/json
{ "enabled": true }
- Requires a verified bank account
- First run is 3 days after enabling, then every 3 days
- Each run requests the full available balance as a payout with
source: "auto" - Skipped (schedule still advances) if available is below Rp10.000
- Payouts still go through the admin status flow below
POST https://qris.ag1it.fun/v1/payouts
Authorization: Bearer sk_sidigi_...
Content-Type: application/json
{ "amount": 100000, "note": "Weekly settlement" }
Admin:
GET https://qris.ag1it.fun/admin/payouts
X-Admin-Key: ...
POST https://qris.ag1it.fun/admin/payouts/{id}/status
X-Admin-Key: ...
Content-Type: application/json
{ "status": "paid", "admin_note": "Disbursed" }
Statuses: requested → approved → processing → paid | rejected | failed
7. Xendit → Sidigi webhook (infra only)
POST https://qris.ag1it.fun/webhooks/xendit
x-callback-token: <XENDIT_CALLBACK_TOKEN>
Sidigi sets this URL on every Create QR Code call (docs).
Mapped events:
qr.payment/status: COMPLETED→ paymentpaid→ merchantpayment.paid- expired / failed → matching merchant callback
Also set the same URL + verification token in Xendit Dashboard Callback Settings.
8. QR branding (Tag 59 + Tag 60 + Tag 61)
After the QR is created, Sidigi rewrites:
| Tag | Field | Value |
|---|---|---|
| 59 | Merchant name / prefix | Store name from portal or merchant profile (max 25 bytes) |
| 60 | Location / city | Papua (QRIS_CITY) |
| 61 | Postal code | 99332 (QRIS_POSTAL_CODE) |
CRC (Tag 63) is recalculated. Change the store name in the portal; city and zip are fixed.
9. Error codes
| HTTP | Meaning |
|---|---|
| 400 | Validation / amount < 1500 / bank not verified |
| 401 | Bad admin or merchant key |
| 403 | Merchant suspended |
| 409 | Duplicate external_id |
| 502 | Xendit rejected create |
10. Merchant IT go-live checklist
- Registered with PIN or received credentials from admin
- HTTPS callback with signature verify
- Default
callback_urlset (register / portal) or pass per QR - Create payment with unique
external_id - Amount ≥ 1500 IDR
- Render
qr_stringwithout modification - Fulfill only on
payment.paid - Bank verified and disbursement activated by Sidigi ops before payouts