PAYWISE
en

API reference

PAYWISE Public API — v1

Accept QRIS and USDT payments with one REST API. A transaction is created as pending and becomes paid only after the payment is confirmed server to server. Sandbox payments are never real: you choose their outcome yourself.

Base URL https://paywise.asia/api/v1

Quick start

  1. 1

    Get an API key

    Create a sandbox key (pw_test_…) under API Keys and send it as a Bearer token.

  2. 2

    Create a payment

    POST /api/v1/transactions with your order ID and amount, then show the QR code or open checkout_url.

  3. 3

    Confirm with the webhook

    Fulfil the order when the signed payment.paid webhook reaches your server.

Authentication

Every request, except the provider webhook endpoint, needs an API key. Create one in your dashboard under API Keys.

HTTP header
Authorization: Bearer pw_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • The full key is shown only once, when it is created. PAYWISE stores only its hash. The rate limit is 60 requests per minute per API key; above that the API returns 429 RATE_LIMITED. Every response, success or error, has an X-Request-Id header, which also appears on the matching row in your API logs.
  • Optionally limit each key to your servers' IP addresses or CIDR ranges on the API Keys page. A request from any other address returns 403 IP_NOT_ALLOWED, and you are notified.

Environments

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. To finish a sandbox payment, open its checkout_url and choose Paid, Failed or Expired: your webhook receives the same event a live payment sends.

Live pw_live_…

Transactions made with a live key are real and count in your dashboard statistics, reports and settlements.

Use the Live/Sandbox switch in the sidebar to choose which environment the dashboard shows. This only changes the view: the environment of a transaction is always set by the key that made the request.

Response envelope

Every response has one of these two formats.

Success
{
    "success": true,
    "data": {
        "…": "…"
    }
}
Error
{
    "success": false,
    "error": {
        "code": "INVALID_API_KEY",
        "message": "Invalid API credentials."
    }
}

Error codes

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 IP_NOT_ALLOWED The API key has an allowed-IP list and the request came from another IP
403 MERCHANT_INACTIVE The merchant account is suspended
404 NOT_FOUND Resource doesn't exist, or belongs to another merchant
405 METHOD_NOT_ALLOWED The endpoint does not accept this HTTP method
409 DUPLICATE_REFERENCE Same merchant_reference reused with a different amount/method
422 VALIDATION_ERROR Request body failed validation
422 RETURN_URL_NOT_ALLOWED return_url is not https, or not on one of your active websites
422 WEBSITE_NOT_FOUND website is not one of your active websites
429 RATE_LIMITED Too many requests
502 PROVIDER_ERROR Live QRIS or USDT: the provider refused or returned an invalid response; the transaction is marked failed
503 PROVIDER_NOT_CONNECTED Live QRIS or USDT: the rail's provider is not connected, so no payment can be created
504 PROVIDER_TIMEOUT Live QRIS or USDT: the provider did not respond in time; the transaction is marked failed

Payments

Create a transaction

