PlatinaPay
PlatinaPay API

PlatinaPay Merchant API

Accept USDT on TRON (TRC-20) and BNB Smart Chain (BEP-20). Fast integration, real-time blockchain confirmation, automatic webhook notifications.

Base URL: https://platinapay.org
All API requests use HTTPS. HTTP requests are redirected.

How it works

1. Create order
2. Redirect to payment page
3. User pays in the chosen network
4. Blockchain confirms
5. Webhook callback

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.

Keep your api_secret private. Never expose it in client-side code, mobile apps, or public repositories. All API calls must be made from your backend server.

Supported Currencies

PlatinaPay accepts payments in fiat currencies and converts them to USDT TRC-20 at the current market rate.

CodeCurrencyMin AmountRate SourceNotes
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.
How to use: Pass "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.

Networknetwork valueCoinContractConfirmations
TRONtron / trc20USDTTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t19
BNB Smart Chainbsc / bep20USDT0x55d398326f99059fF775485246999027B319795515

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.

The list changes as wallets are enabled and disabled. Read GET /api/v1/networks rather than hardcoding it — the endpoint is public and needs no signature.

Current list

GET /api/v1/networks
{
  "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.

1

Collect fields

Take all request body fields except sign.

2

Sort alphabetically

Sort field names by ASCII value (A-Z, a-z).

3

Concatenate

Join as key1=value1&key2=value2&...

4

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

1

Create an order

Call POST /api/v1/orders/create from your backend with the payment amount and your webhook URL.

2

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.

3

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.

4

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.

POST /api/v1/orders/create

Request body

FieldTypeRequiredDescription
order_idstringrequiredYour unique order identifier (max 100 chars)
amountnumberrequiredPayment amount in fiat currency (min 1 RUB / 1 USD)
currencystringoptional"RUB" (default) or "USD". See Currencies.
notify_urlstringoptionalWebhook URL for payment notifications (HTTPS required)
redirect_urlstringoptionalURL to redirect user after payment
signstringrequiredHMAC-SHA256 signature

Response

FieldTypeDescription
trade_idstringPlatinaPay unique payment ID
order_idstringYour order ID (echoed back)
amountnumberOriginal fiat amount
actual_amountnumberUSDT amount to be paid (4 decimal places)
currencystringCurrency code
rate_usednumberExchange rate applied (fiat/USDT)
tokenstringWallet address to pay to, on the chosen network
networkstringThe order's network: tron or bsc
coinstringThe order's coin
expiration_timeintegerUnix timestamp when order expires
payment_urlstringRedirect 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.

POST /api/v1/orders/checkout

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.

POST /api/v1/orders/cancel

Request body

FieldTypeRequiredDescription
trade_idstringrequiredPlatinaPay trade ID from create response
signstringrequiredHMAC-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.

GET /api/v1/orders/query/{trade_id}
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.

GET /pay/status/{trade_id}
// Response
{"status_code": 200, "data": {"status": 1}}
Rate limited to 60 requests/minute per IP. The checkout page already polls this automatically — you don't need to implement polling yourself unless building a custom checkout UI.

Webhook Callback

When a payment is confirmed on the blockchain, PlatinaPay sends a POST request to your notify_url with the payment details.

The field set follows the order's network. TRON orders carry exactly the nine fields they always did, byte for byte. Orders on any other network carry two more: 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..."
}
FieldTypeDescription
trade_idstringPlatinaPay payment ID
order_idstringYour order ID
amountnumberOriginal fiat amount
actual_amountnumberUSDT amount received
currencystringCurrency code
rate_usednumberExchange rate used
tokenstringWallet address the payment arrived at
block_transaction_idstringBlockchain transaction hash
statusintegerAlways 1 (Paid)
networkstringChain id: tron, bsc. Non-TRON orders only
coinstringAsset: USDT. Non-TRON orders only
signstringHMAC-SHA256 signature — verify this!
Return HTTP 200 to acknowledge receipt. Any other status code triggers a retry. Your balance is credited only after successful acknowledgment.

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:

AttemptDelayTotal elapsed
1stImmediate0 min
2nd1 minute1 min
3rd2 minutes3 min
4th5 minutes8 min
5th10 minutes18 min
6th15 minutes33 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.

GET /merchant/api/payment-settings
PUT /merchant/api/payment-settings
FieldTypeDescription
availablearrayEverything the gateway can currently receive. The menu, not your choice
allowed_assetsarrayWhat you accept, as "USDT.bsc" strings. An empty list means everything, not nothing
callback_versioninteger0 — 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
}
An empty 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

ValueNameDescription
0AwaitingNetwork chosen, address assigned, waiting for payment
1PaidPayment confirmed on the blockchain
2ExpiredOrder expired (default: 20 minutes)
3CancelledCancelled by merchant via API
4Awaiting choiceThe payer has not picked a coin and network yet, so there is no address or amount. /orders/checkout orders only

Callback status

ValueNameDescription
0PendingNot yet attempted
1DeliveredMerchant returned HTTP 200
2RetryingDelivery in progress (retry cycle)
3FailedAll 7 attempts exhausted

Error Codes

CodeNameDescription
10001INVALID_REQUESTMissing or invalid request parameters
10002UNAUTHORIZEDInvalid API key or signature
10003MERCHANT_SUSPENDEDYour account has been suspended
10004ORDER_ALREADY_EXISTSDuplicate order_id for this merchant
10005PAY_AMOUNT_TOO_SMALLAmount below minimum (1 RUB / 1 USD)
10006PAY_AMOUNT_TOO_LARGEAmount exceeds maximum (10,000 USDT equivalent)
10007RATE_NOT_AVAILABLEExchange rate temporarily unavailable
10008ORDER_NOT_CANCELLABLEOrder is not in Awaiting status
10009NOT_AVAILABLE_AMOUNTNo payment slot available (all amounts taken)
10010NOT_AVAILABLE_WALLETNo wallet address available
10011INSUFFICIENT_BALANCENot enough balance for withdrawal
10012ORDER_NOT_FOUNDOrder doesn't exist or belongs to another merchant
10013ACCOUNT_PENDINGAccount pending admin approval
10014ACCOUNT_REJECTEDAccount registration was rejected
10015TWO_FACTOR_REQUIRED2FA verification needed
10016UNSUPPORTED_NETWORKUnknown value in network — take values from /api/v1/networks
10017NETWORK_UNAVAILABLEThe 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

EndpointLimitScope
POST /api/v1/orders/create100 req/minPer API key
POST /api/v1/orders/create1,000 req/minGlobal (all merchants)
All /api/v1/* endpoints300 req/minPer API key
GET /pay/status/*60 req/minPer 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.

PlatinaPay API Documentation · USDT TRC-20 Payment Gateway