01 / Workspace
Good morning, operator.
A live read on payment flow, provider health, and the work that needs attention.
Live state
Payment state
No overview loaded. Connect to the Admin API.
Runtime controls
Keep the rails moving
Workers
Heartbeat and pause stateNo worker data loaded.
Provider health
Latest adapter checksNo provider health data loaded.
Attention
Recent errors
No error data loaded.
Traceability
Latest audit events
Audit events load with the overview.
02 / Workspace
Merchants
Create a business, open it, then configure the payment connection it owns.
Step 1
Create or choose a business
A business is the owner of its callback URL and API keys. Open one below to continue setup.
Your businesses
Choose a business to manage
No business data loaded.
03 / Workspace
Providers
Connect GoPay and ShopeePay accounts without exposing stored credentials.
Provider configuration
Connect a provider account
Choose where this account applies, then complete the GoPay or ShopeePay login flow or use the manual fallback.
Select GoPay or ShopeePay.
Use the method supported by your provider.
Enter the code, then save the connection.
Use a manual provider token instead
Log in through Shopee Partner first, then paste only the B: token from a transaction request. Do not paste browser cookies or the full curl command.
Saved connections
Provider accounts
Global provider accounts used by every business.
No provider accounts loaded.
04 / Workspace
Payments
Find a payment, inspect its provider trail, and take an audited recovery action.
Search and recovery
Payment ledger
No payment data loaded.
05 / Workspace
Webhooks
See callback delivery pressure and retry only records that actually failed.
Callback delivery
Delivery queue
No webhook data loaded.
06 / Developer
Playground
Send one real payment request with a business API key and inspect the response before connecting your app.
Try the integration
Create a payment intent
Choose a business key from the setup flow or paste one for this browser session. The playground reuses one idempotency key for the same request, including retries after a timeout. Change the request fields to start a new payment.
No request sent yet.
Before you connect
What this test does
- Creates
- One payment intent in the selected business account.
- Returns
- Payment ID, final amount, expiry, status, and QRIS payload.
- Does not do
- It does not charge a customer or guarantee that a provider account is ready.
07 / Developer
Integration docs
Complete reference for business applications, operators, provider setup, payment state, and signed webhook delivery.
Contract
Base URLs, authentication, and conventions
The gateway serves the dashboard and API from the same origin. Use the API base URL without /admin/ for API calls. FastAPI also publishes the machine-readable contract at /openapi.json, Swagger UI at /docs, and ReDoc at /redoc.
- Health
GET /healthrequires no token and returns{"status":"ok"}.- Business API
- Send
X-API-Key. The key identifies one active business; it cannot call Admin API routes. - Admin API
- Send
X-Admin-Token. Roles arereadonly,support,operator, andowner; each role includes permissions below it. - Content type
- JSON request and response bodies use
Content-Type: application/json. - Time
- All timestamps are ISO-8601 with a UTC offset. Amounts are positive integer rupiah in
IDR. - Identifiers
- Payment intents use
pi_..., businessesbiz_..., provider accountspa_..., API keyskey_..., eventsevt_..., and deliverieswd_....
Start here
Connect a business in six steps
- Create or choose an active businessUse Merchants in the dashboard or the owner-only
POST /v1/admin/businessesendpoint. - Configure a provider accountComplete the GoPay or ShopeePay login flow, or use the manual provider-account endpoint. An active account must have a static QRIS payload before payment creation.
- Configure callback deliverySave an HTTPS callback URL and a secret of at least 16 characters. Private IPs, localhost, credentials in the URL, and non-HTTPS URLs are rejected.
- Issue an API keyCreate a named key with the owner-only endpoint. Copy the plaintext key immediately; it is never returned again.
- Create and display a paymentCall
POST /v1/payment-intentswith bothX-API-Keyand a uniqueIdempotency-Key. Displayqris_payloadand thefinal_amount. - Process the resultConsume signed
payment.paid,payment.expired, andpayment.canceledevents idempotently. Poll the payment endpoint when webhook delivery is delayed.
Business API · 01
Create a payment intent
The idempotency key is scoped to the business. Repeating the same key returns the original payment instead of creating a second intent. Always generate one new key per order.
curl -X POST "$GATEWAY_URL/v1/payment-intents" \
-H "X-API-Key: $BUSINESS_API_KEY" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{
"provider": "gopay",
"reference_id": "order-1001",
"amount": 50000,
"currency": "IDR",
"description": "Lunch order",
"expires_in_seconds": 900
}'Request fields: provider is gopay or shopeepay; reference_id is required; amount is a positive integer; currency must be IDR; expires_in_seconds is 60–86400; static_qris is optional and overrides the account payload for this payment.
{
"id": "pi_...",
"business_id": "biz_...",
"provider": "gopay",
"provider_account_id": "pa_...",
"reference_id": "order-1001",
"requested_amount": 50000,
"unique_code": 137,
"final_amount": 50137,
"currency": "IDR",
"status": "pending",
"qris_payload": "000201...",
"expires_at": "2026-09-27T07:00:00+00:00",
"created_at": "2026-09-27T06:45:00+00:00",
"updated_at": "2026-09-27T06:45:00+00:00",
"paid_at": null,
"provider_transaction_id": null,
"failure_code": null
}Business API · 02–05
Read, check, or cancel a payment
The GET route is read-only. Use the POST /check route after the customer reports payment to trigger one immediate provider lookup without waiting for the worker interval. It reuses the same matching and settlement safeguards as the worker. Calling it again after settlement is safe and returns the current terminal status.
# Current payment state
curl "$GATEWAY_URL/v1/payment-intents/pi_..." \
-H "X-API-Key: $BUSINESS_API_KEY"
# Trigger one immediate provider status check
curl -X POST "$GATEWAY_URL/v1/payment-intents/pi_.../check" \
-H "X-API-Key: $BUSINESS_API_KEY"
# Webhook delivery events for one payment
curl "$GATEWAY_URL/v1/payment-intents/pi_.../events" \
-H "X-API-Key: $BUSINESS_API_KEY"
# Cancel a pending payment
curl -X POST "$GATEWAY_URL/v1/payment-intents/pi_.../cancel" \
-H "X-API-Key: $BUSINESS_API_KEY"The check response contains payment and matched. A true matched means this check moved the payment to paid. Routes: GET /v1/payment-intents/{payment_id}, POST /v1/payment-intents/{payment_id}/check, GET /v1/payment-intents/{payment_id}/events, and POST /v1/payment-intents/{payment_id}/cancel.
{
"matched": true,
"payment": { "id": "pi_...", "status": "paid", "reference_id": "order-1001", "provider_transaction_id": "txn_..." }
}Webhook contract
Verify the raw body before parsing
The worker signs the exact compact JSON bytes it sends. Build message = <X-Webhook-Timestamp> + "." + <raw request body>, compute HMAC-SHA256 with the webhook secret, and compare the hexadecimal digest as v1=<digest> using a constant-time comparison. Reject timestamps older than five minutes, invalid signatures, and duplicate event IDs.
import hashlib, hmac
def verify_webhook(secret: str, timestamp: str, raw_body: bytes, received: str) -> bool:
message = timestamp.encode() + b"." + raw_body
expected = "v1=" + hmac.new(
secret.encode(), message, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)
# Headers sent by the gateway:
# X-Webhook-Id: evt_...
# X-Webhook-Timestamp: 172742...
# X-Webhook-Signature: v1=...Read the request body once as bytes before JSON decoding. Return any HTTP 2xx response only after validation and idempotent processing. A non-2xx response, timeout, or network error schedules a retry; the event ID remains stable across retries.
- payment.paid
- Emitted after the provider transaction matches provider, account, currency, and final amount and the payment is durably marked paid.
- payment.expired
- Emitted when a pending payment is expired.
- payment.canceled
- Emitted when a pending payment is canceled.
- Retry schedule
- Attempts use bounded exponential backoff starting at 30 seconds and stop after the configured maximum. Failed deliveries remain visible to support for manual retry.
Kontrak bahasa Indonesia
Jangan tukar gateway dan provider
payment.provider berarti jalur pembayaran, yaitu gopay atau shopeepay. Field ini bukan nama gateway. Identitas gateway berasal dari konfigurasi integrasi aplikasi bisnis. Payload, aturan signature, retry, idempotensi, dan contoh handler lengkap tersedia di Kontrak Webhook Pembayaran.
# Benar: provider adalah jalur pembayaran
if payment["provider"] in {"gopay", "shopeepay"}:
cocokkan_dengan_order(payment)
# Salah: provider bukan identitas gateway
if payment["provider"] == "nama-gateway-anda":
proses_callback()Admin API
Operator routes and role boundaries
Every Admin API request needs X-Admin-Token. Mutating actions that affect operations require a non-empty reason; these actions write an audit record. Never send provider credentials, webhook secrets, or admin tokens to the browser or logs.
| Role | Read | Mutations |
|---|---|---|
readonly | Overview, businesses, keys metadata, providers, payments, deliveries, audit, workers | None |
support | Everything in readonly | Retry one webhook, retry all failed webhooks, provider health test |
operator | Everything in support | Payment reconcile/retry-check/expire, business lifecycle, provider disable, worker pause/resume |
owner | Everything in operator | Create/update businesses, issue/revoke/rotate keys, provider login/accounts, webhook configuration |
Businesses and API keys
POST /v1/admin/businesses · GET /v1/admin/businesses · GET /v1/admin/businesses/{business_id} · PATCH /v1/admin/businesses/{business_id} · POST /v1/admin/businesses/{business_id}/activate · POST /v1/admin/businesses/{business_id}/suspend · POST /v1/admin/businesses/{business_id}/archive · POST /v1/admin/businesses/{business_id}/api-keys · GET /v1/admin/businesses/{business_id}/api-keys · POST /v1/admin/api-keys/{key_id}/revoke · POST /v1/admin/api-keys/{key_id}/rotate
curl -X POST "$GATEWAY_URL/v1/admin/businesses/biz_.../api-keys" \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"orders-prod","reason":"issue production integration key"}'
{ "id": "key_...", "business_id": "biz_...", "name": "orders-prod",
"api_key": "qris_live_...", "created_at": "2026-09-27T06:45:00+00:00" }The plaintext api_key appears only on issue or rotate. Listing keys returns metadata only. A suspended or archived business cannot authenticate its business key.
Provider accounts and login
POST /v1/admin/provider-accounts · GET /v1/admin/provider-accounts · POST /v1/admin/businesses/{business_id}/provider-accounts · GET /v1/admin/businesses/{business_id}/provider-accounts · GET /v1/admin/provider-accounts/{account_id} · PATCH /v1/admin/provider-accounts/{account_id} · POST /v1/admin/provider-accounts/{account_id}/rotate-token · POST /v1/admin/provider-accounts/{account_id}/disable · POST /v1/admin/provider-accounts/{account_id}/test · GET /v1/admin/provider-accounts/{account_id}/health
Use PATCH for a field update when an active account already exists; omitted fields are preserved, including credentials. Use rotate-token when replacing credentials intentionally. Provider login adds these owner-only routes: POST /v1/admin/businesses/{business_id}/provider-login/{provider}/start, POST .../{session_id}/verify, and ShopeePay merchant selection POST .../{session_id}/complete. OTP sessions expire after ten minutes.
Webhook, payment, delivery, and worker operations
POST /v1/admin/businesses/{business_id}/webhook-endpoint configures a callback. Payment operations are GET /v1/admin/payments, GET /v1/admin/payments/{payment_id}, POST .../{payment_id}/reconcile, POST .../{payment_id}/retry-check, and POST .../{payment_id}/expire. Delivery operations are GET /v1/admin/webhook-deliveries, GET /v1/admin/webhook-deliveries/{delivery_id}, POST .../{delivery_id}/retry, and POST /v1/admin/webhook-deliveries/retry-failed. Worker operations are GET /v1/admin/workers, GET /v1/admin/workers/{worker_id}, POST .../{worker_id}/pause, and POST .../{worker_id}/resume. Audit history is GET /v1/admin/audit-logs; system summary is GET /v1/admin/overview.
curl -X POST "$GATEWAY_URL/v1/admin/webhook-deliveries/wd_.../retry" \
-H "X-Admin-Token: $SUPPORT_TOKEN" \
--data-urlencode "reason=callback endpoint recovered"
{ "id": "wd_...", "event_id": "evt_...", "status": "pending",
"attempt_count": 2, "next_attempt_at": "2026-09-27T07:00:00+00:00" }Errors and recovery
Treat error codes as the contract
Errors use the same JSON shape: {"detail":{"code":"...","message":"..."}}. Validation errors generated by FastAPI use its standard detail list. Log the code and request identifier in the caller, never secrets or full credentials.
| Status | Codes and meaning | Caller action |
|---|---|---|
| 401 | invalid_api_key, invalid_admin_token | Check the correct header, key status, and business status. Do not retry unchanged credentials. |
| 403 | admin_role_forbidden | Use a token with the required role or ask an owner/operator. |
| 404 | payment_not_found, business_not_found, provider_account_not_found, delivery_not_found | Confirm the identifier and API scope; business endpoints do not reveal another business's payment. |
| 409 | provider_account_unavailable, missing_static_qris, payment_not_cancelable, provider_account_conflict | Fix configuration or current state before retrying. Do not create duplicate provider accounts. |
| 400 | missing_idempotency_key, invalid_amount, invalid_expiry, invalid_callback_url, invalid_reason | Correct the request. Reuse the same idempotency key only when intentionally retrying the same order. |
| 502 | provider_login_failed or provider error code | Inspect provider health and login session state; bound retries and avoid repeated OTP requests. |
Troubleshooting
Diagnose webhook and payment issues
- Webhook signature fails
- Use the raw bytes, not reserialized JSON; include the exact timestamp string; prepend
v1=; use the configured business secret; enforce constant-time comparison. Check clock skew when the five-minute freshness window rejects a valid request. - Webhook is not arriving
- Check
GET /v1/admin/webhook-deliveries?payment_id=.... Readstatus,attempt_count,last_http_status, andlast_error_code. Confirm the callback is public HTTPS and not a private or loopback address, then retry with a reason after recovery. - Payment stays pending
- Confirm the provider account is active and healthy, the QRIS payload belongs to that account, and the customer paid the exact
final_amount. Use operator reconcile or retry-check with an auditable reason; never edit payment rows directly. - Duplicate account error
- An active account already exists. Use
PATCH /v1/admin/provider-accounts/{account_id}for QRIS or metadata changes; leave credentials omitted to preserve the existing secret. - API key stopped working
- Check business status and key metadata. A revoked key cannot be restored; rotate it through the owner route and update the calling application atomically.
- Worker pause confusion
- Dashboard pause/resume changes the operational control record. It does not terminate the separate systemd worker process. For maintenance, stop/start
qris-gateway-workerand verify its heartbeat.
Production checklist
Verify the boundary before enabling a business
- Health and access
GET /healthis OK; Admin API access uses an external MFA/re-authentication boundary; the dashboard loads without browser console errors. - SecretsAdmin tokens, encryption key, database credentials, provider credentials, and webhook secrets are outside Git and never printed in logs.
- DurabilityDatabase migration, backup, and restore have been tested. Payment and audit history is retained across suspend/archive.
- CanaryStart with one business and one provider; verify pending, paid, expiration, cancel, webhook retry, audit, worker heartbeat, disk, and journal behavior before expanding.
- RollbackDeploy the previous verified package and restart both gateway units. Do not roll back encryption keys or schema without a tested recovery plan.
See deploy/OPERATIONS.md for systemd, database, secret, backup, canonical domain, and rollback procedures.
08 / Records
Audit log
A chronological record of operator actions and their stated reasons.
Operator trail
All recorded events
Connect to load audit events.
07 / Records
Settings
Configure this browser's Admin API connection and the reason attached to write actions.
Admin API
Connection
The token is kept in this tab's memory and sent as X-Admin-Token.
Write controls
Action reason
Every state-changing action uses this field and appears in the audit record.
Operational boundary
What this panel can do
- Read
- Overview, merchants, provider accounts, payments, webhooks, workers, and audit records.
- Write
- Lifecycle actions, provider login, payment recovery, worker state, and webhook retry, all with a reason.
- Secrets
- Provider credentials and webhook secrets are never rendered by the API.