RUNEPAY API Reference v1.0
Sign in Get API keys

Rune Pay API

Accept cryptocurrency payments and send payouts from your own product. One REST API, signed callbacks, and no percentage taken by a middleman you did not choose.

Everything below is generated from the live service, so the parameters, responses and limits on this page are the ones the API actually enforces. Base URL:

Base URL
https://rpay.gg/api/v1

Accepting payments right now

These networks are live on this instance. The authoritative list is GET /common/currencies — build your checkout from that, not from this page.

BTC · Bitcoin TRX · TRON (TRC20) USDT · TRON (TRC20) USDC · TRON (TRC20) ETH · Ethereum (ERC20) USDT · Ethereum (ERC20) USDC · Ethereum (ERC20) DAI · Ethereum (ERC20) BNB · BNB Smart Chain (BEP20) USDT · BNB Smart Chain (BEP20) POL · Polygon USDT · Polygon ETH · Base ETH · Arbitrum USDT · Arbitrum USDC · Arbitrum SOL · Solana USDT · Solana TON · TON USDT · TON LTC · Litecoin DOGE · Dogecoin
Your first call
curl -X POST https://rpay.gg/api/v1/payment/invoice \
  -H 'merchant_api_key: rp_sec_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"amount": 25.00, "currency": "USD"}'
