Merchant API reference
The conventions every method shares, plus the read-only catalog methods. Method-by-method pages: invoices and refunds, transfers and checks, subscriptions, webhooks.
The API is Crypto Bot-compatible: an existing Crypto Bot integration works after changing only the base URL and the token. Everything beyond that contract is marked extension below.
A machine-readable OpenAPI 3.1 spec of the whole API — every method, object, error name, and webhook — is published alongside these pages: crypto-pay-openapi.yaml · crypto-pay-openapi.json. Feed it to code generators, API clients, or your AI tools.
Base URL and authentication
All methods live at https://app.tgpaycrypto.com/pay/api/<methodName>.
On the testnet the base is
https://testnet.tgpaycrypto.com/pay/api — separate apps and tokens, coins
with no value.
Authenticate every request with the token in the TgPayCrypto-API-Token
header (TgCryptoPay-API-Token, the name before 2026-09-07, and
Crypto-Pay-API-Token are accepted as compatibility aliases; if several
are sent, the canonical one wins). A missing, invalid, or revoked token — or
a token of a deleted app — returns 401 unauthorized.
The token has the form <app_id>:<secret> and is shown once, at creation or
rotation; the server stores only its hash. See
Getting started as a developer.
Tokens and scopes
Two kinds of credentials authenticate identically:
- The main token — full access to every method, and the only key that signs webhooks.
- Restricted tokens (extension) — up to 10 live per app, created in More → Merchant API under Restricted tokens, each with a label and a subset of scopes. Revoking one is instant and doesn’t touch the others; rotating the main token doesn’t touch them either.
| Scope | Methods it unlocks |
|---|---|
| (any valid token) | getMe, getCurrencies, getExchangeRates |
read | getBalance, getStats, getInvoices, getChecks, getTransfers, getSubscriptionPlans, getSubscriptions |
invoices | createInvoice, deleteInvoice |
refunds | refundInvoice |
payouts | transfer, transferBatch |
checks | createCheck, deleteCheck |
subscriptions | createSubscriptionPlan, archiveSubscriptionPlan, cancelSubscription |
Scopes are additive and independent — invoices does not imply read, so a
server that only creates invoices can hold a token that can’t read anything.
Calling a method the token doesn’t cover returns 403 scope_required.
getMe reports the current token’s scopes (null for the main token) and
token_name, so you can always check what you’re holding.
Payout mode
A scope says which methods a token may call; the app’s payout mode
says who any of its tokens — the main one included — may pay out to.
A new app pays anyone from the first minute; the owner may tighten that
in More → Merchant API → Payouts through the API as a fence against
a leaked token, the way the daily limit is. Loosening it back asks for
their PIN or Face ID; the API can only read it (getMe → payout_mode).
payout_mode | What transfer, transferBatch and createCheck may do |
|---|---|
any | pay any user, park a payout for an unknown @username, create bearer checks — the default, and the Crypto Bot behaviour |
allowlist | pay only the Telegram users on the app’s recipient list. Anyone else, an unknown @username (no escrow) and a check that isn’t pinned to a listed user return 403 recipient_not_allowed |
off | nothing — every payout call returns 403 payouts_disabled; invoices, subscriptions, refunds and webhooks work as usual |
If a payout is rejected, the owner is notified in Telegram and can change the mode. The daily payout limit applies on top of the mode. What you may pay for is set by the Merchant API terms.
Requests and responses
- Read methods are
GETwith query-string parameters. Money-moving methods arePOSTonly — parameters as a JSON body, form-urlencoded, or query params (the body wins on conflict);multipart/form-datais rejected. - Every response is JSON with the same envelope:
success
{"ok": true, "result": …}, error{"ok": false, "error": {"code": <HTTP status>, "name": "<error_name>"}}. Branch onerror.name— it’s the stable machine-readable string. - One edge case: a type-invalid query value on a GET method (e.g.
offset=abc) returns HTTP 422 with a{"detail": …}body outside the envelope. Send well-typed query values.
Amounts
- Crypto amounts are decimal strings in whole-coin units (
"10.5") — never JSON numbers. Responses also carryamount_minor(extension): the integer minor-unit value as a string, because wei-scale integers overflow a JavaScriptNumber. - Per-asset
decimalscome fromgetCurrencies— drive your amount math from there instead of hardcoding. - Fiat amounts have at most 2 decimal places.
- Rates are fixed-point strings, never numbers.
Pagination
List methods (getInvoices, getChecks, getTransfers,
getSubscriptions) take offset (default 0) and count (default 100, max
1000; getSubscriptions max 500) and return {"items": […]}, newest
first. ID filters (invoice_ids, check_ids, transfer_ids) are
comma-separated integer lists. Timestamps everywhere are ISO 8601 strings.
Idempotency: spend_id
Methods that move money take a caller-generated spend_id key (1–64
characters): required on transfer and each transferBatch item, optional
on createCheck and refundInvoice (extension — use it anyway). Retrying
with the same key and the same parameters replays the original result
instead of moving funds twice, so a timed-out request is always safe to
retry. The same key with different parameters returns
409 idempotency_conflict; a retry while the original is still executing
returns 409 idempotency_in_progress.
Rate limits
Per app, shared across all its tokens; exceeding returns
429 rate_limited:
| Method | Limit |
|---|---|
createInvoice, createCheck | 60 per minute |
refundInvoice, transfer | 30 per minute |
transferBatch | 10 per minute |
Read methods are not rate-limited. During platform maintenance, write
methods return 503 maintenance while reads keep working.
Catalog and account methods
getMe
GET /pay/api/getMe — no parameters. Returns the app’s identity:
app_id, name, payment_processing_bot_username, webhook_url,
webhook_events (the extended webhook types the app opted into), the
token introspection fields scopes / token_name described above, and
payout_mode (off / allowlist / any).
getBalance
GET /pay/api/getBalance — no parameters. Returns an array with one row
per supported asset, even at zero: currency_code, available (spendable
balance), onhold (funds locked in your outstanding checks), and
amount_minor.
getCurrencies
GET /pay/api/getCurrencies — no parameters. The authoritative list of
what the API supports: crypto rows (is_blockchain: true) and the fiat
currencies invoices can be priced in (is_fiat: true). Each row carries
code, name, decimals, and the is_stablecoin flag.
getExchangeRates
GET /pay/api/getExchangeRates — no parameters. Crypto-to-fiat quotes:
source, target, rate (a fixed-point string), and is_valid — false
means the whole table is served from a stale cache; treat those rates as
indicative only.
getStats
GET /pay/api/getStats — optional start_at / end_at (ISO 8601; the
default window is the last 24 hours). Returns volume (USD value of paid
invoices in the window), conversion (paid/created, percent),
unique_users_count, created_invoice_count, paid_invoice_count, and
the effective window bounds. An unparsable date returns
400 invalid_date.
Errors every method can return
| HTTP | error.name | When |
|---|---|---|
| 401 | unauthorized | missing, invalid, or revoked token |
| 403 | scope_required | the token lacks the method’s scope |
| 403 | payouts_disabled | the app’s payout mode is off (payout methods only) |
| 403 | recipient_not_allowed | the recipient is not on the app’s recipient list (payout methods only) |
| 400 | invalid_request | unparsable or invalid parameters (POST) |
| 429 | rate_limited | rate limit exceeded |
| 503 | maintenance | platform maintenance (write methods) |
| 500 | internal_error | unexpected server error |
Method-specific errors are listed on each method’s page.
Was this article helpful?
Thanks for the feedback.