tgpay cryptoAPI
crypto-payapireferencetokens

Merchant API reference

8 min readUpdated Oct 22, 2026

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.
ScopeMethods it unlocks
(any valid token)getMe, getCurrencies, getExchangeRates
readgetBalance, getStats, getInvoices, getChecks, getTransfers, getSubscriptionPlans, getSubscriptions
invoicescreateInvoice, deleteInvoice
refundsrefundInvoice
payoutstransfer, transferBatch
checkscreateCheck, deleteCheck
subscriptionscreateSubscriptionPlan, archiveSubscriptionPlan, cancelSubscription
webhooksupdateApp

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_modeWhat transfer, transferBatch and createCheck may do
anypay any user, hold a payout for an unknown @username, create bearer checks — the default, and the Crypto Bot behavior
allowlistpay 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
offnothing — 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 GET with query-string parameters. Money-moving methods are POST only — parameters as a JSON body, form-urlencoded, or query params (the body wins on conflict); multipart/form-data is 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 on error.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 carry amount_minor (extension): the integer minor-unit value as a string, because wei-scale integers can exceed the exact integer precision of a JavaScript Number.
  • Per-asset decimals come from getCurrencies — 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:

MethodLimit
createInvoice, createCheck60 per minute
refundInvoice30 per minute
transfer30 per minute
transferBatch10 per minute
updateApp10 per minute
createSubscriptionPlan20 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

HTTPerror.nameWhen
401unauthorizedmissing, invalid, or revoked token
403scope_requiredthe token lacks the method’s scope
403payouts_disabledthe app’s payout mode is off (payout methods only)
403recipient_not_allowedthe recipient is not on the app’s recipient list (payout methods only)
400invalid_requestunparsable or invalid parameters (POST)
403daily_cap_exceededthe app’s daily payout limit would be exceeded
403verification_requiredthe owner’s outgoing operations are restricted; affects transfers, checks and refunds
503merchant_payouts_pausedapp-balance debits are temporarily paused
429rate_limitedrate limit exceeded
503maintenanceplatform maintenance (write methods)
500internal_errorunexpected 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.