API reference
Base URL: https://www.paywise.asia/api/v1
This page describes what's actually implemented today. Payment methods are QRIS and USDT only. There is currently no live payment provider connected — transactions you create are recorded as pending and stay that way until a provider integration exists to move them to paid or failed.
Every request — except the provider webhook endpoint — requires an API key, generated from your dashboard under API Keys.
The full key is shown once, at creation time, and never again — PAYWISE stores only a hash of it. Rate limit is 60 requests/minute per API key; exceeding it returns 429 RATE_LIMITED. Every response — success or error — carries an X-Request-Id header, also visible against the matching row in your API Logs dashboard.
Every API key belongs to exactly one environment, visible directly in its prefix:
Sandbox
pw_test_…
Transactions created with a sandbox key are tagged sandbox and never appear in your live reports or default dashboard view.
Live
pw_live_…
Transactions created with a live key are production transaction data and count toward your live dashboard stats, reports, and settlement records.
Switch which environment your dashboard is viewing with the Live/Sandbox toggle in the sidebar. This is a display preference only — the environment a transaction is written into is always determined by which key authenticated the request, never by anything a client can set directly.
Every response is one of these two shapes — never anything else.
Success
{ "success": true, "data": { ... } }
Error
{ "success": false, "error": { "code": "INVALID_API_KEY", "message": "Invalid API credentials." } }
| HTTP | Code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY | Missing, malformed, or unrecognized API key |
| 401 | API_KEY_REVOKED | The key was revoked from the dashboard |
| 403 | MERCHANT_INACTIVE | The merchant account is suspended |
| 404 | NOT_FOUND | Resource doesn't exist, or belongs to another merchant |
| 409 | DUPLICATE_REFERENCE | Same merchant_reference reused with a different amount/method |
| 422 | VALIDATION_ERROR | Request body failed validation |
| 429 | RATE_LIMITED | Too many requests |
| 501 | PROVIDER_NOT_CONNECTED | Inbound provider webhook — no real provider integration exists |
POST /api/v1/transactions
| Field | Type | Required | Notes |
|---|---|---|---|
| merchant_reference | string | yes | Your own order ID. Doubles as the idempotency key. |
| amount | number | yes | Minimum 0.01 |
| payment_method | string | yes | QRIS (IDR) or USDT (BEP20) only |
| currency | string | no | Optional. Must match the rail: IDR for QRIS, USDT for USDT. Stored as-is — never converted. |
| network | string | no | USDT only, required: BEP20. TRC20/ERC20 are rejected. Omit for QRIS. |
Request
{
"merchant_reference": "ORDER-1042",
"amount": 250000,
"currency": "IDR",
"payment_method": "QRIS"
}
{
"merchant_reference": "ORDER-1043",
"amount": 100,
"currency": "USDT",
"payment_method": "USDT",
"network": "BEP20"
}
Response — 201 Created
{
"success": true,
"data": {
"reference": "PW20260921143022AB3F",
"merchant_reference": "ORDER-1042",
"payment_method": "QRIS",
"environment": "sandbox",
"amount": 250000,
"currency": "IDR",
"network": null,
"status": "pending",
"created_at": "2026-09-21T14:30:22+00:00",
"paid_at": null
}
}
Idempotency: retrying with the same merchant_reference and the same amount/method returns the original transaction with 200 OK instead of creating a duplicate. Reusing the reference with a different amount or method returns 409 DUPLICATE_REFERENCE. Sandbox and live keys are separate: the same merchant_reference can be used once in each, and a key never sees the other environment's transactions.
GET /api/v1/transactions/{reference}
{reference} is the PAYWISE-generated reference returned when you created the transaction — not your own merchant_reference, and never a raw database ID.
You can only ever retrieve your own merchant's transactions. Requesting another merchant's reference returns 404 NOT_FOUND — never a 403, which would confirm the reference exists at all.
The same values are used consistently across the database, this API, the merchant dashboard, and the admin dashboard.
Configure a webhook URL and events from Dashboard → Webhooks. When a transaction's status changes to a subscribed event, PAYWISE sends:
POST <your webhook_url> Content-Type: application/json X-PAYWISE-Signature: <hex-encoded HMAC-SHA256>
Verify the signature by computing HMAC-SHA256(raw_json_body, your_webhook_secret) (shown on the Webhooks page) and comparing it to the X-PAYWISE-Signature header. Events: payment.paid · payment.failed · payment.expired.
Delivery only actually fires once a real provider integration exists to move a transaction out of pending. The delivery mechanism itself is real and records actual HTTP response codes, never an assumed success — every attempt is logged on your Webhooks page, including the event, attempt number, actual response status (or nothing, if the request timed out), and up to 2KB of your endpoint's response body.
POST /api/v1/webhooks/provider/{provider}
Reserved for QRIS/USDT provider callbacks. Currently returns 501 PROVIDER_NOT_CONNECTED for every request — there is no provider integration wired in yet, so there is no signature to verify, and this endpoint deliberately refuses to trust an unverified payload rather than risk marking a transaction paid incorrectly.