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 follows Crypto Bot conventions for the methods documented here. Change the base URL and token, then test the methods your integration uses on testnet. Our additions are marked extension below.
A machine-readable OpenAPI 3.1 spec is also available: crypto-pay-openapi.yaml · crypto-pay-openapi.json. Feed it to code generators, API clients, or your AI tools, but check the method pages too: the schema may lag behind newer extensions.
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, test funds
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
an expired restricted token, a token of a deleted app, or a token whose
owner is blocked — 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 unrevoked tokens per app, including expired ones. Open More → Merchant API, choose the app, then Access and tokens. Create them 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. A token can also have an expiry; after that it no longer authenticates.
| 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 |
webhooks | updateApp |
Scopes are additive and independent — invoices does not imply read, so a
server that only creates invoices can use a token without access to balances or payment history.
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 by opening the app, then Payout settings → Payouts through the API. Like the daily limit, this limits what a leaked token can do. Loosening the restriction asks for
their PIN or biometrics; the API can only read it (getMe → payout_mode).
payout_mode | What transfer, transferBatch and createCheck may do |
|---|---|
any | pay any user, hold a payout for an unknown @username, create bearer checks — the default, and the Crypto Bot behavior |
allowlist | pay only listed Telegram users. Every pinned or named check recipient must be listed. Other recipients, unknown @usernames and bearer checks 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. - Responses produced by the API handlers use a JSON 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. - A malformed query value on a GET method (for example,
offset=abc) can be rejected before the API handler runs. Check the HTTP status and do not assume every error response has the envelope above. Send query values with the expected types.
Amounts
- Crypto amounts are decimal strings in units of the asset (
"10.5") — never JSON numbers. Responses also carryamount_minor(extension): the integer minor-unit value as a string, because wei-scale integers can exceed the exact integer precision of 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 | 30 per minute |
transfer | 30 per minute |
transferBatch | 10 per minute |
updateApp | 10 per minute |
createSubscriptionPlan | 20 per minute |
Additional request limits may apply. On 429 rate_limited, increase the
delay before retrying. 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),
daily_payout_max_usd_cents (0 = no owner-set limit) and
daily_payout_spent_usd_cents (spend in the rolling 24-hour window).
updateApp
POST /pay/api/updateApp — scope webhooks. Optional parameters:
name (1–64 characters), webhook_url (a public HTTPS URL; an empty
string removes it), and webhook_events (an array or comma-separated list
of supported extended event names). An empty event list leaves only
invoice_paid. Returns the same app fields as getMe.
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 (unclaimed check funds and pending @username payouts), 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.
Treat these rates as indicative. is_valid: true does not guarantee that
a quote is current. If rates cannot be obtained, the result may be an
empty array. Use the invoice’s recorded amounts and rates to reconcile
an actual payment.
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 paid in the window, estimated at current rates; unpriced assets
count as zero), 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) |
| 403 | daily_cap_exceeded | the app’s daily payout limit would be exceeded |
| 403 | verification_required | the owner’s outgoing operations are restricted; affects transfers, checks and refunds |
| 503 | merchant_payouts_paused | app-balance debits are temporarily paused |
| 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.
Errors without the JSON envelope
A plain-text or HTML error alone does not identify which layer returned it. Check the HTTP status, response body and response headers.
If Cloudflare returns error code: 1010, it has rejected the client based on its browser signature before the request reaches the API. Token scopes are not the cause of this Cloudflare error. A missing or nonstandard User-Agent can trigger network protection. Try a descriptive User-Agent header, such as MyShop/1.0, and retry; this is not a guaranteed fix. If the error persists, send support the HTTP status, response body and response headers, with tokens and cookies removed.
Was this article helpful?
Thanks for the feedback.