POST /api/v1/transactions
Field Notes
merchant_reference string · required Your own order ID. Doubles as the idempotency key.
amount number · required QRIS: whole rupiah, 1,000–10,000,000 IDR. USDT: at least 1, up to 2 decimals
payment_method string · required QRIS (IDR) or USDT (BEP20) only
customer_id string · required Required (max 100). Your own id for the customer: user id, username or email. Shown in your dashboard, exports, API responses and webhooks, so you can see who paid. Also passed to the payment provider as the customer label, unless it contains "@" (emails are never sent).
currency string · optional Optional. Must match the method: IDR for QRIS, USDT for USDT. Stored as sent, without conversion.
network string · optional USDT only, required: BEP20. TRC20/ERC20 are rejected. Omit for QRIS.
customer_name string · optional Optional (max 50), any method. The payer's name, shown in your dashboard. Passed to the provider instead of customer_id when customer_id is an email.
return_url string · optional Optional. Overrides the website's return URL for this payment. https, on one of your active websites (Dashboard → Websites & webhook).
website string · optional Optional. The domain of one of your websites (e.g. shop.example.com); its return URL is used. Omit both to use your default website. A mirror domain counts as its website: the return URL and a webhook URL on the website's domain move to that mirror.
expires_in integer · optional Optional, QRIS only. How long the QR code stays payable, in seconds: 60 to 1,200 (the default).
Request · QRIS
{
    "merchant_reference": "ORDER-1042",
    "amount": 250000,
    "currency": "IDR",
    "payment_method": "QRIS",
    "customer_id": "user-1042",
    "customer_name": "Budi Santoso"
}
Request · USDT
{
    "merchant_reference": "ORDER-1043",
    "amount": 100,
    "currency": "USDT",
    "payment_method": "USDT",
    "network": "BEP20",
    "customer_id": "user-1043"
}
Show the full response (Response — 201 Created)
Response — 201 Created
{
    "success": true,
    "data": {
        "reference": "PW20260921143022AB3F",
        "merchant_reference": "ORDER-1042",
        "payment_method": "QRIS",
        "environment": "sandbox",
        "customer_id": "user-1042",
        "customer_name": "Budi Santoso",
        "amount": 250000,
        "fee_bearer": "merchant",
        "currency": "IDR",
        "network": null,
        "status": "pending",
        "created_at": "2026-09-21T14:30:22+00:00",
        "paid_at": null,
        "qr_string": "SANDBOX-QRIS-PW20260921143022AB3F-NOT-PAYABLE",
        "pay_address": null,
        "pay_amount": 250000,
        "expires_at": "2026-09-21T14:50:22+00:00",
        "checkout_url": "https://checkout.paywise.asia/Xk7p2QaZ9mRt"
    }
}

USDT: show your customer pay_address (a BEP20 address) and pay_amount, the exact USDT to send. pay_amount is amount plus the network and processing fee, which the customer pays, so you receive the full amount. The address stays open for 60 minutes (expires_at); a deposit that arrives later is still confirmed. A payment short of pay_amount is not marked paid.

Who pays the transaction fee is set per website under Dashboard → Websites & webhook. With fee_bearer customer, the customer pays pay_amount (the amount plus the fee: render the QR string or show this amount) and you receive exactly amount; the webhook and your dashboard show amount. Tell your customers the price includes a service fee. Otherwise pay_amount equals amount for QRIS. A change applies to new payments only.

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.

Hosted payment page

Turn on the hosted payment page for a website under Dashboard → Websites & webhook. A transaction for that website (named in website, matched by return_url, or your default website) comes with a checkout_url (null otherwise): a PAYWISE-hosted page that shows your customer the QR code (QRIS) or the deposit address and exact amount (USDT), a countdown, and the payment status, updating by itself. Redirect your customer there, or open it in a new tab. You can still render qr_string or pay_address yourself instead.

After the payment is paid, expired or failed, the page sends the customer back to the return URL you set for your website under Dashboard → Websites & webhook (the default website, or the one named in website). Send return_url to override it for one payment: it must be https and on one of those domains (or a subdomain), otherwise 422 RETURN_URL_NOT_ALLOWED. PAYWISE appends the outcome to it:

Return URL
https://shop.example.com/thanks?reference=PW20260921143022AB3F&merchant_reference=ORDER-1042&status=paid
The status in that URL is only for your page's message. Anyone can type it, so never fulfil an order on it: wait for the payment.paid webhook or check with GET /api/v1/transactions/{reference}.

Retrieve a transaction

GET /api/v1/transactions/{reference}

{reference} is the PAYWISE reference returned when you created the transaction, not your own merchant_reference.

You can only retrieve your own transactions. Another merchant's reference returns 404 NOT_FOUND, not 403, so it does not reveal that the reference exists.

List transactions

GET /api/v1/transactions?status=paid&date_from=2026-09-01&date_to=2026-09-30

Your transactions in the key's environment, newest first: 20 per page, up to 100 with per_page. All filters are optional and can be combined. Each item is the same object as GET /api/v1/transactions/{reference} returns.

