PlatinaPay Merchant API
Accept USDT on TRON (TRC-20) and BNB Smart Chain (BEP-20). Fast integration, real-time blockchain confirmation, automatic webhook notifications.
https://platinapay.orgAll API requests use HTTPS. HTTP requests are redirected.
How it works
Authentication
Every API request must include your api_key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
You receive your api_key and api_secret after your merchant account is approved by the platform administrator. Find them in your Merchant Dashboard → API Keys.
Supported Currencies
PlatinaPay accepts payments in fiat currencies and converts them to USDT TRC-20 at the current market rate.
| Code | Currency | Min Amount | Rate Source | Notes |
|---|---|---|---|---|
RUB |
Russian Ruble | 1 ₽ | CoinMarketCap (live, updated every 60s) | Default. Rate markup may apply per merchant. |
USD |
US Dollar | 1 $ | CoinMarketCap (live, ≈1.00) | No rate markup. USDT ≈ USD. |
"currency": "RUB" or "currency": "USD" in the request body.
If omitted, defaults to RUB. The amount field is always in the specified fiat currency — the system calculates the USDT equivalent automatically.
Rate calculation
// Example: 1000 RUB order at rate 83.21 RUB/USDT actual_usdt = 1000 / 83.21 = 12.0178 USDT // Example: 50 USD order at rate ~1.00 USD/USDT actual_usdt = 50 / 1.00 = 50.0000 USDT // The rate_used field in the response shows the exact rate applied. // Max order: 10,000 USDT equivalent (configurable by platform admin).
Networks & Coins
The gateway accepts USDT on two networks. The network is fixed when the order is created: either you name it, or the payer picks it on the hosted checkout page.
| Network | network value | Coin | Contract | Confirmations |
|---|---|---|---|---|
| TRON | tron / trc20 | USDT | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t | 19 |
| BNB Smart Chain | bsc / bep20 | USDT | 0x55d398326f99059fF775485246999027B3197955 | 15 |
The network field accepts either a chain id (bsc, tron) or the name used on exchange withdrawal screens (bep20, trc20, erc20), in any case. Omit it and the order is created on TRON.
GET /api/v1/networks rather than hardcoding it — the endpoint is public and needs no signature.Current list
{
"status_code": 200,
"data": [
{
"network": "bsc",
"network_name": "BNB Smart Chain",
"coin": "USDT",
"contract": "0x55d398326f99059fF775485246999027B3197955",
"decimals": 18,
"confirmations": 15,
"payment_window_seconds": 1200
}
]
}
What comes back is the intersection of three things: what the gateway supports, what it has an enabled wallet for, and what you accept — see Payment Settings.
Request Signing (HMAC-SHA256)
Every POST request body must include a sign field. This prevents request tampering and proves your identity.
Collect fields
Take all request body fields except sign.
Sort alphabetically
Sort field names by ASCII value (A-Z, a-z).
Concatenate
Join as key1=value1&key2=value2&...
HMAC-SHA256
Compute HMAC-SHA256 of the string using your api_secret as the key. The result (lowercase hex) is your sign value.
Signing example
Given these fields and api_secret = "abc123secret":
// Input fields (excluding "sign"):
order_id = "ORDER-001"
amount = 1000
currency = "RUB"
notify_url = "https://example.com/callback"
// Step 2: Sort by key name
amount, currency, notify_url, order_id
// Step 3: Concatenate
"amount=1000¤cy=RUB¬ify_url=https://example.com/callback&order_id=ORDER-001"
// Step 4: HMAC-SHA256 with api_secret
sign = hmac_sha256("amount=1000¤cy=RUB¬ify_url=https://example.com/callback&order_id=ORDER-001", "abc123secret")
// → "e5f3a1b2c4d6..."
Integration Flow
Create an order
Call POST /api/v1/orders/create from your backend with the payment amount and your webhook URL.
Redirect the user
Redirect the user to the payment_url from the response. They'll see a checkout page with a QR code, wallet address, and countdown timer.
Receive the webhook
When payment is confirmed on the blockchain, we POST to your notify_url. Verify the sign field and respond with HTTP 200.
Fulfill the order
Mark the order as paid in your system. The user is automatically redirected to your redirect_url.
Create Order
Create a new payment order. Returns a payment URL to redirect the user to.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| order_id | string | required | Your unique order identifier (max 100 chars) |
| amount | number | required | Payment amount in fiat currency (min 1 RUB / 1 USD) |
| currency | string | optional | "RUB" (default) or "USD". See Currencies. |
| notify_url | string | optional | Webhook URL for payment notifications (HTTPS required) |
| redirect_url | string | optional | URL to redirect user after payment |
| sign | string | required | HMAC-SHA256 signature |
Response
| Field | Type | Description |
|---|---|---|
| trade_id | string | PlatinaPay unique payment ID |
| order_id | string | Your order ID (echoed back) |
| amount | number | Original fiat amount |
| actual_amount | number | USDT amount to be paid (4 decimal places) |
| currency | string | Currency code |
| rate_used | number | Exchange rate applied (fiat/USDT) |
| token | string | Wallet address to pay to, on the chosen network |
| network | string | The order's network: tron or bsc |
| coin | string | The order's coin |
| expiration_time | integer | Unix timestamp when order expires |
| payment_url | string | Redirect user here to pay |
Code examples
curl -X POST https://platinapay.org/api/v1/orders/create \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "ORDER-001",
"amount": 1000.00,
"currency": "RUB",
"notify_url": "https://yoursite.com/webhook",
"redirect_url": "https://yoursite.com/success",
"sign": "COMPUTED_HMAC_SHA256_HEX"
}'
Response example
{
"status_code": 200,
"message": "success",
"data": {
"trade_id": "20260322a1b2c3d4e5f67890",
"order_id": "ORDER-001",
"amount": 1000.00,
"actual_amount": 12.0758,
"currency": "RUB",
"rate_used": 82.81,
"token": "TN4JsVEyUBMcBjJbRGTriAPBDMjZaxnMet",
"expiration_time": 1774131385,
"payment_url": "https://platinapay.org/pay/checkout-counter/20260322a1b2c3d4e5f67890"
},
"request_id": "req_abc123"
}
Order with payer-chosen network
Creates an order whose coin and network the payer picks on our hosted checkout page. Use it when you would rather not maintain a network list of your own.
Takes the same fields as Create Order except network and coin — the point is that they are not yet known.
{
"status_code": 200,
"data": {
"trade_id": "20260322a1b2c3d4e5f67890",
"order_id": "ORDER-001",
"amount": 1000.00,
"currency": "RUB",
"expiration_time": 1774131385,
"payment_url": "https://platinapay.org/pay/checkout-counter/20260322a1b2c3d4e5f67890"
}
}
No token, actual_amount or rate_used: nothing about the payment can be quoted until the payer picks an asset. The order sits at status 4.
expiration_time here is the deadline for choosing, not for paying. The payment window starts when the payer chooses, and its length depends on the asset.From there it is the usual flow: send the payer to payment_url and learn the outcome from the webhook or by polling Query Order. If no asset is currently receivable the endpoint returns 10010 rather than sending the payer to a page with nothing to pick.
Cancel Order
Cancel a pending (unpaid) order. Only orders with status 0 (Awaiting) can be cancelled.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| trade_id | string | required | PlatinaPay trade ID from create response |
| sign | string | required | HMAC-SHA256 signature |
curl -X POST https://platinapay.org/api/v1/orders/cancel \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"trade_id": "20260322a1b2c3d4e5f67890", "sign": "HMAC_HEX"}'
Query Order
Retrieve the current status and details of an order.
curl https://platinapay.org/api/v1/orders/query/20260322a1b2c3d4e5f67890 \ -H "Authorization: Bearer YOUR_API_KEY"
Response example
{
"status_code": 200,
"data": {
"trade_id": "20260322a1b2c3d4e5f67890",
"order_id": "ORDER-001",
"amount": 1000.00,
"actual_amount": 12.0758,
"currency": "RUB",
"rate_used": 82.81,
"token": "TN4JsVEyUBMcBjJbRGTriAPBDMjZaxnMet",
"status": 1,
"block_transaction_id": "7f3a2b1c...",
"callback_status": 1,
"commission": 0.2415,
"net_amount": 11.8343,
"created_at": "2026-03-22T12:00:00Z",
"paid_at": "2026-03-22T12:03:45Z",
"expiration_time": null,
"callback_payload": { "..." }
}
}
Payment Status (Polling)
Lightweight endpoint to check if a payment has been received. Useful for frontend polling while the user is on the checkout page. No authentication required.
// Response
{"status_code": 200, "data": {"status": 1}}
Webhook Callback
When a payment is confirmed on the blockchain, PlatinaPay sends a POST request to your notify_url with the payment details.
network and coin.The body is signed, so its field set is part of the contract. A handler that hardcodes the nine field names will sign nine while the gateway signed eleven: every BEP-20 callback then fails verification, the payment reads as unpaid, and the money is already on the wallet. A handler that signs whatever arrived needs no change — the examples below are written that way.
Want one shape on every network? Pin version 2 in payment settings and network and coin arrive on TRON too.
Callback payload
{
"trade_id": "20260322a1b2c3d4e5f67890",
"order_id": "ORDER-001",
"amount": 1000.00,
"actual_amount": 12.0758,
"currency": "RUB",
"rate_used": 82.81,
"token": "TN4JsVEyUBMcBjJbRGTriAPBDMjZaxnMet",
"block_transaction_id": "7f3a2b1c4d5e6f7890abcdef...",
"status": 1,
"sign": "a3f2b1c4d5e6f7890123456789abcdef..."
}
| Field | Type | Description |
|---|---|---|
| trade_id | string | PlatinaPay payment ID |
| order_id | string | Your order ID |
| amount | number | Original fiat amount |
| actual_amount | number | USDT amount received |
| currency | string | Currency code |
| rate_used | number | Exchange rate used |
| token | string | Wallet address the payment arrived at |
| block_transaction_id | string | Blockchain transaction hash |
| status | integer | Always 1 (Paid) |
| network | string | Chain id: tron, bsc. Non-TRON orders only |
| coin | string | Asset: USDT. Non-TRON orders only |
| sign | string | HMAC-SHA256 signature — verify this! |
Verifying Webhook Signatures
Always verify the sign field in webhook callbacks to ensure the request is authentic.
# Flask example
from flask import Flask, request, jsonify
import hmac, hashlib
app = Flask(__name__)
API_SECRET = "your_api_secret"
def verify_sign(payload: dict, secret: str) -> bool:
received_sign = payload.pop("sign", "")
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(payload.items()))
expected = hmac.new(secret.encode(), sorted_str.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(received_sign, expected)
@app.route("/webhook", methods=["POST"])
def webhook():
data = request.json
if not verify_sign(dict(data), API_SECRET):
return "Invalid signature", 403
trade_id = data["trade_id"]
order_id = data["order_id"]
amount_usdt = data["actual_amount"]
# ✅ Mark order as paid in your database
print(f"Payment confirmed: {order_id} — {amount_usdt} USDT")
return "ok", 200 # Must return 200!
Retry Policy
If your server doesn't respond with HTTP 200, we retry with exponential backoff:
| Attempt | Delay | Total elapsed |
|---|---|---|
| 1st | Immediate | 0 min |
| 2nd | 1 minute | 1 min |
| 3rd | 2 minutes | 3 min |
| 4th | 5 minutes | 8 min |
| 5th | 10 minutes | 18 min |
| 6th | 15 minutes | 33 min |
| 7th (final) | 30 minutes | ~63 min |
After 7 failed attempts, the callback is marked as failed. You can still query the order status via the API. The platform administrator can also trigger a new retry cycle from the admin panel.
Payment Settings
Which coins and networks you accept, and which callback shape you receive. Set in the merchant cabinet under Payment settings, or through the API.
| Field | Type | Description |
|---|---|---|
| available | array | Everything the gateway can currently receive. The menu, not your choice |
| allowed_assets | array | What you accept, as "USDT.bsc" strings. An empty list means everything, not nothing |
| callback_version | integer | 0 — shape follows the order's network (default), 1 — always the nine v1 fields, 2 — always with network and coin |
PUT /merchant/api/payment-settings
{
"allowed_assets": ["USDT.tron", "USDT.bsc"],
"callback_version": 2
}
allowed_assets means "accept everything", including networks added later. Want a fixed set? List it explicitly.Restricting the list narrows both what GET /api/v1/networks returns for your orders and what the hosted checkout offers a payer. An entry naming an asset the gateway cannot currently receive is kept rather than rejected — it simply matches nothing until a wallet for it exists.
Pinned callback_version: 1 and still need the network? It is in the Query Order response — network and coin are reported there on every network, because that response is not signed.
Order & Callback Statuses
Order status
| Value | Name | Description |
|---|---|---|
| 0 | Awaiting | Network chosen, address assigned, waiting for payment |
| 1 | Paid | Payment confirmed on the blockchain |
| 2 | Expired | Order expired (default: 20 minutes) |
| 3 | Cancelled | Cancelled by merchant via API |
| 4 | Awaiting choice | The payer has not picked a coin and network yet, so there is no address or amount. /orders/checkout orders only |
Callback status
| Value | Name | Description |
|---|---|---|
| 0 | Pending | Not yet attempted |
| 1 | Delivered | Merchant returned HTTP 200 |
| 2 | Retrying | Delivery in progress (retry cycle) |
| 3 | Failed | All 7 attempts exhausted |
Error Codes
| Code | Name | Description |
|---|---|---|
10001 | INVALID_REQUEST | Missing or invalid request parameters |
10002 | UNAUTHORIZED | Invalid API key or signature |
10003 | MERCHANT_SUSPENDED | Your account has been suspended |
10004 | ORDER_ALREADY_EXISTS | Duplicate order_id for this merchant |
10005 | PAY_AMOUNT_TOO_SMALL | Amount below minimum (1 RUB / 1 USD) |
10006 | PAY_AMOUNT_TOO_LARGE | Amount exceeds maximum (10,000 USDT equivalent) |
10007 | RATE_NOT_AVAILABLE | Exchange rate temporarily unavailable |
10008 | ORDER_NOT_CANCELLABLE | Order is not in Awaiting status |
10009 | NOT_AVAILABLE_AMOUNT | No payment slot available (all amounts taken) |
10010 | NOT_AVAILABLE_WALLET | No wallet address available |
10011 | INSUFFICIENT_BALANCE | Not enough balance for withdrawal |
10012 | ORDER_NOT_FOUND | Order doesn't exist or belongs to another merchant |
10013 | ACCOUNT_PENDING | Account pending admin approval |
10014 | ACCOUNT_REJECTED | Account registration was rejected |
10015 | TWO_FACTOR_REQUIRED | 2FA verification needed |
10016 | UNSUPPORTED_NETWORK | Unknown value in network — take values from /api/v1/networks |
10017 | NETWORK_UNAVAILABLE | The network is temporarily unavailable — offer another or retry later |
Error response format
{
"status_code": 10004,
"message": "order_id already exists for this merchant",
"data": null,
"request_id": "req_abc123"
}
Rate Limits
| Endpoint | Limit | Scope |
|---|---|---|
POST /api/v1/orders/create | 100 req/min | Per API key |
POST /api/v1/orders/create | 1,000 req/min | Global (all merchants) |
All /api/v1/* endpoints | 300 req/min | Per API key |
GET /pay/status/* | 60 req/min | Per IP address |
When rate limited, the API returns HTTP 429 (Too Many Requests).
Best Practices
Always verify webhook signatures
Use constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing attacks.
Make your webhook handler idempotent
We may send the same callback multiple times. Check if the order is already fulfilled before processing. Use trade_id as the idempotency key.
Respond to webhooks quickly
Return HTTP 200 within 30 seconds. Do heavy processing asynchronously after acknowledging.
Use unique order_ids
Each order_id must be unique per merchant. Reusing an order_id returns error 10004.
Handle order expiration
Orders expire after 20 minutes by default. If the user doesn't pay in time, create a new order.
Keep your API secret secure
Store it in environment variables or a secrets manager. Rotate via the merchant dashboard if compromised.