merchantid
QRIS payment gateway Not connected
Connection
Enter an admin token in Settings to load live gateway data.

01 / Workspace

Good morning, operator.

A live read on payment flow, provider health, and the work that needs attention.

EnvironmentProduction gatewayadm.uang.cc.cd

Live state

Payment state

Counts from the gateway database

No overview loaded. Connect to the Admin API.

Runtime controls

Keep the rails moving

Workers

Heartbeat and pause state

No worker data loaded.

Provider health

Latest adapter checks

No provider health data loaded.

Attention

Recent errors

No error data loaded.

Traceability

Latest audit events

Open full log

Audit events load with the overview.

02 / Workspace

Merchants

Create a business, open it, then configure the payment connection it owns.

One business at a timeSetup stays with its owner

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.

Credential policyRedacted responses

Provider configuration

Connect a provider account

Choose where this account applies, then complete the GoPay or ShopeePay login flow or use the manual fallback.

01
Choose provider

Select GoPay or ShopeePay.

02
Choose login method

Use the method supported by your provider.

03
Verify and save

Enter the code, then save the connection.

01

Choose a provider

Start with the provider account you want to connect.

Which provider do you want to connect?

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.

RecoveryReason required

Search and recovery

Payment ledger

No payment data loaded.

05 / Workspace

Webhooks

See callback delivery pressure and retry only records that actually failed.

DeliveryRetry is audited

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.

Live requestUses the gateway API

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.

API referenceBusiness-scoped access

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 /health requires 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 are readonly, support, operator, and owner; 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_..., businesses biz_..., provider accounts pa_..., API keys key_..., events evt_..., and deliveries wd_....

Start here

Connect a business in six steps

  1. Create or choose an active businessUse Merchants in the dashboard or the owner-only POST /v1/admin/businesses endpoint.
  2. 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.
  3. 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.
  4. Issue an API keyCreate a named key with the owner-only endpoint. Copy the plaintext key immediately; it is never returned again.
  5. Create and display a paymentCall POST /v1/payment-intents with both X-API-Key and a unique Idempotency-Key. Display qris_payload and the final_amount.
  6. Process the resultConsume signed payment.paid, payment.expired, and payment.canceled events idempotently. Poll the payment endpoint when webhook delivery is delayed.

Business API · 01

Create a payment intent

POST /v1/payment-intents

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

HMAC-SHA256 · 5 min freshness

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

Referensi kanonis

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.

RoleReadMutations
readonlyOverview, businesses, keys metadata, providers, payments, deliveries, audit, workersNone
supportEverything in readonlyRetry one webhook, retry all failed webhooks, provider health test
operatorEverything in supportPayment reconcile/retry-check/expire, business lifecycle, provider disable, worker pause/resume
ownerEverything in operatorCreate/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.

StatusCodes and meaningCaller action
401invalid_api_key, invalid_admin_tokenCheck the correct header, key status, and business status. Do not retry unchanged credentials.
403admin_role_forbiddenUse a token with the required role or ask an owner/operator.
404payment_not_found, business_not_found, provider_account_not_found, delivery_not_foundConfirm the identifier and API scope; business endpoints do not reveal another business's payment.
409provider_account_unavailable, missing_static_qris, payment_not_cancelable, provider_account_conflictFix configuration or current state before retrying. Do not create duplicate provider accounts.
400missing_idempotency_key, invalid_amount, invalid_expiry, invalid_callback_url, invalid_reasonCorrect the request. Reuse the same idempotency key only when intentionally retrying the same order.
502provider_login_failed or provider error codeInspect 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=.... Read status, attempt_count, last_http_status, and last_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-worker and verify its heartbeat.

Production checklist

Verify the boundary before enabling a business

  • Health and accessGET /health is 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.

RetentionGateway source of truth

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.

Trust boundaryBrowser session only

Admin API

Connection

The token is kept in this tab's memory and sent as X-Admin-Token.

01

Write controls

Action reason

Every state-changing action uses this field and appears in the audit record.

02

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.