Field Notes
status pending, paid, failed or expired.
payment_method QRIS or USDT.
date_from YYYY-MM-DD. First day (UTC+7) the transaction was created.
date_to YYYY-MM-DD. Last day (UTC+7), inclusive.
merchant_reference Your order reference, exact match.
customer_id Your customer id, exact match.
per_page 1–100, default 20.
page Page number, default 1.
Response — 200 OK
{
    "success": true,
    "data": [
        {
            "reference": "PW20260921143022AB3F",
            "merchant_reference": "ORDER-1042",
            "payment_method": "QRIS",
            "status": "paid",
            "amount": 250000,
            "currency": "IDR",
            "…": "…"
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 20,
        "total": 1,
        "last_page": 1
    }
}

meta gives the current page, the page size, the total number of matching transactions and the last page.

Transaction statuses

Pending Paid Failed Expired

The same values are used consistently across the database, this API, the merchant dashboard, and the admin dashboard.

Withdrawals

Withdrawals

POST /api/v1/withdrawals

Request a withdrawal of your settled funds from your server: the same request as Dashboard → Withdrawals, with the same minimum, maximum, fee and available funds, and the same automatic payout. Only a live key can request one (a sandbox key gets 403 LIVE_KEY_REQUIRED); there is no email code, so the live key alone authorises the payout. Keep it on your server and consider an allowed-IP list on the API Keys page.

amount is what arrives at the destination. The withdrawal fee (Rp3.000 for QRIS, 1.00 USDT for USDT) is added on top and taken from your available funds; the response shows amount (taken), fee and payout_amount (sent). Limits per withdrawal: Rp25.000 – Rp20.000.000 for QRIS, 10.00 USDT – 10,000.00 USDT for USDT.

Field Notes
payment_method Required. Which funds to withdraw: QRIS (paid out to a bank account in IDR) or USDT (sent to a BEP20 wallet).
amount Required. What arrives at the destination, in the rail's currency (up to 2 decimals).
bank_code QRIS only, required. A code from the list below: 3 digits for a bank, or DANA, OVO, GOPAY or SHOPEEPAY for an e-wallet (account_number is then the 08… phone number). An e-wallet takes at most IDR 10,000,000 per withdrawal and has no owner name check: a wrong number is still paid.
account_number QRIS only, required. 5 to 30 digits.
wallet_address USDT only, required. A BEP20 address: 0x followed by 40 hex characters.
network USDT only, required: BEP20.
Bank codes (124)
  • 002 BRI
  • 008 MANDIRI
  • 009 BNI
  • 011 DANAMON
  • 013 PERMATABANK
  • 014 BCA
  • 016 MAYBANK
  • 019 PANIN BANK
  • 022 CIMB NIAGA
  • 023 UOB
  • 028 OCBC
  • 031 CITIBANK
  • 032 JPMORGAN
  • 033 BANK OF AMERICA
  • 036 PT. BANK WINDU KENTJANA INT,TBK
  • 037 ARTHA GRAHA
  • 040 BANGKOK BANK
  • 041 HSBC
  • 042 MUFG
  • 046 DBS
  • 047 PT. BANK RESONA PERDANIA
  • 048 MIZUHO
  • 050 STANDARD CHARTERED
  • 052 ROYAL BANK OF SCOTLAND N.V.
  • 054 BANK CAPITAL
  • 057 BNP PARIBAS
  • 059 PT. BANK KEB INDONESIA
  • 061 ANZ
  • 067 DEUTSCHE BANK
  • 068 WOORI
  • 069 BANK OF CHINA
  • 076 BANK BUMI ARTA
  • 087 BANK EKONOMI
  • 088 PT. BANK ANTARDAERAH
  • 089 PT. BANK INTERIM INDONESIA
  • 095 J TRUST BANK
  • 097 MAYAPADA
  • 110 BJB
  • 111 BANK DKI
  • 112 BANK BPD DIY
  • 113 BANK JATENG
  • 114 BANK JATIM
  • 115 BANK JAMBI
  • 116 BANK ACEH
  • 117 BANK SUMUT
  • 118 BANK NAGARI
  • 119 BANK RIAU KEPRI
  • 120 BANK SUMSEL BABEL
  • 121 BANK LAMPUNG
  • 122 BANK KALSEL
  • 123 BANK KALBAR
  • 124 BANK KALTIMTARA
  • 125 BANK KALTENG
  • 126 BANK SULSELBAR
  • 127 BANK SULUTGO
  • 128 BANK NTB
  • 129 BANK BPD BALI
  • 130 BANK NTT
  • 131 BANK MALUKU
  • 132 BANK PAPUA
  • 133 BANK BENGKULU
  • 134 BANK SULTENG
  • 135 BANK SULTRA
  • 145 PT. BANK NUSANTARA PARAHYANGAN
  • 146 BANK OF INDIA
  • 147 BANK MUAMALAT
  • 151 BANK MESTIKA
  • 152 PT. BANK METRO EXPRESS
  • 153 BANK SINARMAS
  • 157 BANK MASPION
  • 161 BANK GANESHA
  • 164 ICBC
  • 167 QNB
  • 200 BTN
  • 212 PT. BANK HIMPUNAN SAUDARA 1906, TBK.
  • 213 BTPN
  • 405 BANK VICTORIA SYARIAH
  • 422 PT. BANK SYARIAH BRI
  • 425 BJB SYARIAH
  • 426 BANK MEGA
  • 427 PT. BANK BNI SYARIAH
  • 441 KB BANK
  • 451 BSI
  • 459 PT. BANK BISNIS INTERNASIONAL
  • 466 PT. BANK ANDARA
  • 472 BANK SAQU
  • 484 HANA BANK
  • 485 PT. BANK ICB BUMIPUTERA, TBK.
  • 490 BNC
  • 491 PT. BANK MITRA NIAGA
  • 494 BANK RAYA
  • 498 SBI INDONESIA
  • 501 BLU BCA DIGITAL
  • 503 NOBU BANK
  • 506 PT. BANK SYARIAH MEGA INDONESIA
  • 513 BANK INA
  • 517 PANIN DUBAI SYARIAH
  • 520 PT. PRIMA MASTER BANK
  • 521 KB BANK SYARIAH
  • 523 BANK SAMPOERNA
  • 526 PT. LIMAN INTERNASIONAL BANK
  • 531 PT. ANGLOMAS INTERNATIONAL BANK
  • 535 SEABANK
  • 536 BCA SYARIAH
  • 542 BANK JAGO
  • 547 BTPN SYARIAH
  • 548 BANK MAS
  • 553 BANK MAYORA
  • 555 BANK INDEX
  • 558 PT. BANK PUNDI INDONESIA, TBK.
  • 559 PT. CENTRATAMA NASIONAL BANK
  • 562 SUPERBANK
  • 564 PT. BANK SINAR HARAPAN BALI
  • 566 BANK VICTORIA
  • 567 ALLO BANK
  • 945 IBK
  • 947 MAYBANK SYARIAH
  • 949 PT. BANK CHINATRUST INDONESIA
  • 950 COMMONWEALTH BANK
  • 911 LINKAJA
  • DANA DANA
  • OVO OVO
  • GOPAY GOPAY
  • SHOPEEPAY SHOPEEPAY
Idempotency-Key header (required): 8 to 64 characters, one per withdrawal (e.g. your payout id). Sending the same key with the same details returns the original withdrawal with 200, so a timed-out request is safe to retry. The same key with a different amount, method or destination returns 409 DUPLICATE_REQUEST.
Request · cURL
curl -X POST https://paywise.asia/api/v1/withdrawals \
  -H "Authorization: Bearer pw_live_…" \
  -H "Idempotency-Key: payout-2026-09-27-001" \
  -H "Content-Type: application/json" \
  -d '{"payment_method":"QRIS","amount":1500000,"bank_code":"014","account_number":"1234567890"}'
Show the full response (Response — 201 Created)
Response — 201 Created
{
    "success": true,
    "data": {
        "reference": "PWD20260927090112C4DQ",
        "payment_method": "QRIS",
        "environment": "live",
        "payout_method": "bank_transfer",
        "currency": "IDR",
        "network": null,
        "amount": 1503000,
        "fee": 3000,
        "payout_amount": 1500000,
        "status": "requested",
        "destination": {
            "bank_code": "014",
            "bank_name": "BCA",
            "account_number": "1234567890"
        },
        "reason": null,
        "requested_at": "2026-09-27T02:01:12+00:00",
        "completed_at": null,
        "closed_at": null
    }
}

422 INSUFFICIENT_FUNDS means the amount plus the fee is more than your settled, not yet withdrawn funds of that method; error.available is the largest amount you can request now. QRIS funds settle in batches a few times a day; USDT funds settle when paid.

A withdrawal starts as requested and then moves through approved and processing to completed, or ends failed or rejected (with reason). Follow it with the withdrawal.* webhooks or with the endpoints below.

GET /api/v1/withdrawals

Your withdrawals in the key's environment, newest first, with the same paging and meta as the transaction list. Filters (all optional): status (requested, approved, processing, completed, failed, rejected), payment_method, date_from and date_to (YYYY-MM-DD, UTC+7).

GET /api/v1/withdrawals/{reference}

{reference} is the PWD… reference returned when the withdrawal was requested. Another merchant's withdrawal, or one from the other environment, returns 404 NOT_FOUND.

Available funds

GET /api/v1/balance

Your funds per payment method in the key's environment, as on the Withdrawals page: available to withdraw now, waiting for the next settlement batch, the most one withdrawal can pay out now (max_withdrawal) and each withdrawal's limits and fee.

Response — 200 OK
{
    "success": true,
    "data": {
        "environment": "live",
        "methods": {
            "QRIS": {
                "currency": "IDR",
                "available": 1250000,
                "awaiting_settlement": 480000,
                "next_settlement_at": "2026-09-28T08:00:00+00:00",
                "next_settlement_amount": 480000,
                "max_withdrawal": 1245000,
                "withdrawal": {
                    "min": 25000,
                    "max": 20000000,
                    "fee": 5000
                }
            },
            "USDT": {
                "…": "…"
            }
        }
    }
}

Webhooks

Webhooks

PAYWISE → you

Configure a webhook URL and events from Dashboard → Websites & webhook. When a transaction's status changes to a subscribed event, PAYWISE sends:

HTTP
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) (the secret is shown on the Websites & webhook page; one secret for every webhook) and comparing it to the X-PAYWISE-Signature header. Events: payment.paid · payment.failed · payment.expired.