Response · 201
{
    "data": {
        "track_id": "INV_9F2K4M1Q7X",
        "payment_url": "https://rpay.gg/checkout/INV_9F2K4M1Q7X",
        "expired_at": 1754096400,
        "date": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}

Download the reference

Every format is generated from the same source as this page, so none of them can drift out of date. Each is a live endpoint, not a stored file — fetch it in CI and your client stays in step with the API.

Introduction

The Rune Pay API accepts cryptocurrency payments and sends payouts. It is a JSON API over HTTPS: every request and response is application/json, and there are no SDK-only features — anything the dashboard can do, the API can do.

Request and response shapes follow the OxaPay convention. If you already have a working OxaPay integration, changing the base URL and the API key is normally the whole migration.

The essentials

Base URL https://rpay.gg/api/v1
Protocol HTTPS only. Plain HTTP is redirected and must never carry a key.
Encoding UTF-8. Send Content-Type: application/json on every request with a body.
Versioning The version is in the path (/v1). A breaking change ships as /v2; /v1 keeps working.
Amounts Always strings, never floats. See Amounts and precision.
Timestamps Unix seconds (UTC integers), named date, paid_at, expired_at.

Authentication

There are two kinds of key, deliberately separated. A merchant key (rp_sec_…) can take money in. A payout key (rp_out_…) can send money out. A key that leaks from your checkout page therefore cannot drain your balance.

Keys are shown once
The raw key is displayed only at creation. Rune Pay stores nothing but its SHA-256 hash, so a database leak does not expose usable credentials — and we genuinely cannot recover it for you. If you lose it, revoke it and create another.

Merchant key — payments

Prefix rp_sec_
Preferred header merchant_api_key: rp_sec_…
Also accepted Authorization: Bearer rp_sec_… · x-api-key: rp_sec_…
Grants Create and read invoices, white-label payments and static addresses.

Payout key — sending funds

Prefix rp_out_
Preferred header payout_api_key: rp_out_…
Also accepted Authorization: Bearer rp_out_…
Grants Read your balance, queue payouts, read payout history.
A request that works
curl -X POST https://rpay.gg/api/v1/payment/invoice \
  -H 'merchant_api_key: rp_sec_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"amount": 25.00, "currency": "USD"}'

Prefixes are checked before the database is touched: a token that does not start with the right prefix is rejected as malformed, which means sending a payout key to a payment endpoint fails immediately and tells you so.

Response envelope

Every response — success or failure — has the same five top-level keys. You can branch on error === null without special-casing status codes.

Success
{
    "data": {
        "track_id": "INV_9F2K4M1Q7X"
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}
Failure
{
    "data": null,
    "message": "Payment not found.",
    "error": {
        "type": "request",
        "message": "Payment not found."
    },
    "status": 404,
    "version": "1.0"
}

Envelope fields

data The result. null on failure.
message Human-readable summary. Safe to log; do not parse it.
error null on success. Otherwise {type, message, fields?}.
status Mirrors the HTTP status code.
version API version that produced the response.

Amounts and precision

Never parse an amount as a float
Rune Pay returns every monetary value as a decimal string and computes internally with arbitrary precision. IEEE-754 doubles cannot represent 0.1 exactly, and 8-decimal BTC values exceed the safe integer range of a JavaScript number once denominated in satoshis. Use a decimal library — BigDecimal, decimal.Decimal, bcmath, big.js.

How amounts behave

Type on the wire JSON string, e.g. "24.55000000".
Trailing zeros Padded to the asset's precision, so string comparison is unreliable — compare numerically with a decimal type.
Rounding Crypto amounts round up at the asset's precision when converting from fiat, so an exactly-paid invoice is never a satoshi short.
amount vs pay_amount amount is what you charged (fiat). pay_amount is what the customer must send (crypto).
received_amount What actually arrived. Compare this against pay_amount, not against amount.

The exchange rate is locked when the payment is created and stored on it as rate. A customer who pays 20 minutes later still pays the quoted crypto amount — the volatility between quote and settlement is ours, not theirs.

Rate limits

Requests are limited per API key, at 60 per minute on the default plan. The window is a rolling 60 seconds.

429 Too Many Requests
{
    "data": null,
    "message": "Rate limit exceeded.",
    "error": {
        "type": "auth",
        "message": "Rate limit exceeded.",
        "retry_after": 34
    },
    "status": 429,
    "version": "1.0"
}

error.retry_after is the number of seconds until the window clears. Wait that long rather than retrying immediately — a tight retry loop keeps the window permanently full and locks you out for longer than backing off would.

Do not poll for payment status
Polling GET /payment/{track_id} in a loop is the usual way people hit this limit. Use callbacks: they arrive within seconds of the block confirming and cost you no requests at all. Poll only as a reconciliation sweep, minutes apart.

Idempotency and duplicates

Payments and payouts handle repeat requests differently, because the consequences of a duplicate are not the same.

Behaviour by endpoint

POST /payment/invoice Creates a new payment every call. Two calls with the same order_id produce two independent invoices — dedupe on your side if that matters.
POST /payment/static-address Idempotent per order_id. The same reference always returns the same address; a new reference mints a new one.
POST /payout Idempotent when you send order_id: it is used as the idempotency key, and a repeat returns the original payout instead of sending twice.
Always send order_id on payouts
If a payout request times out you cannot tell whether it was queued. Without order_id, retrying may send the funds twice, and an on-chain transaction cannot be recalled. With it, retrying is safe.

Testing without real coins

Send "sandbox": true when creating a payment. It runs the entire pipeline — invoice, checkout page, callbacks, status transitions — without watching a real chain, so you can build and test your integration end to end before moving any money.

Sandbox payments are not real
A sandbox payment credits nothing. Make sure your own code checks the flag before releasing goods, or a caller who sets sandbox: true against your production endpoint gets your product for free.

Reference data

Public endpoints. No API key required, no rate limit tied to a key. Cache these — they change rarely.

List supported currencies

GET /common/currencies public

Returns coins grouped by symbol, each with the networks it can be paid on. Build your checkout selector from this rather than hard-coding a list, because availability changes: a network under maintenance, or one whose provider is unreachable, reports "status": "unavailable" and must not be offered to a customer. A coin's top-level status is true when at least one of its networks is active.

data is an object keyed by coin symbol, not an array, and networks is keyed by network id. Iterate the values (Object.values(data)) rather than indexing by position. Each network also repeats its own id so a value-only iteration still knows what it is holding.

Response fields

FieldTypeDescription
id string Network identifier. This is the value you pass as network elsewhere.
contract string|null Token contract address. null for native coins.
decimals integer On-chain precision for this asset.
confirmations integer Blocks required before a payment is treated as final.
min_deposit string Below this, a payment cannot cover its own sweep cost and will not be credited.
withdrawal_fee string Flat fee deducted from a payout on this network.
needs_memo boolean Whether the chain requires a destination tag or memo.
status string active or unavailable. Only offer active networks.
Request
curl -X GET https://rpay.gg/api/v1/common/currencies
Response · 200
{
    "data": {
        "USDT": {
            "symbol": "USDT",
            "name": "Tether",
            "color": "#26a17b",
            "icon": "https://rpay.gg/assets/coins/usdt.svg",
            "status": true,
            "networks": {
                "TRC20": {
                    "id": "TRC20",
                    "network": "TRON (TRC20)",
                    "chain": "tron",
                    "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
                    "decimals": 6,
                    "confirmations": 20,
                    "min_deposit": "1",
                    "min_withdrawal": "10",
                    "withdrawal_fee": "1",
                    "needs_memo": false,
                    "status": "active"
                }
            }
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

List fiat currencies

GET /common/fiat-currencies public

You price in fiat and the customer pays in crypto; this is the list of fiat currencies you may price in. Sending an unlisted currency is rejected at creation.

Request
curl -X GET https://rpay.gg/api/v1/common/fiat-currencies
Response · 200
{
    "data": {
        "USD": {
            "symbol": "USD",
            "name": "US Dollar",
            "sign": "$"
        },
        "EUR": {
            "symbol": "EUR",
            "name": "Euro",
            "sign": "€"
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Current exchange rates

GET /common/prices public

Useful for showing an approximate crypto amount before the customer commits. It is indicative only: the binding rate is the one locked onto the payment at creation and returned as rate. Never compute the amount to charge from this endpoint — use pay_amount from the payment itself.

Request
curl -X GET https://rpay.gg/api/v1/common/prices
Response · 200
{
    "data": {
        "BTC": "64210.55",
        "USDT": "1.0002",
        "ETH": "3120.40"
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

List networks

GET /common/networks public

The same information as currencies, without the grouping. Currencies nests networks under their coin, which is what you want for a checkout selector and not what you want when the question is simply "what exactly do I put in the network field".

Keyed by network id. Only offer the ones reporting "status": "active".

Response fields

FieldTypeDescription
id string What to send as network when creating a payment or payout.
chain string The settlement layer. Assets sharing a chain share an address.
confirmations integer Blocks required before a payment is final.
status string active or unavailable.
Request
curl -X GET https://rpay.gg/api/v1/common/networks
Response · 200
{
    "data": {
        "TRC20": {
            "id": "TRC20",
            "currency": "USDT",
            "network": "TRON (TRC20)",
            "chain": "tron",
            "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
            "decimals": 6,
            "confirmations": 20,
            "min_deposit": "1",
            "min_withdrawal": "10",
            "withdrawal_fee": "1",
            "needs_memo": false,
            "status": "active"
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

System status

GET /common/status public

operational reflects whether the blockchain provider for that network is reachable and in sync. A network can be enabled but not operational — during a provider outage, for instance. Treat a non-operational network as unavailable for checkout even if it appears in the currencies list.

Request
curl -X GET https://rpay.gg/api/v1/common/status
Response · 200
{
    "data": {
        "networks": {
            "BTC": {
                "id": "BTC",
                "currency": "BTC",
                "network": "Bitcoin",
                "provider": "esplora",
                "operational": true
            },
            "TRC20": {
                "id": "TRC20",
                "currency": "USDT",
                "network": "TRON (TRC20)",
                "provider": "trongrid",
                "operational": true
            }
        },
        "time": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Payments

Taking money in. Requires a merchant key (rp_sec_…).

Create a hosted invoice

POST /payment/invoice merchant_api_key

The quickest way to take a payment. Rune Pay hosts the checkout page — coin selection, address, QR code, countdown, confirmation tracking — so you only handle the redirect and the callback.

Every field is optional except amount; anything you omit falls back to the defaults configured on the API key, so you can move settings out of your code and into the dashboard. A field sent in the request overrides the key default for that one payment.

Body parameters

ParameterTypeDescription
amount required number Amount to charge, in currency. Must be greater than 0.
currency string Fiat currency to price in. Defaults to USD.
pay_currency string Preselect the coin the customer pays in, e.g. USDT. Must be accepted by the key.
network string Preselect the network, e.g. TRC20. Takes precedence over pay_currency.
order_id string Your own reference. Returned on every callback and searchable in the dashboard. Max 100 chars.
description string Shown to the customer on the checkout page. Max 500 chars.
email string Customer email. Prefills the receipt field on checkout.
callback_url string Where to POST status changes. Overrides the key default. Must be a public HTTPS URL.
return_url string Where the customer lands after paying.
lifetime integer Minutes before the payment expires. 15–2880 (48 h). Default 60.
under_paid_coverage number Percent shortfall to still accept, 0–60. 2 accepts a payment 2% short — useful when the customer's wallet deducts its own fee.
fee_paid_by_payer boolean When true, the processing fee is added on top of the amount so you receive the full amount.
to_currency string Accepted for OxaPay compatibility and ignored: every payment is already converted to your settlement coin and delivered to your settlement address automatically. Configure both in the dashboard under Settlement.
auto_withdrawal boolean Accepted for OxaPay compatibility and ignored: settlement already forwards every confirmed payment to your own wallet — there is nothing extra to switch on.
mixed_payment boolean Allow the invoice to be settled by several transactions instead of one.
sandbox boolean Simulate the payment without watching a real chain. See Testing.

Response fields

FieldTypeDescription
track_id string Rune Pay's identifier for this payment. Store it — every other call and callback uses it.
payment_url string Redirect the customer here.
expired_at integer Unix seconds. After this the payment stops accepting funds.

When it fails

StatusMeaning
400 The amount is below the network minimum, or no exchange rate is available for the pair.
401 Missing, malformed or revoked API key.
422 A field failed validation. error.fields names which.
503 No blockchain network is currently available. Retry shortly; do not treat as fatal.
Request
curl -X POST https://rpay.gg/api/v1/payment/invoice \
  -H 'merchant_api_key: rp_sec_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"amount":25,"currency":"USD","order_id":"ORDER-1043","description":"Rune verification — 1 year","callback_url":"https://example.com/hooks/runepay","return_url":"https://example.com/thanks","lifetime":60}'
Response · 201
{
    "data": {
        "track_id": "INV_9F2K4M1Q7X",
        "payment_url": "https://rpay.gg/checkout/INV_9F2K4M1Q7X",
        "expired_at": 1754096400,
        "date": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}

Create a white-label payment

POST /payment/white-label merchant_api_key

Use this when checkout must stay inside your own interface. You get the deposit address, the exact crypto amount and a QR code URL, and you render the page.

The trade-off is that everything the hosted page does — the countdown, the "waiting for confirmations" state, partial-payment handling, expiry — becomes yours to build. pay_currency is required here because there is no page for the customer to choose a coin on.

The response still carries payment_url. You do not need it — that is the page you are replacing — but it exists for every payment, and it is the link to send somebody on their phone, or the fallback while your own page is being built.

Body parameters

ParameterTypeDescription
amount required number Amount to charge, in currency.
pay_currency required string The coin the customer will pay in, e.g. USDT.
network string Which network, when the coin exists on several. Without it, the first accepted network for that coin is used.
currency string Fiat currency to price in. Defaults to USD.
order_id string Your own reference.
description string Stored on the payment and echoed in callbacks.
email string Customer email.
callback_url string Where to POST status changes.
lifetime integer Minutes before expiry. 15–2880.
under_paid_coverage number Percent shortfall to still accept, 0–60.

Response fields

FieldTypeDescription
address string Where the customer sends funds. Unique to this payment.
pay_amount string Exactly what must arrive, at the asset's precision. Display it verbatim.
memo string|null Destination tag, when the chain needs one. If present it is mandatory — a payment without it cannot be attributed.
rate string The locked fiat→crypto rate used for this payment.
qr_code string URL of a ready-made QR image encoding address and amount.
network_id string The network identifier; network is its display name.

When it fails

StatusMeaning
400 The key does not accept that coin or network. The message names which was requested.
422 A field failed validation — commonly a missing pay_currency.
Request
curl -X POST https://rpay.gg/api/v1/payment/white-label \
  -H 'merchant_api_key: rp_sec_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"amount":25,"currency":"USD","pay_currency":"USDT","network":"TRC20","order_id":"ORDER-1044","callback_url":"https://example.com/hooks/runepay"}'
Response · 201
{
    "data": {
        "track_id": "INV_7C3P8L2V5N",
        "amount": "25.00",
        "currency": "USD",
        "pay_amount": "24.995000",
        "pay_currency": "USDT",
        "network": "TRON",
        "network_id": "TRC20",
        "address": "TSkW4NMLLp5ZLbP2NC1Fv3n1RmghJ8UtwT",
        "memo": null,
        "rate": "1.0002",
        "qr_code": "https://rpay.gg/checkout/INV_7C3P8L2V5N/qr",
        "payment_url": "https://rpay.gg/invoice/INV_9F2K4M1Q7X",
        "callback_url": "https://example.com/hooks/runepay",
        "order_id": "ORDER-1044",
        "email": null,
        "fee_paid_by_payer": 0,
        "under_paid_coverage": "0",
        "expired_at": 1754096400,
        "date": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}

Retrieve a payment

GET /payment/{track_id} merchant_api_key

Returns the same object your callback receives, so you can write one handler for both. Use it to reconcile — after a missed callback, or when a customer asks where their payment is — rather than as a polling loop.

A payment belonging to another merchant returns 404, not 403: the API does not reveal that an identifier exists.

Path parameters

ParameterTypeDescription
track_id required string The identifier returned at creation.

Response fields

FieldTypeDescription
status string See Payment statuses. Release goods on Paid.
received_amount string Total actually received across all transactions.
txs array Every transaction seen for this payment, oldest first. Empty until one arrives.
txs[].confirmations integer Confirmations so far, against required_confirmations.
paid_at integer|null When the payment became fully paid. null until then.

When it fails

StatusMeaning
404 No such payment on this account.
Request
curl -X GET https://rpay.gg/api/v1/payment/{track_id} \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "type": "invoice",
        "track_id": "INV_9F2K4M1Q7X",
        "status": "Paid",
        "amount": "25.00",
        "currency": "USD",
        "pay_amount": "24.995000",
        "pay_currency": "USDT",
        "network": "TRC20",
        "address": "TSkW4NMLLp5ZLbP2NC1Fv3n1RmghJ8UtwT",
        "received_amount": "24.995000",
        "rate": "1.0002",
        "order_id": "ORDER-1043",
        "email": "buyer@example.com",
        "description": "Rune verification — 1 year",
        "fee_paid_by_payer": 0,
        "under_paid_coverage": "0",
        "txs": [
            {
                "tx_hash": "9f2c…a41b",
                "amount": "24.995000",
                "currency": "USDT",
                "network": "TRC20",
                "confirmations": 20,
                "required_confirmations": 20,
                "status": "confirmed",
                "received_at": 1754093100
            }
        ],
        "date": 1754092800,
        "paid_at": 1754093100
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

List payments

GET /payment merchant_api_key

Newest first. Combine filters freely — they AND together. Intended for reconciliation and reporting; for live status use callbacks.

Query parameters

ParameterTypeDescription
size integer Results per page, 1–200. Default 25. Values outside the range are clamped, not rejected.
page integer Page number, starting at 1.
order_id string Exact match on your reference.
type string Filter by kind: invoice, white_label or static_address.
currency string Filter by the crypto paid in, e.g. USDT.
from_date integer Unix seconds. Only payments created at or after this.
to_date integer Unix seconds. Only payments created at or before this.

Response fields

FieldTypeDescription
meta.total integer Total matching payments across all pages.
meta.pages integer Last page number. Stop when page reaches it.
Request
curl -X GET https://rpay.gg/api/v1/payment \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "list": [
            "…payment objects, as returned by Retrieve a payment…"
        ],
        "meta": {
            "page": 1,
            "size": 25,
            "total": 143,
            "pages": 6
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Payment statistics

GET /payment/statistics merchant_api_key

Aggregates your payments so a dashboard does not have to page the whole history and add it up itself. Defaults to the last thirty days.

Volume counts confirmed payments only. Including money that has been promised but not confirmed is how a dashboard ends up flattering the merchant, and the figure would move backwards whenever something expired.

Query parameters

ParameterTypeDescription
from_date integer Unix seconds. Defaults to thirty days ago.
to_date integer Unix seconds. Defaults to now.

Response fields

FieldTypeDescription
total integer Payments created in the window, whatever their outcome.
counts object Payments by outcome. confirming covers both the paid and confirming states.
volume string Confirmed volume, in each payment's own fiat currency. Decimal string.
conversion_rate number|null Paid as a percentage of payments that REACHED a final state — invoices still open are excluded, or an account with customers mid-checkout would look broken. null when nothing finished.
by_currency object Confirmed count and volume per coin, keyed by symbol.
Request
curl -X GET https://rpay.gg/api/v1/payment/statistics \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "from_date": 1751500800,
        "to_date": 1754092800,
        "total": 143,
        "counts": {
            "paid": 118,
            "waiting": 4,
            "confirming": 2,
            "underpaid": 1,
            "expired": 16,
            "cancelled": 2,
            "failed": 0
        },
        "volume": "38207.85",
        "conversion_rate": 86.76,
        "by_currency": {
            "USDT": {
                "count": 96,
                "volume": "29180.40"
            },
            "BTC": {
                "count": 22,
                "volume": "9027.45"
            }
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Cancel a payment

POST /payment/{track_id}/cancel merchant_api_key

Only a payment that has not yet received money can be cancelled. Once funds are on the way — even unconfirmed — the payment is in a final state and this returns 409, because the coins are real and cannot be un-sent.

Cancelling is optional: a payment expires on its own at expired_at. Cancel explicitly when the customer abandons checkout, so your dashboard stays clean.

Path parameters

ParameterTypeDescription
track_id required string The payment to cancel.

When it fails

StatusMeaning
404 No such payment on this account.
409 The payment is paid, expired or already cancelled.
Request
curl -X POST https://rpay.gg/api/v1/payment/{track_id}/cancel \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": [
        "…the updated payment object, with status \"Cancelled\"…"
    ],
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Get balances

GET /balance merchant_api_key

The same figures as the payout balance, but reachable with a MERCHANT key.

That distinction is the point: a merchant who only takes payments can hold no payout key at all — the credential that could move money out — and still see what they have. Reading a balance is not spending it.

Response fields

FieldTypeDescription
available string Free to withdraw or spend.
locked string Reserved by a payout already queued. Still yours, already committed.

When it fails

StatusMeaning
409 The custodial wallet is disabled, so Rune Pay holds nothing to report.
Request
curl -X GET https://rpay.gg/api/v1/balance \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "USDT": {
            "available": "4210.55000000",
            "locked": "100.00000000"
        },
        "BTC": {
            "available": "0.04120000",
            "locked": "0.00000000"
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Create a static address

POST /payment/static-address merchant_api_key

Unlike an invoice, a static address has no amount and no expiry. Anything sent to it is credited (minus your key's payment fee) and reported by callback. This is how you build user-balance top-ups: mint one address per user, store it, and let them fund it repeatedly.

The order_id is the identity. Calling again with the same order_id and network returns the same address with a 200 — retries are free and safe. A different order_id mints a new address with a 201. Omitting it entirely gives you one standing address per network. So for a platform with many users, pass each user's ID as order_id and every user gets an address of their own.

Body parameters

ParameterTypeDescription
network required string Which network to derive on, e.g. TRC20. Must be enabled and accepted by the key.
callback_url string Where to POST each incoming payment.
order_id string Your reference, and the reuse key — typically your user's ID. Same reference, same address; new reference, new address. Omitted means one shared standing address per network.
email string Associated email.
description string Free-text label.
to_currency string Auto-convert arrivals to this currency, priced at NEAR Intents rather than a market feed.

When it fails

StatusMeaning
400 The network is not available on this API key.
409 The custodial wallet is disabled. Static addresses require Rune Pay to derive and watch the address itself.
422 The network is not in the enabled catalogue.
429 The account is at its static-address cap (10,000 by default). Revoke unused addresses or contact support to raise it.
500 Address derivation failed. The message carries the reason; nothing is stored.
Request
curl -X POST https://rpay.gg/api/v1/payment/static-address \
  -H 'merchant_api_key: rp_sec_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"network":"TRC20","order_id":"user-8842","callback_url":"https://example.com/hooks/topup","description":"Balance top-up"}'
Response · 201
{
    "data": {
        "track_id": "SA_K3M9P2X7Q4L8VZ",
        "network": "TRON",
        "network_id": "TRC20",
        "currency": "USDT",
        "address": "TQ5nT8vRvZ2xKpL9mWc4bHfJ7dYaE1sGu3",
        "memo": null,
        "callback_url": "https://example.com/hooks/topup",
        "order_id": "user-8842",
        "email": null,
        "description": "Balance top-up",
        "status": "active",
        "date": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}

List static addresses

GET /payment/static-address merchant_api_key

Includes revoked ones, so check status. There is no pagination here — the result is capped at 200; keep your own mapping of user to address rather than searching this list at runtime.

Request
curl -X GET https://rpay.gg/api/v1/payment/static-address \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "list": [
            "…static address objects…"
        ]
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Revoke a static address

POST /payment/static-address/{track_id}/revoke merchant_api_key

Marks it revoked and stops watching it.

The address still exists on the blockchain and anyone who kept it can still send to it. Those funds will not be credited or reported. Stop showing a revoked address to customers before you revoke it, not after.

Path parameters

ParameterTypeDescription
track_id required string The static address identifier.

When it fails

StatusMeaning
404 No such static address on this account.
Request
curl -X POST https://rpay.gg/api/v1/payment/static-address/{track_id}/revoke \
  -H 'merchant_api_key: rp_sec_YOUR_KEY'
Response · 200
{
    "data": {
        "track_id": "SA_K3M9P2X7Q4L8VZ",
        "status": "revoked"
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Payouts

Sending money out. Requires a payout key (rp_out_…) and the custodial wallet.

Get balances

GET /payout/balance payout_api_key

available is what you can pay out right now. locked is reserved by payouts already queued or awaiting approval — it is still yours, but already committed. Check available before queueing a payout, not the sum.

Request
curl -X GET https://rpay.gg/api/v1/payout/balance \
  -H 'payout_api_key: rp_out_YOUR_KEY'
Response · 200
{
    "data": {
        "USDT": {
            "available": "4210.55000000",
            "locked": "100.00000000"
        },
        "BTC": {
            "available": "0.04120000",
            "locked": "0.00000000"
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Create a payout

POST /payout payout_api_key

The funds are locked immediately and the payout enters the same pipeline as a dashboard withdrawal — including approval, if your account requires it. This queues a payout; it does not bypass any control.

The destination address is not recoverable if wrong. Rune Pay validates the format for the network, but cannot know whether the address is the one you meant. Validate against your own records before calling.

Body parameters

ParameterTypeDescription
address required string Destination address. 20–120 characters, validated for the network.
currency required string Currency to send, e.g. USDT.
amount required number Amount to send, before fees. Must exceed the network minimum.
network string Which network, when the coin exists on several. Strongly recommended — sending USDT to a TRON address over ERC20 loses the funds.
order_id string Idempotency key. Repeating a request with the same value returns the original payout instead of sending twice.
callback_url string Where to POST payout status changes.
description string Internal note, echoed in callbacks.

Response fields

FieldTypeDescription
status string Coarse public status. See Payout statuses.
internal_status string Finer-grained stage, e.g. awaiting_approval, approved, sent. Useful for support; do not build logic on it.
fee string Network fee deducted.
net_amount string What actually arrives at the destination: amount minus fee.
tx_hash string|null On-chain hash. null until broadcast.

When it fails

StatusMeaning
400 Insufficient available balance, amount below the minimum, invalid address for the network, or no available network for that currency.
409 The custodial wallet is disabled. In gateway mode funds settle straight to your own wallet, so there is no balance to pay out from.
422 A field failed validation.
Request
curl -X POST https://rpay.gg/api/v1/payout \
  -H 'payout_api_key: rp_out_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"address":"TSkW4NMLLp5ZLbP2NC1Fv3n1RmghJ8UtwT","currency":"USDT","network":"TRC20","amount":100,"order_id":"PAYOUT-556","callback_url":"https://example.com/hooks/payout"}'
Response · 201
{
    "data": {
        "track_id": "WD_4X8N2K9P5M",
        "status": "Processing",
        "internal_status": "awaiting_approval",
        "amount": "100.00000000",
        "currency": "USDT",
        "network": "TRON",
        "address": "TSkW4NMLLp5ZLbP2NC1Fv3n1RmghJ8UtwT",
        "tx_hash": null,
        "fee": "1.00000000",
        "net_amount": "99.00000000",
        "error": null,
        "date": 1754092800
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 201,
    "version": "1.0"
}

Retrieve a payout

GET /payout/{track_id} payout_api_key

Watch tx_hash to appear once broadcast, and status to reach Confirmed. On failure, error carries the reason — a rejection note from an approver, or the broadcast error.

Path parameters

ParameterTypeDescription
track_id required string The payout identifier.

When it fails

StatusMeaning
404 No such payout on this account.
Request
curl -X GET https://rpay.gg/api/v1/payout/{track_id} \
  -H 'payout_api_key: rp_out_YOUR_KEY'
Response · 200
{
    "data": [
        "…the payout object, as returned at creation…"
    ],
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

List payouts

GET /payout payout_api_key

Newest first. Same pagination shape as the payment list.

Query parameters

ParameterTypeDescription
size integer Results per page, 1–200. Default 25.
page integer Page number, starting at 1.
currency string Filter by currency.
status string Filter by internal status, e.g. sent, confirmed, rejected.
Request
curl -X GET https://rpay.gg/api/v1/payout \
  -H 'payout_api_key: rp_out_YOUR_KEY'
Response · 200
{
    "data": {
        "list": [
            "…payout objects…"
        ],
        "meta": {
            "page": 1,
            "size": 25,
            "total": 38,
            "pages": 2
        }
    },
    "message": "Operation completed successfully",
    "error": null,
    "status": 200,
    "version": "1.0"
}

Callbacks

When a payment or payout changes state, Rune Pay POSTs the full object to your callback_url. This is the intended way to learn about payments — it is immediate, and it costs you no rate limit.

What we send

Method POST
Content-Type application/json
User-Agent RunePay-Callback/1.0
HMAC header Hex HMAC-SHA512 of the raw request body.
Body The same object GET /payment/{track_id} returns, plus type.
Expected reply Any 2xx. Anything else is a failure and will be retried.
Redirects Not followed. Point callback_url at the final URL.
Timeout 15 seconds.

Callback types

The type field names what the callback is about: invoice, white_label, handle, payout — and static_address, whose payload differs from the others because there is no invoice behind it. A static-address callback carries: track_id (the SA_… id), your order_id, address, network, currency, amount (what arrived), fee, credited_amount (what your balance gained), tx_hash, from_address, confirmations, status (always Paid — it fires only on credit) and date. One callback per on-chain transaction.

The signing secret

The signing secret is the SHA-256 hex digest of your API key — not the key itself. Rune Pay stores only that digest, so it can sign with a value it holds while the raw key stays exclusively yours. Compute it once and store it in your environment: echo -n "rp_sec_YOUR_KEY" | sha256sum.

Verify against the raw body
Verify against the raw request body, before any JSON parsing. Re-encoding the parsed object produces different bytes — different key order, different spacing — and the signature will never match. Compare with a constant-time function: == on strings leaks timing information that can be used to forge a signature byte by byte.

Retries

A failed delivery is retried five times with growing delays: 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours. After the sixth total attempt the callback is marked exhausted and no further attempt is made — reconcile with GET /payment if you suspect you missed one.

Handle duplicates
Callbacks may arrive more than once for the same state — a retry after your server accepted but timed out, for example. Make your handler idempotent: key on track_id and ignore a status you have already processed. Delivering an order twice is worse than delivering it late.

URL restrictions

Callback URLs must be public. Requests to localhost, loopback and private ranges (10.x, 172.16–31.x, 192.168.x, 169.254.x) are refused outright, so an attacker who sets a callback URL cannot make our servers probe an internal network.

If you are getting 401s
The three mistakes behind almost every 401 seen in the wild: (1) verifying with the raw API key — the secret is its SHA-256 hex digest, see above. (2) looking for a Stripe-style header — there is no t=…,v1=… scheme here; the header is literally named HMAC and contains plain hex. (3) verifying a re-encoded body — the signature covers the raw bytes as delivered. If your endpoint rejects us, our retries stop after six attempts and the payment still exists: reconcile with GET /payment.
Verify — PHP
<?php
// The secret is the SHA-256 of your API key, not the key itself.
$secret = hash('sha256', getenv('RUNEPAY_API_KEY'));

$raw       = file_get_contents('php://input');   // raw bytes, never $_POST
$signature = $_SERVER['HTTP_HMAC'] ?? '';

if (! hash_equals(hash_hmac('sha512', $raw, $secret), $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);

// Only 'Paid' means the money is final and confirmed.
if ($event['status'] === 'Paid') {
    fulfilOrder($event['order_id'], $event['track_id']);
}

http_response_code(200);   // any 2xx stops the retries

Statuses

Release goods on Paid, and only on Paid
Confirming means the money arrived but is not yet buried under enough blocks to be irreversible. A chain reorganisation can still take it back. The wait exists precisely so you are not the one who absorbs that.

Payment statuses

Waiting Created, nothing received yet. The customer has until expired_at.
Underpaid Funds arrived but less than pay_amount, beyond what under_paid_coverage forgives. Do not release goods; refund or ask for the difference.
Confirming The full amount arrived and is waiting for confirmations. Not yet final — a chain reorganisation can still reverse it.
Paid Confirmed and final. This is the only status on which to release goods.
Expired The window closed with no payment. Anything arriving now is late and will not be credited automatically.
Cancelled Cancelled before any funds arrived.
Failed Something went wrong. Check the dashboard or contact support.

Payout statuses

Processing Queued, funds locked. May be awaiting approval.
Confirming Broadcast to the network. tx_hash is now populated.
Confirmed Confirmed on-chain and final.
Failed Rejected by an approver or failed to broadcast. error says why; the locked funds are released.

Errors

Errors use standard HTTP status codes. error.type is auth for credential and rate-limit problems, request for anything else under 500, and server at 500 and above.

CodeMeaning
200
OK
The request succeeded.
201
Created
A payment, static address or payout was created.
400
Bad Request
The request was understood but cannot be fulfilled — amount below the minimum, unsupported network for this key, insufficient balance. The message says which. Retrying unchanged will not help.
401
Unauthorized
Missing, malformed, revoked or unknown API key. Check the prefix matches the endpoint family.
403
Forbidden
The key is valid but the project or merchant account is inactive.
404
Not Found
No such resource on this account. Also returned for resources belonging to another merchant, deliberately.
409
Conflict
The resource is in a state that forbids the action — cancelling a paid payment, or using a payout endpoint in gateway mode.
422
Unprocessable Entity
Validation failed. error.fields maps each field to its problems.
429
Too Many Requests
Rate limit exceeded. Wait error.retry_after seconds.
500
Server Error
Something failed on our side. Safe to retry with backoff; nothing was created.
503
Service Unavailable
No blockchain network is currently available. Transient — retry shortly.

Validation errors name the field

422
{
    "data": null,
    "message": "The amount field is required.",
    "error": {
        "type": "request",
        "message": "The amount field is required.",
        "fields": {
            "amount": [
                "The amount field is required."
            ]
        }
    },
    "status": 422,
    "version": "1.0"
}

Rune Pay API v1.0 · OpenAPI · Postman · Markdown