BRPAY Merchant API Integration Guide
API: Merchant API · Java / PHP SDK: see the delivery package for its version · Updated: 2026-11-06
1. Quick start
Use this API for game recharges and withdrawals. Orders, balances, ledger entries and callbacks use BRC. The platform values 1 BRC at 1 BRL; this API transfers BRC between BRPAY wallets. Domains, accounts, quantities and fees in this guide are examples.
Ask BRPAY for these server-side credentials and settings:
| Parameter | Description |
|---|---|
baseUrl |
Merchant API HTTPS origin, without a path, query or trailing slash. Use the origin supplied by BRPAY. |
merchantId |
Your assigned merchant number: uppercase M + 8 uppercase letters or digits. |
signingSecret |
32–512 UTF-8 bytes. Keep it on your server for API signing and callback verification. |
- Get set up. Ask BRPAY for the three settings above and provide
notifyUrlandreturnUrlin each order request as needed. - Check your balance first. Synchronize your server clock and run the signed request example. Once you get
availableBalance, test orders in your test environment. - Try each flow. Recharge: save your order → create a PAYIN → open
cashierUrl→ confirm payment by query or verified callback before crediting the game account. Withdrawal: save your order → verify the recipient wallet → create a PAYOUT → confirm receipt onSUCCESS. - Try three failure cases. An expired PAYIN is never credited. A repeated callback is handled once. After a submission timeout, query the original order number first.
| Function | Path | SDK method |
|---|---|---|
| Create PAYIN | /api/merchant/orders/payin |
payin(...) |
| Create PAYOUT | /api/merchant/orders/payout |
payout(...) |
| Query order | /api/merchant/orders/query |
queryOrder(...) / querySystemOrder(...) |
| Query availability | /api/merchant/orders/availability |
queryAvailability(...) |
| Query merchant balance | /api/merchant/account/balance |
queryBalance(...) |
| Query merchant transactions | /api/merchant/account/transactions |
queryTransactions(...) |
| Look up wallet | /api/merchant/wallets/query |
queryWallet(...) |
2. Common protocol
2.1 Request format
| Item | Rule |
|---|---|
| Protocol / method | HTTPS / POST; loopback HTTP is allowed only for local development |
| Content-Type | application/json; charset=utf-8 |
| Encoding / body limit | UTF-8 / 8192 bytes; JSON object only; duplicate keys and unknown fields rejected |
| Quantities | Positive decimal string, at most 17 integer digits after leading zeros, exactly representable in cents. "00030.1200" means "30.12". No JSON numbers, exponents or fractional cents. |
| Time | UTC RFC3339 ending in Z, e.g. 2026-11-05T04:00:00Z |
| Evolution | Ignore added optional response/callback fields |
Every endpoint requires these headers:
| Header | Format | Description |
|---|---|---|
X-Merchant-Id |
Uppercase M followed by eight uppercase letters or digits | Platform merchant number |
X-Timestamp |
10-digit Unix seconds | Current time, with 300 seconds of clock tolerance |
X-Request-Id |
Lowercase UUID | Generate a new value for each HTTP request. Keep your business order number unchanged for the same order. |
X-Signature |
64 lowercase hex characters | HMAC-SHA256; duplicate authentication headers rejected |
2.2 Request signature
Sign on your server. Encode the JSON as UTF-8 and hash those bytes with SHA-256. Send the same bytes you hashed. Join the seven lines below with \n (LF), with no trailing newline:
BP-MERCHANT
POST
<full-request-path>
<merchantId>
<epochSeconds>
<requestId>
<sha256Hex(rawBodyBytes)><full-request-path> is the actual path, such as /api/merchant/orders/payin, without a host, query string or extra slash. Do not parse and reformat the JSON before hashing it or substitute Windows CRLF for LF in the signature input.
Runnable example: sign a read-only balance query
Save this as merchant-balance.mjs. Use Node.js with built-in fetch (18+), set BRPAY_BASE_URL, BRPAY_MERCHANT_ID and BRPAY_SIGNING_SECRET on the server, and run node merchant-balance.mjs. It prints the available BRC balance. Use the new API origin supplied by BRPAY; local port 8109 serves the DEMO pages.
import { createHash, createHmac, randomUUID } from 'node:crypto';
const { BRPAY_BASE_URL, BRPAY_MERCHANT_ID, BRPAY_SIGNING_SECRET } = process.env;
if (!BRPAY_BASE_URL || !/^M(?!0{8}$)[0-9A-Z]{8}$/.test(BRPAY_MERCHANT_ID ?? '') ||
!BRPAY_SIGNING_SECRET) {
throw new Error('Set BRPAY_BASE_URL, BRPAY_MERCHANT_ID and BRPAY_SIGNING_SECRET');
}
const origin = new URL(BRPAY_BASE_URL);
const loopback = origin.protocol === 'http:' &&
['127.0.0.1', 'localhost', '[::1]'].includes(origin.hostname);
if ((!loopback && origin.protocol !== 'https:') || origin.pathname !== '/' ||
origin.search || origin.hash || origin.username || origin.password) {
throw new Error('BRPAY_BASE_URL must be the API origin (HTTPS or loopback HTTP)');
}
const path = '/api/merchant/account/balance';
const body = Buffer.from(JSON.stringify({}), 'utf8');
const timestamp = String(Math.floor(Date.now() / 1000));
const requestId = randomUUID();
const bodyHash = createHash('sha256').update(body).digest('hex');
const text = ['BP-MERCHANT', 'POST', path, BRPAY_MERCHANT_ID,
timestamp, requestId, bodyHash].join('\n');
const signature = createHmac('sha256', BRPAY_SIGNING_SECRET).update(text, 'utf8').digest('hex');
const response = await fetch(origin.origin + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Merchant-Id': BRPAY_MERCHANT_ID,
'X-Timestamp': timestamp,
'X-Request-Id': requestId,
'X-Signature': signature
},
body,
redirect: 'error',
signal: AbortSignal.timeout(30_000)
});
const reply = await response.json();
if (response.status !== 200 || reply?.success !== true || reply.code !== 'OK' ||
reply.requestId !== requestId || reply.data?.currency !== 'BRC' ||
typeof reply.data.availableBalance !== 'string') {
throw new Error(`HTTP ${response.status}; code=${reply?.code ?? '(missing)'}; requestId=${requestId}`);
}
console.log(reply.data.availableBalance);Fixed signing values. Compare these with the body hash and signature produced by your own code.
signingSecret = example-only-signing-secret-32-bytes
BP-MERCHANT
POST
/api/merchant/orders/payin
M00000325
1793851200
aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
74cbca80326f6565ae3a2f5c16607f96b8f42d13e30b7c99290ef75c8a05c07a
X-Signature: 8c5374d38568c2c84d1ca15d101b6c902f32398f83a45f9edbc30ac45b7b5c8cComplete HTTP request for offline signing checks. Use the sample secret above. The body is the single line of JSON below, with no trailing newline. The fixed timestamp is for checking the signature only.
POST /api/merchant/orders/payin HTTP/1.1
Host: api.example.com
Content-Type: application/json; charset=utf-8
X-Merchant-Id: M00000325
X-Timestamp: 1793851200
X-Request-Id: aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
X-Signature: 8c5374d38568c2c84d1ca15d101b6c902f32398f83a45f9edbc30ac45b7b5c8c
Content-Length: 86
{"merchantOrderNo":"RECHARGE-001","orderAmount":"30.00","description":"Game recharge"}2.3 Response envelope
A successful response has HTTP 200, success: true and code: "OK". Each endpoint below shows a full response. Its business fields are under data. For an order request, HTTP 200 means the request was handled; read data.status to see whether funds moved.
On failure, success is false and data is null. Use the HTTP status and code in your program. message is for people to read. requestId echoes your request's trace ID (it may be null if omitted); share it and your order number with BRPAY when troubleshooting. timestamp is the UTC time when BRPAY built the response.
For example, an incorrect signature returns HTTP 401 / INVALID_SIGNATURE.
{
"success": false,
"code": "INVALID_SIGNATURE",
"message": "Invalid signature",
"requestId": "99999999-aaaa-4bbb-8ccc-dddddddddddd",
"timestamp": "2026-11-05T04:06:00Z",
"data": null
}3. Orders, fees and states
The order response, the payment result and callback delivery are separate. A new PAYIN normally returns PENDING and has not debited the payer. A PAYOUT returning SUCCESS has credited the recipient. notificationStatus describes delivery only; EXHAUSTED does not change the payment result.
| Flow | Customer BRC balance | Merchant BRC balance | How to confirm success |
|---|---|---|---|
| PAYIN | Debited by the order quantity at payment | Credited with the order quantity minus the fee | An order query returns SUCCESS, or a signed success callback is verified |
| PAYOUT | Credited with the order quantity | Debited by the order quantity plus the fee | A successful payout response, an order query returning SUCCESS, or a verified success callback |
Fee = the greater of the minimum fee and (order quantity × fee rate + fixed fee), rounded to two decimal places using HALF_UP. A PAYIN fee must be less than the order quantity. The fee is fixed when the order is created. Use Query availability to see current fees and allowed quantities.
Your order number identifies one business order and cannot be reused across PAYIN and PAYOUT. On ORDER_EXISTS, query the original order and check its direction, quantity and payout recipient. A conflict does not confirm that this request succeeded.
4. Endpoints
Merchant PAYIN orders use PENDING (awaiting or processing payment), SUCCESS (merchant payment committed), and FAILED (a confirmed terminal failure). failureCode is required for FAILED; expiry currently uses ORDER_EXPIRED. It is null for other states. Corrections use ORDER_CORRECTED. Initial terminal events: ORDER_SUCCEEDED or ORDER_FAILED. Wallet top-up success/failure, insufficient funds and cancellation of automatic-payment authorization do not terminate a merchant order that remains payable. A request timeout is not an order failure: query the original order. Rejected requests that create no order return an API error without an order callback.
4.1 Create PAYIN
POST /api/merchant/orders/payin · SDK payin(...)
Call this when a customer wants to top up a game account. Save both order numbers and send the customer to cashierUrl (or use appUrl to open the native app). Creating the order does not move funds. Credit the game account after verifying and matching an ORDER_SUCCEEDED callback, or confirming SUCCESS through the order query.
The complete minimal PAYIN body is {"merchantOrderNo":"RECHARGE-001","orderAmount":"30.00"}.
Callback URLs: Optionally provide notifyUrl and returnUrl in this request. Merchant profiles no longer configure a callback URL. See Result callbacks.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
merchantOrderNo |
string | Yes | Your reference for this business order. Use 1–64 case-sensitive characters matching [A-Za-z0-9][A-Za-z0-9._-]{0,63}. Use one number per business order across both PAYIN and PAYOUT. |
orderAmount |
string | Yes | Positive decimal string, at most 17 integer digits after leading zeros, exactly representable in cents. "00030.1200" means "30.12". No JSON numbers, exponents or fractional cents. |
description |
string / null | No | Optional business description. Omit it or send an empty string; omission is stored as an empty string. Up to 160 UTF-16 code units, with no ASCII control characters. null also means no description. |
notifyUrl |
string / null | No | Asynchronous notification URL; omitted means no notification. Full HTTPS URL, max 512 characters. |
returnUrl |
string / null | No | Synchronous merchant return page; omitted means no return action. Full HTTPS URL, max 512 characters. |
{
"merchantOrderNo": "RECHARGE-001",
"orderAmount": "30.00",
"description": "Game recharge",
"notifyUrl": "https://merchant.example.com/webhooks/brpay",
"returnUrl": "https://merchant.example.com/orders/RECHARGE-001"
}Success response data
A new order normally starts at PENDING. The example uses a 1% fee plus 0.10 BRC. Use expiresAt for the deadline; stop directing the customer to checkout after expiry.
| Response field | Type | Description |
|---|---|---|
orderNo |
string | BRPAY collection order number: RC + 14 digits. |
merchantOrderNo |
string | Your order number from the original request. |
direction |
string | Always PAYIN. |
currency |
string | Always BRC. |
orderAmount |
string | BRC the customer should pay. |
feeAmount |
string | Fee fixed at order creation, in BRC. Can be "0.00". |
merchantAmount |
string | Your net credit on success: orderAmount - feeAmount, in BRC. |
actualAmount |
string / null | null while awaiting payment; the BRC actually debited from the customer on success. |
status |
string | Starts at PENDING (awaiting payment). Confirm the outcome by query or callback. |
failureCode |
string / null | Failure reason; required for FAILED. Current expiry code: ORDER_EXPIRED. Null otherwise. |
walletId |
string / null | null while awaiting payment; payer's wallet account (W + 8 uppercase letters or digits) on success. |
createdAt |
string | Order creation time in UTC. |
expiresAt |
string | Payment deadline in UTC. The order cannot be paid after expiry. |
completedAt |
string / null | null while pending; query the order for its terminal timestamp. |
orderRevision |
integer | Result revision, initially 0. Each correction increments it. |
notificationStatus |
string / null | null while awaiting payment; no result callback exists yet. |
cashierUrl |
string | BRPAY checkout URL for Web payment, opening the app, or scanning the checkout QR code in the app. |
appUrl |
string | Native app payment link: brazilpay://cashier/<token>. The 22-character Base64URL token matches cashierUrl. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
"timestamp": "2026-11-05T04:00:00Z",
"data": {
"orderNo": "RC00000000128364",
"merchantOrderNo": "RECHARGE-001",
"direction": "PAYIN",
"currency": "BRC",
"orderAmount": "30.00",
"feeAmount": "0.40",
"merchantAmount": "29.60",
"actualAmount": null,
"status": "PENDING",
"walletId": null,
"createdAt": "2026-11-05T04:00:00Z",
"expiresAt": "2026-11-05T04:30:00Z",
"completedAt": null,
"orderRevision": 0,
"notificationStatus": null,
"cashierUrl": "https://payments.example.com/pay/aaaaaaaaaaaaaaaaaaaaaA",
"appUrl": "brazilpay://cashier/aaaaaaaaaaaaaaaaaaaaaA",
"failureCode": null
}
}If the request fails: On ORDER_EXISTS, timeout or an invalid receipt, query the original merchant order number first. For an invalid quantity, check the allowed range and fees.
4.2 Create PAYOUT
POST /api/merchant/orders/payout · SDK payout(...)
Call this to pay a withdrawal into a BRPAY wallet. First look up the wallet, require exists=true and canReceive=true, and confirm the intended recipient. HTTP 200 means the order was accepted as PENDING and the principal plus fee was reserved. Only SUCCESS confirms receipt; FAILED releases the hold. Rejected requests that create no order reserve no funds.
Callback URLs: Optionally provide notifyUrl and returnUrl in this request. Merchant profiles no longer configure a callback URL. See Result callbacks.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
merchantOrderNo |
string | Yes | Your reference for this business order. Use 1–64 case-sensitive characters matching [A-Za-z0-9][A-Za-z0-9._-]{0,63}. Use one number per business order across both PAYIN and PAYOUT. |
orderAmount |
string | Yes | Positive decimal string, at most 17 integer digits after leading zeros, exactly representable in cents. "00030.1200" means "30.12". No JSON numbers, exponents or fractional cents. |
recipientWalletId |
string | Yes | Use walletId from wallet lookup: W + 8 uppercase letters or digits. |
description |
string / null | No | Optional business description. Omit it or send an empty string; omission is stored as an empty string. Up to 160 UTF-16 code units, with no ASCII control characters. null also means no description. |
notifyUrl |
string / null | No | Asynchronous notification URL; omitted means no notification. Full HTTPS URL, max 512 characters. |
returnUrl |
string / null | No | Synchronous merchant return page; omitted means no return action. Full HTTPS URL, max 512 characters. |
{
"merchantOrderNo": "WITHDRAW-001",
"orderAmount": "20.00",
"description": "Game withdrawal",
"recipientWalletId": "W00012836",
"notifyUrl": "https://merchant.example.com/webhooks/brpay",
"returnUrl": "https://merchant.example.com/orders/WITHDRAW-001"
}Success response data
The example uses a 2% fee plus 0.20 BRC. Acceptance returns PENDING; the example below is a final successful query. A confirmed SUCCESS is enough to confirm receipt; no need to wait for a callback. PAYOUT does not return cashierUrl or appUrl.
| Response field | Type | Description |
|---|---|---|
orderNo |
string | BRPAY payout order number: PY + 14 digits. |
merchantOrderNo |
string | Your order number from the original request. |
direction |
string | Always PAYOUT. |
currency |
string | Always BRC. |
orderAmount |
string | BRC the recipient should receive. |
feeAmount |
string | Fee fixed at order creation, in BRC. Can be "0.00". |
merchantAmount |
string | The amount debited from your balance: orderAmount + feeAmount, in BRC. |
actualAmount |
string / null | Credited principal on SUCCESS; null on PENDING or FAILED. |
status |
string | PENDING accepted and processing; SUCCESS credited; FAILED failed and hold released. |
failureCode |
string / null | Failure reason on FAILED; null otherwise. |
walletId |
string | Designated recipient wallet in every state (W + 8 uppercase letters or digits), matching recipientWalletId. Only SUCCESS confirms credit. |
createdAt |
string | Order creation time in UTC. |
expiresAt |
null | Always null. There is no payout payment deadline. |
completedAt |
string / null | Terminal completion time in UTC; null on PENDING. |
orderRevision |
integer | Result revision, initially 0. Each correction increments it. |
notificationStatus |
string / null | PENDING awaiting delivery or retry, DELIVERED acknowledged with 2xx, or EXHAUSTED after automatic attempts end. Independent of payment status. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "22222222-3333-4444-8555-666666666666",
"timestamp": "2026-11-05T04:05:00Z",
"data": {
"orderNo": "PY00000000128365",
"merchantOrderNo": "WITHDRAW-001",
"direction": "PAYOUT",
"currency": "BRC",
"orderAmount": "20.00",
"feeAmount": "0.60",
"merchantAmount": "20.60",
"actualAmount": "20.00",
"status": "SUCCESS",
"walletId": "W00012836",
"createdAt": "2026-11-05T04:05:00Z",
"expiresAt": null,
"completedAt": "2026-11-05T04:05:00Z",
"failureCode": null,
"orderRevision": 0,
"notificationStatus": "PENDING"
}
}If the request fails: For WALLET_NOT_FOUND, ACCOUNT_DISABLED or WALLET_LIMIT_EXCEEDED, check the recipient wallet. For MERCHANT_INSUFFICIENT_BALANCE, check your balance. On ORDER_EXISTS, timeout or an invalid receipt, query the original order number and confirm the direction, quantity and recipient.
4.3 Query order
POST /api/merchant/orders/query · SDK queryOrder(...) (merchant order number) / querySystemOrder(...) (BRPAY order number)
Look up an order belonging to the authenticated merchant by your reference or the BRPAY order number. Use this when a callback is missing, after an order request times out, or for reconciliation. Read status for the payment outcome and notificationStatus for callback delivery.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
merchantOrderNo |
string | One of the two | Original merchant order number: 1–64 case-sensitive characters matching [A-Za-z0-9][A-Za-z0-9._-]{0,63}. |
orderNo |
string | One of the two | BRPAY order number: RC for PAYIN or PY for PAYOUT, followed by 14 digits. |
Send one of these two request bodies:
{
"merchantOrderNo": "RECHARGE-001"
}{
"orderNo": "RC00000000128364"
}Success response data
The example shows a paid PAYIN with an acknowledged callback. All fields below are present except the two checkout links, which are returned only for PAYIN. A terminal PAYIN can still carry the links; use them only while PENDING.
| Response field | Type | Description |
|---|---|---|
orderNo |
string | BRPAY order number: RC for PAYIN or PY for PAYOUT, followed by 14 digits. |
merchantOrderNo |
string | Your order number from the original request. |
direction |
string | PAYIN or PAYOUT. |
currency |
string | Always BRC. |
orderAmount |
string | Original order quantity in BRC. |
feeAmount |
string | Fee fixed at order creation, in BRC. Can be "0.00". |
merchantAmount |
string | Positive BRC quantity: net credit on PAYIN success or total PAYOUT debit including the fee. |
actualAmount |
string / null | On SUCCESS, BRC actually debited from the customer (PAYIN) or credited to the recipient (PAYOUT); otherwise null. |
status |
string | PAYIN: PENDING (awaiting payment), SUCCESS (paid), or FAILED. PAYOUT: PENDING (processing), SUCCESS (credited), or FAILED (failed and hold released). |
failureCode |
string / null | Failure reason; required for FAILED. Current expiry code: ORDER_EXPIRED. Null otherwise. |
walletId |
string / null | PAYOUT: designated recipient wallet in every state. PAYIN: actual payer when known (a corrected failure may retain the payer), otherwise null. Format: W + 8 uppercase letters or digits. |
createdAt |
string | Order creation time in UTC. |
expiresAt |
string / null | PAYIN payment deadline in UTC; null for PAYOUT. |
completedAt |
string / null | Completion time in UTC on success; equal to expiresAt on expiry; null while pending. |
orderRevision |
integer | Result revision, initially 0. Each correction increments it. |
notificationStatus |
string / null | PENDING awaiting delivery or retry, DELIVERED acknowledged with 2xx, EXHAUSTED after automatic attempts end, or null if no notification record exists yet. |
cashierUrl |
string | PAYIN only. BRPAY checkout URL; use only while awaiting payment. |
appUrl |
string | PAYIN only. Native app payment link with the same token as cashierUrl. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "33333333-4444-4555-8666-777777777777",
"timestamp": "2026-11-05T04:03:00Z",
"data": {
"orderNo": "RC00000000128364",
"merchantOrderNo": "RECHARGE-001",
"direction": "PAYIN",
"currency": "BRC",
"orderAmount": "30.00",
"feeAmount": "0.40",
"merchantAmount": "29.60",
"actualAmount": "30.00",
"status": "SUCCESS",
"walletId": "W00012836",
"createdAt": "2026-11-05T04:00:00Z",
"expiresAt": "2026-11-05T04:30:00Z",
"completedAt": "2026-11-05T04:02:00Z",
"orderRevision": 0,
"notificationStatus": "DELIVERED",
"cashierUrl": "https://payments.example.com/pay/aaaaaaaaaaaaaaaaaaaaaA",
"appUrl": "brazilpay://cashier/aaaaaaaaaaaaaaaaaaaaaA",
"failureCode": null
}
}No order found: HTTP 404 / ORDER_NOT_FOUND. If recovering from a timed-out order request, follow Unconfirmed outcomes; a single 404 does not rule out a request that is still processing.
Full response when the order is missing · HTTP 404
{
"success": false,
"code": "ORDER_NOT_FOUND",
"message": "Record not found",
"requestId": "33333333-4444-4555-8666-888888888888",
"timestamp": "2026-11-05T04:06:10Z",
"data": null
}4.4 Query availability
POST /api/merchant/orders/availability · SDK queryAvailability(...)
Check whether orders are currently accepted and get the allowed BRC range and fees. The check reserves no capacity and does not check your actual balance or the recipient wallet's capacity. During maintenance, new orders are paused; existing orders and accounts remain queryable.
The allowed range intersects fee and storage requirements, the merchant's single-order limits, and its remaining daily quota. Collections and payouts each count the gross BRC amount of accepted orders in the Sao Paulo calendar day. Unpaid and expired orders still count on their creation day; rolled-back order creation does not count. A merchant amount limit configured as 0 adds no restriction for that limit. New orders still require a positive amount and must satisfy fee and balance requirements.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
direction |
string | No | PAYIN checks collections; PAYOUT checks payouts. Omit to check both. |
{}To check payouts only, send:
{
"direction": "PAYOUT"
}Success response data
With {}, data includes both payin and payout. With direction, it contains only the requested object. Each object contains:
| Response field | Type | Description |
|---|---|---|
acceptingOrders |
boolean | Whether new orders are currently accepted, based on maintenance, the merchant's direction switch, single-order range, remaining daily quota, and fee requirements. |
minimumAmount |
string / null | Minimum BRC order quantity (inclusive); null if there is no valid range. |
maximumAmount |
string / null | Maximum BRC order quantity (inclusive); null together with the minimum if there is no valid range. |
feeRate |
string | Decimal rate from 0 to 1, up to six decimal places. "0.01" means 1%. |
feeFixed |
string | Fixed fee per order, in BRC. Can be "0.00". |
feeMin |
string | Minimum fee per order, in BRC. |
A disabled direction can still return a range. Check acceptingOrders; even when true, the platform checks eligibility again when you submit an order.
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "44444444-5555-4666-8777-999999999999",
"timestamp": "2026-11-05T03:59:00Z",
"data": {
"payin": {
"acceptingOrders": true,
"minimumAmount": "1.00",
"maximumAmount": "10000.00",
"feeRate": "0.01",
"feeFixed": "0.10",
"feeMin": "0.00"
},
"payout": {
"acceptingOrders": true,
"minimumAmount": "1.00",
"maximumAmount": "10000.00",
"feeRate": "0.02",
"feeFixed": "0.20",
"feeMin": "0.00"
}
}
}Full payout-only response · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "44444444-5555-4666-8777-aaaaaaaaaaaa",
"timestamp": "2026-11-05T03:59:10Z",
"data": {
"payout": {
"acceptingOrders": true,
"minimumAmount": "1.00",
"maximumAmount": "10000.00",
"feeRate": "0.02",
"feeFixed": "0.20",
"feeMin": "0.00"
}
}
}4.5 Query merchant balance
POST /api/merchant/account/balance · SDK queryBalance(...)
One snapshot returns total, held and available BRC balances as decimal strings. Payout acceptance atomically reserves principal plus fees and rejects insufficient available funds before creating an order. Pending pay-ins and pending notifications do not hold funds.
Request
No business fields. Send an empty JSON object {}. Do not omit the body.
{}Success response data
| Response field | Type | Description |
|---|---|---|
currency |
string | Always BRC. |
balance |
string | Total booked BRC balance including held funds. |
heldBalance |
string | Generic occupied BRC funds, temporarily unavailable. Includes payout principal plus fees until settlement or release; not limited to payouts. |
availableBalance |
string | BRC available for new business: balance - heldBalance. |
asOf |
string | Balance read time in UTC. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "55555555-6666-4777-8888-aaaaaaaaaaaa",
"timestamp": "2026-11-05T04:04:00Z",
"data": {
"currency": "BRC",
"balance": "29.60",
"heldBalance": "5.30",
"availableBalance": "24.30",
"asOf": "2026-11-05T04:04:00Z"
}
}If the query fails: Show the error; do not display a zero balance as a fallback.
4.6 Query merchant transactions
POST /api/merchant/account/transactions · SDK queryTransactions(...)
List posted changes to the merchant balance and match them to orders using either order number. Pages contain up to 50 entries, sorted by createdAt descending and then id ascending. New entries can shift the pages; use id to recognize a repeated entry.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
page |
integer | No | Zero-based JSON integer. Defaults to 0; maximum 2147483647. |
{
"page": 0
}Success response data
| Response field | Type | Description |
|---|---|---|
page |
integer | Current page number. |
hasMore |
boolean | If true, request page + 1 for more entries. |
rows |
array | Entries on this page, or [] if there are none. |
Fields in each rows[] entry
| Response field | Type | Description |
|---|---|---|
id |
string | Ledger entry UUID; use it to recognize the same entry. |
orderNo |
string | Related BRPAY order number: RC or PY + 14 digits. |
merchantOrderNo |
string | Your reference for the related order. |
direction |
string | PAYIN or PAYOUT. |
amount |
string | Signed change in the booked BRC balance. A reversal has the opposite sign to the original entry; use the amount, not the order direction, for reconciliation. |
feeAmount |
string | Order fee in BRC, already reflected in amount. |
balanceAfter |
string | Merchant BRC balance after the entry was posted. |
createdAt |
string | Ledger entry timestamp in UTC. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "66666666-7777-4888-8999-bbbbbbbbbbbb",
"timestamp": "2026-11-05T04:04:30Z",
"data": {
"page": 0,
"hasMore": false,
"rows": [
{
"id": "66666666-7777-4888-8999-aaaaaaaaaaaa",
"orderNo": "RC00000000128364",
"merchantOrderNo": "RECHARGE-001",
"direction": "PAYIN",
"amount": "29.60",
"feeAmount": "0.40",
"balanceAfter": "29.60",
"createdAt": "2026-11-05T04:02:00Z"
}
]
}
}This is a live ledger view. Fetching multiple pages does not produce a point-in-time statement.
4.7 Look up wallet
POST /api/merchant/wallets/query · SDK queryWallet(...)
Check whether the customer's BRPAY wallet can receive a payout. No name, balance or other personal details are returned. Confirm the intended recipient in your own business flow.
Request
| Request field | Type | Required | Description |
|---|---|---|---|
account |
string | Yes | Uppercase W + 8 uppercase letters or digits, for example W00012836. |
{
"account": "W00012836"
}Success response data
| Response field | Type | Description |
|---|---|---|
exists |
boolean | Whether the wallet exists. When false, this is the only field. |
walletId |
string | Present when the wallet exists. Pass it as recipientWalletId when creating a payout. |
account |
string | Present when the wallet exists. The matching wallet account (W + 8 uppercase letters or digits). |
canReceive |
boolean | Present when the wallet exists. true means it can currently receive funds; the payout still checks the account and wallet capacity. |
Full response example · HTTP 200
{
"success": true,
"code": "OK",
"message": "",
"requestId": "77777777-8888-4999-8aaa-bbbbbbbbbbbb",
"timestamp": "2026-11-05T04:04:45Z",
"data": {
"exists": true,
"walletId": "W00012836",
"account": "W00012836",
"canReceive": true
}
}An unknown wallet still returns HTTP 200, with only exists: false inside data. Ask the customer to check the wallet number before submitting a payout.
{
"success": true,
"code": "OK",
"message": "",
"requestId": "88888888-9999-4aaa-8bbb-cccccccccccc",
"timestamp": "2026-11-05T04:04:50Z",
"data": {
"exists": false
}
}5. Result callbacks
5.1 Receiving and verification
Order requests optionally accept notifyUrl for asynchronous result notifications and returnUrl for the synchronous merchant return page. Each URL is saved with the order, up to 512 ASCII characters; percent-encode non-ASCII content. Use a full HTTPS URL without user credentials, fragments or control characters. Only local verification allows http://127.0.0.1. Omitting notifyUrl creates no notification and leaves notificationStatus null; omitting returnUrl hides the merchant return action. Null and empty strings mean omitted; invalid nonempty URLs are rejected. Both directions accept both fields. PAYOUT has no cashier: its return URL is stored without navigation. A terminal PAYIN result page offers a Return to merchant button using the saved URL unchanged, without automatic redirects or appended result parameters. A browser return is not proof of payment; verify a signed notification or query the order server-side.
URLs belong to orders. Each PAYIN or PAYOUT uses its own request URLs. New requests do not alter existing orders or retries. The platform must still allow the notification HTTPS origin for outbound delivery.
BRPAY sends an HTTP POST with UTF-8 JSON. Return the HTTP status directly; redirects are not followed.
| Header | Description |
|---|---|
X-BrazilPay-Event-Id |
Lowercase UUID equal to body eventId |
X-BrazilPay-Timestamp |
Delivery time in UTC RFC3339 ending in Z, unlike the API request’s Unix seconds |
X-BrazilPay-Signature |
64-character lowercase HMAC-SHA256 hex |
Sign the following byte sequence using the exact received UTF-8 body, without appending a newline:
canonicalBytes = utf8(timestamp + "\n" + eventId + "\n") + rawBodyBytes
signature = hmacSha256Hex(utf8(signingSecret), canonicalBytes)Compare signatures in constant time. The SDK checks signature, matching header/body event IDs, order fields and a 300-second delivery timestamp window. occurredAt may be an older business time; do not substitute it for delivery time validation.
5.2 Event format
ORDER_SUCCEEDED is sent after a PAYIN is paid or a PAYOUT is credited. ORDER_FAILED is sent for an expired PAYIN or a failed PAYOUT. There is no result callback while payment is pending.
| Callback field | Type | Description |
|---|---|---|
eventId |
string | Event UUID, matching X-BrazilPay-Event-Id; stable across delivery attempts. |
eventType |
string | ORDER_SUCCEEDED, ORDER_FAILED or ORDER_CORRECTED. |
occurredAt |
string | When the order reached its terminal state, equal to data.completedAt in UTC. |
data |
object | Order result, with the fields listed below. |
Fields in data
| Callback field | Type | Description |
|---|---|---|
orderNo |
string | BRPAY order number: RC for PAYIN or PY for PAYOUT, followed by 14 digits. |
merchantOrderNo |
string | Your order number from the original request. |
direction |
string | PAYIN or PAYOUT. |
currency |
string | Always BRC. |
orderAmount |
string | Original order quantity in BRC. |
feeAmount |
string | Fee fixed at order creation, in BRC. Can be "0.00". |
merchantAmount |
string | Positive BRC quantity: net PAYIN credit or total PAYOUT debit including the fee. An expired PAYIN is not credited. |
actualAmount |
string / null | BRC debited from the payer (PAYIN) or credited to the recipient (PAYOUT) on success; null on expiry. |
status |
string | SUCCESS for a paid PAYIN or credited PAYOUT; FAILED for an expired PAYIN or a confirmed failure; see failureCode. |
failureCode |
string / null | Failure reason; required for FAILED. Current expiry code: ORDER_EXPIRED. Null otherwise. |
walletId |
string / null | PAYOUT: designated recipient in every terminal state. PAYIN: actual payer when known, otherwise null. A wallet identifier alone does not confirm credit. |
createdAt |
string | Order creation time in UTC. |
expiresAt |
string / null | PAYIN payment deadline in UTC; null for PAYOUT. |
completedAt |
string | Terminal order time in UTC: completion on success, expiresAt on expiry. |
These are three complete callback bodies. BRPAY sends them as HTTP requests with eventId, eventType, occurredAt and data at the top level.
PAYIN paid · ORDER_SUCCEEDED
{
"eventId": "44444444-5555-4666-8777-888888888888",
"eventType": "ORDER_SUCCEEDED",
"occurredAt": "2026-11-05T04:02:00Z",
"data": {
"orderNo": "RC00000000128364",
"merchantOrderNo": "RECHARGE-001",
"direction": "PAYIN",
"currency": "BRC",
"orderAmount": "30.00",
"feeAmount": "0.40",
"merchantAmount": "29.60",
"actualAmount": "30.00",
"status": "SUCCESS",
"walletId": "W00012836",
"createdAt": "2026-11-05T04:00:00Z",
"expiresAt": "2026-11-05T04:30:00Z",
"completedAt": "2026-11-05T04:02:00Z",
"failureCode": null
}
}HTTP headers for the successful PAYIN (offline signature check). Recompute the signature using the sample secret in §2.2 and the successful PAYIN JSON above, encoded as UTF-8 with LF line breaks and no trailing newline. For a time-window test, set the test clock to X-BrazilPay-Timestamp; use the current time in your real receiver.
POST /webhooks/br-pay HTTP/1.1
Host: merchant.example.com
Content-Type: application/json; charset=utf-8
X-BrazilPay-Event-Id: 44444444-5555-4666-8777-888888888888
X-BrazilPay-Timestamp: 2026-11-05T04:02:01Z
X-BrazilPay-Signature: be9644982cae18aead3d900226ca585e2d30e6ab62370147e6f049404ce858a0
Content-Length: 585PAYOUT credited · ORDER_SUCCEEDED
{
"eventId": "44444444-5555-4666-8777-bbbbbbbbbbbb",
"eventType": "ORDER_SUCCEEDED",
"occurredAt": "2026-11-05T04:05:00Z",
"data": {
"orderNo": "PY00000000128365",
"merchantOrderNo": "WITHDRAW-001",
"direction": "PAYOUT",
"currency": "BRC",
"orderAmount": "20.00",
"feeAmount": "0.60",
"merchantAmount": "20.60",
"actualAmount": "20.00",
"status": "SUCCESS",
"walletId": "W00012836",
"createdAt": "2026-11-05T04:05:00Z",
"expiresAt": null,
"completedAt": "2026-11-05T04:05:00Z"
}
}PAYIN expired without payment · ORDER_FAILED
{
"eventId": "44444444-5555-4666-8777-aaaaaaaaaaaa",
"eventType": "ORDER_FAILED",
"occurredAt": "2026-11-05T04:30:00Z",
"data": {
"orderNo": "RC00000000128366",
"merchantOrderNo": "RECHARGE-002",
"direction": "PAYIN",
"currency": "BRC",
"orderAmount": "30.00",
"feeAmount": "0.40",
"merchantAmount": "29.60",
"actualAmount": null,
"status": "FAILED",
"walletId": null,
"createdAt": "2026-11-05T04:00:00Z",
"expiresAt": "2026-11-05T04:30:00Z",
"completedAt": "2026-11-05T04:30:00Z",
"failureCode": "ORDER_EXPIRED"
}
}5.3 Acknowledgment and repeated delivery
- Verify the signature first. Keep the raw body and verify it using the three callback headers. If verification fails, do not update an order or return 2xx.
- Find your order. Look up
data.merchantOrderNoin your system. Check the direction, BRC quantity, saved BRPAY order number and, for PAYOUT, the recipient wallet. If the callback arrives before the order response and you do not yet have the BRPAY order number, query by your original merchant order number first. - Apply the result. On
ORDER_SUCCEEDED, credit the game account or confirm the withdrawal only if your order has not been processed. On ORDER_FAILED, do not credit a failed PAYIN or mark a failed PAYOUT as paid. Handle ORDER_CORRECTED using its revision. - Acknowledge after saving. After your order and related changes are saved, return
204 No Content. Any 2xx works; no response body is needed. If the same event is delivered again and you confirm it is already handled, return 2xx again.
Delivery runs after the payment transaction commits. Repeated deliveries retain eventId and the original business body, with new delivery timestamps and signatures. Delivery timeout, failure or EXHAUSTED never rolls back successful funds. Current adjustable engineering defaults: at most 10 automatic attempts, 5-second HTTP timeout, exponential delay starting at 5 seconds capped at 300 seconds. Persist essential work within 5 seconds and perform lengthy fulfillment in your own background system. When the budget is exhausted, query the order and contact the platform for manual redelivery.
When outbound origins are unconfigured, the platform preserves the pending notification with DELIVERY_DISABLED without consuming HTTP attempts. Do not create another funds order because of this condition.
6. Errors and unconfirmed outcomes
| HTTP | code | Action |
|---|---|---|
| 400 | MERCHANT_REQUEST_INVALID |
Invalid fields, JSON or UUID; correct input |
| 401 | MERCHANT_AUTH_REQUIRED / INVALID_SIGNATURE / SIGNATURE_EXPIRED |
Check merchant ID, secret, raw bytes, path and clock |
| 403 | MERCHANT_DIRECTION_DISABLED / ORIGIN_REJECTED |
Query availability if closed by merchant; for rejected Origin, call from your merchant server |
| 403 | ACCOUNT_DISABLED |
Recipient account is disabled. Stop payouts to it and contact the platform; recheck wallet canReceive rather than changing the merchant order number |
| 404 | ORDER_NOT_FOUND / WALLET_NOT_FOUND |
Check order or wallet; wallet lookup normally returns exists=false when missing |
| 405 | METHOD_NOT_ALLOWED |
Use POST; response includes Allow: POST |
| 409 | ORDER_EXISTS |
Query and compare the existing order; conflict is not success |
| 409 | MERCHANT_INSUFFICIENT_BALANCE / WALLET_LIMIT_EXCEEDED |
Insufficient merchant balance or user wallet capacity |
| 409 | CONFIG_CHANGED / MERCHANT_DAILY_LIMIT |
Configuration changed or daily amount limit reached; recheck availability |
| 413 | BODY_TOO_LARGE |
Body exceeds 8192 bytes |
| 415 | JSON_REQUIRED |
Send application/json |
| 422 | INVALID_AMOUNT / INVALID_FEE / MERCHANT_AMOUNT_OUT_OF_RANGE |
Check quantity strings, fees and current allowed range |
| 503 | MAINTENANCE_CONFIG_UNAVAILABLE |
Maintenance configuration cannot be verified, so new orders are rejected. Contact the platform; treat funds receipts as unconfirmed and query the original order first |
| 503 | CLIENT_MAINTENANCE |
Maintenance closes new orders; existing orders/accounts remain queryable |
| 503 | MERCHANT_SCHEMA_REQUIRED / CLIENT_MAINTENANCE / SERVICE_UNAVAILABLE |
Service/configuration unavailable; contact platform and query unconfirmed writes first |
Codes are case-sensitive. Treat HTTP 5xx, disconnects, timeouts, and invalid or mismatched receipts as unconfirmed. Query the original merchant order number before taking any further action.
Example: PAYOUT WITHDRAW-001 times out.
- Keep the original request, merchant order number
WITHDRAW-001and request trace ID. Do not change your balance or make a payout with a new order number. - Query the original order with
{"merchantOrderNo":"WITHDRAW-001"}. If it isSUCCESS, verify the direction, BRC quantity and recipient wallet before marking it credited. If this was a PAYIN, handle itsPENDING,SUCCESSorFAILEDstatus. - If the query also times out or returns 5xx, wait and query again. Resubmit only after an explicit
404 / ORDER_NOT_FOUND, using the original merchant order number and the original business fields. The first request may still be running. If you get409 / ORDER_EXISTS, query that same order.
The Java and PHP SDKs use MerchantApiException for explicit rejections and UnknownOutcomeException for unconfirmed outcomes, including HTTP 5xx. Default total request deadline is 30 seconds including connection, headers and body, with a 256 KiB response limit. Configure deadlines for your actual environment. SDKs do not follow redirects automatically.
7. SDK usage
7.1 Java
Requires Java 17+. Use merchant-sdk.jar with its supplied standalone POM, or build sdk/java. Dependency: tools.jackson.core:jackson-databind:3.1.4. The following demonstrates individual calls, not an automatic sequence. Quantities and recipients require your own business validation:
import java.time.Duration;
import tech.brazilpay.merchant.sdk.MerchantClient;
import tech.brazilpay.merchant.sdk.MerchantWebhookVerifier;
// Load settings from your server-side configuration / secret store.
var client = new MerchantClient(baseUrl, merchantId, signingSecret, Duration.ofSeconds(30));
var balance = client.queryBalance();
String availableBalance = balance.body().path("data").path("availableBalance").asText();
var availability = client.queryAvailability("PAYOUT");
var wallet = client.queryWallet("W00012836");
// Check exists / canReceive and confirm the actual recipient before payout.
var payin = client.payin(new MerchantClient.PayinRequest("RECHARGE-001", "30.00", "Game recharge", notifyUrl, returnUrl));
var payout = client.payout(new MerchantClient.PayoutRequest("WITHDRAW-001", "20.00", "Game withdrawal", recipientWalletId, notifyUrl, returnUrl));
var order = client.queryOrder("RECHARGE-001");
var orderByPlatformNo = client.querySystemOrder("RC00000000128364");
var transactions = client.queryTransactions(0);
// Inside your HTTP callback receiver; rawBodyBytes must be the untouched body.
var event = new MerchantWebhookVerifier(signingSecret)
.verifyAndParse(timestampHeader, eventIdHeader, signatureHeader, rawBodyBytes);
// Verify your business order, persist under its state conditions, then return 2xx.7.2 PHP
Requires PHP 8.1+ with cURL and JSON. Use Composer PSR-4 namespace BrazilPay\Merchant, or require src/*.php. The CLI demo.php queries balance by default without creating orders.
use BrazilPay\Merchant\MerchantClient;
use BrazilPay\Merchant\MerchantWebhookVerifier;
// Require Composer autoload.php, or the supplied src/*.php classes first.
$client = new MerchantClient($baseUrl, $merchantId, $signingSecret, 30);
$balance = $client->queryBalance();
$availableBalance = $balance['data']['availableBalance'];
$availability = $client->queryAvailability('PAYOUT');
$wallet = $client->queryWallet('W00012836');
// Check exists / canReceive and confirm the actual recipient before payout.
$payin = $client->payin(['merchantOrderNo'=>'RECHARGE-001', 'orderAmount'=>'30.00', 'description'=>'Game recharge', 'notifyUrl'=>$notifyUrl, 'returnUrl'=>$returnUrl]);
$payout = $client->payout(['merchantOrderNo'=>'WITHDRAW-001', 'orderAmount'=>'20.00', 'description'=>'Game withdrawal', 'recipientWalletId'=>$recipientWalletId, 'notifyUrl'=>$notifyUrl, 'returnUrl'=>$returnUrl]);
$order = $client->queryOrder('RECHARGE-001');
$orderByPlatformNo = $client->querySystemOrder('RC00000000128364');
$transactions = $client->queryTransactions(0);
// Inside your HTTP callback receiver; reject failures before business changes.
$rawBody = file_get_contents('php://input');
$event = (new MerchantWebhookVerifier($signingSecret))->verifyAndParse(
$_SERVER['HTTP_X_BRAZILPAY_TIMESTAMP'] ?? '',
$_SERVER['HTTP_X_BRAZILPAY_EVENT_ID'] ?? '',
$_SERVER['HTTP_X_BRAZILPAY_SIGNATURE'] ?? '',
$rawBody
);
// Verify your business order, persist under its state conditions, then return 2xx.8. App payment and web guidance
- Create PAYIN on the merchant server. The client opens cashierUrl, or opens appUrl with Android ACTION_VIEW.
- A valid Web session opens confirmation for the original order directly. Other users can pay on the Web, open the native app, or scan the checkout QR in the app. A confirmed installation makes opening the app the primary action. Links carry a random order credential and do not authorize a debit.
- Login/registration, profile and payment password setup return to the original order. If the checkout page is closed, reopen it from the merchant order.
- Honor server expiresAt; installation does not extend it. Complete the merchant business only after SUCCESS from a signed callback or server-side query.
Opening the app does not prove payment. Production HTTPS App Links association, release signing and real-device payments require separate acceptance. The configured payment origin must actually serve /pay/*, not just a website without the payment entry. The SDK accepts only the current /pay/ path.
9. Local SDK DEMO
Open the local DEMO. Its /api/* paths belong to the test workspace. Use the baseUrl supplied by BRPAY in your integration. To test the new transaction API with the DEMO, first check that both use the same version. Download OpenAPI from the top of this page.
Before testing callbacks, ask BRPAY to verify the callback URL and notification delivery for your test merchant. Passing a read-only query does not verify callback delivery.
10. Integration acceptance
- Check baseUrl, merchantId and the signing secret; verify signing with a read-only request. Confirm the order includes notifyUrl and BRPAY can deliver to it through the allowed HTTPS origin.
- In an authorized test environment, verify PAYIN success/expiry, PAYOUT success/explicit rejection, fees and both sides’ balances.
- Verify rejection of tampered bodies, invalid signatures and stale timestamps; persist successful callback processing before 2xx and do not complete the business twice on repeated delivery.
- Handle timeouts and 5xx as unconfirmed outcomes and recover using the original merchant order number; never hide uncertainty with a new business number.
- Real-device app payments, HTTPS, production callback egress and release require acceptance in their own environments. Local builds and page checks do not replace them.
Administrative order corrections
After verifying the actual funds, an administrator may settle a pending order or correct a terminal result. The original principal and fee remain unchanged; compensating ledger entries preserve the complete history.
ORDER_CORRECTED carries a new event ID, correctionId, previousStatus (PENDING / SUCCESS / FAILED), and a positive increasing data.orderRevision. Its data is the corrected terminal snapshot. Failed corrections use MANUAL_CORRECTION; a failed pay-in can retain its original payer wallet ID. Queries expose orderRevision; original settlement events have revision 0.
Persist the last processed terminal revision in the merchant's own order transaction. Before any terminal result has been processed, accept the first normal success or failure event at revision 0. After that, only a newer revision can replace the terminal result. Revision 0 on a PENDING query does not mark a terminal result as processed and must not suppress the following normal terminal event. Late older events and repeated delivery must not repeat fulfillment. A correction is not a second deposit. Reconcile any previous game credit or fulfillment explicitly. Each identified event retains its immutable body. Java and PHP verifiers accept correction events, and the demo displays the snapshot; its in-memory event list is not a durable merchant ledger.