Webhooks are sent from 217.217.253.90 · 2407:3640:2358:7339::1. If your firewall only accepts known addresses, allow these; still verify the signature on every delivery. Changes to this list are announced in advance.

Several websites on different servers? Give a website its own webhook URL under Dashboard → Websites & webhook. Payments for that website (the one named in website, matched by return_url, or your default website) are then sent there; everything else still goes to your main webhook URL. All of them are signed with the same secret. A website that got its own secret earlier keeps it until you switch it to the main secret on that page.

Every delivery attempt is logged on the Websites & webhook page with the event, the attempt number, the real HTTP status your endpoint returned (empty on a timeout) and up to 2 KB of its response. Failed deliveries are retried automatically, and you can resend one from the transaction page.

Withdrawals

Withdrawal events (withdrawal.completed · withdrawal.failed · withdrawal.rejected) go to your main webhook URL when a withdrawal is completed, fails or is rejected. A failed or rejected amount can be withdrawn again. Subscribe on the Websites & webhook page. reason gives the reason for a failure or rejection and is null when completed.

Show the full response (withdrawal.completed)
withdrawal.completed
{
    "event": "withdrawal.completed",
    "data": {
        "reference": "PWD20260925090112C4DQ",
        "payment_method": "QRIS",
        "environment": "live",
        "payout_method": "bank_transfer",
        "currency": "IDR",
        "network": null,
        "amount": 1500000,
        "fee": 5000,
        "payout_amount": 1495000,
        "status": "completed",
        "reason": null,
        "requested_at": "2026-09-25T02:01:12+00:00",
        "completed_at": "2026-09-25T02:03:40+00:00",
        "closed_at": null
    }
}
Provider → PAYWISE · Internal: used by PAYWISE's payment network
POST /api/v1/webhooks/provider/{provider}

Used internally by PAYWISE's payment network, not by merchants. Every inbound payment notification is authenticated (secret token or HMAC signature), matched to one live transaction, and then confirmed server to server; the notification's own status never marks a payment paid. A rail that is not available returns 503 PROVIDER_NOT_CONNECTED.