Overview Not connected
Connection settings
Enter an admin token in Settings to load live gateway data.

Overview

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

EnvironmentNot connectedWorker API

Payment state

Counts from the gateway database

No overview loaded. Connect to the Admin API.

Keep the rails moving

Workers

Heartbeat and pause state

No worker data loaded.

Provider health

Latest adapter checks

No provider health data loaded.

Recent errors

No error data loaded.

Latest audit events

Open full log

Audit events load with the overview.

Merchants

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

Create or choose a business

A business is the owner of its callback URL and API keys. Open one below to continue setup.

Choose a business to manage

No business data loaded.

Providers

Connect GoPay and ShopeePay accounts without exposing stored credentials.

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.

Provider accounts

Global provider accounts used by every business.

No provider accounts loaded.

Payments

Find a payment, inspect its provider trail, and take an audited recovery action.

Payment ledger

No payment data loaded.

Webhooks

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

Delivery queue

No webhook data loaded.

Playground

Send one real payment request with a business API key and inspect the response before connecting your app.

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.

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.

Integration docs

Complete reference for business applications, operators, provider setup, payment state, and signed webhook delivery.

Base URLs, authentication, and conventions

The dashboard is served by Cloudflare Pages and calls the Worker API at the Admin API URL configured in Settings. Use that Worker origin for every API call. The Worker exposes no OpenAPI, Swagger, or ReDoc route; this page is the contract.

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. This deployment uses one owner token; it can read and mutate every Admin route.
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_....

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.

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
}

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_..." }
}

Business provider status

GET /v1/status

Send the business X-API-Key. The Worker resolves the business override first, then the shared account, and probes the provider token. The response contains no credentials or account ids.

curl "$GATEWAY_URL/v1/status" \
  -H "X-API-Key: $BUSINESS_API_KEY"

{
  "business_id": "biz_...",
  "checked_at": "2026-11-02T23:00:00.000Z",
  "providers": {
    "gopay": { "status": "active", "reason": null, "scope": "business" },
    "shopeepay": { "status": "inactive", "reason": "provider_health_check_failed", "scope": "system" }
  }
}
active
The provider answered the token probe; payments can use this account.
inactive
The token or session is unusable, no account is connected (not_connected), or the adapter is disabled.
scope
business means the business override answered; system means the shared account. null means nothing is connected.
cache
Public status is cached for 60 seconds. Operators can force a live check with GET /v1/admin/businesses/{business_id}/status?fresh=1.

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.

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()

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.

TokenReadMutations
ownerEvery Admin routeEvery state-changing route, each with a reason

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"}'

{ "api_key": { "id": "key_...", "name": "orders-prod", "active": true },
  "plaintext": "qris_live_..." }

The plaintext plaintext field appears only on issue or rotate; it is never returned again. Listing keys returns metadata only. Rotate with POST /v1/admin/api-keys/{key_id}/rotate and revoke with POST /v1/admin/api-keys/{key_id}/revoke?reason=.... Rotation revokes the old key immediately.

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" }

Treat error codes as the contract

Errors use the same JSON shape: {"detail":{"code":"...","message":"..."}}. Validation errors use the same detail object; fix the field named in the message. 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.

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 writes the durable worker_controls row. The cron tick reads that row before polling or delivering, so no process restart is needed. Resume writes the same row; verify the heartbeat on Overview.

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.
  • DurabilityD1 migrations 0001 to 0003 are applied and an export has been restored. 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 Worker version and keep the current D1 schema. Do not roll back encryption keys or schema without a tested recovery plan.

See the repository README for D1 migration, secret, deployment, backup, and rollback procedures.

Audit log

A chronological record of operator actions and their stated reasons.

All recorded events

Connect to load audit events.

Settings

Configure this browser’s Admin API connection and the reason attached to write actions.

Connection

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

01

Action reason

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

02

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.