tgpay cryptoAPI
crypto-payapiinvoicesrefunds

API reference: invoices and refunds

5 min readUpdated Aug 22, 2026

The invoice methods: createInvoice, getInvoices, deleteInvoice, refundInvoice. Conventions (auth, envelope, amounts, spend_id) are on the Merchant API reference page; the guided walkthrough is Accepting payments with invoices.

createInvoice

POST /pay/api/createInvoice — scope invoices, limit 60 per minute.

ParameterTypeRequiredMeaning
currency_typestringnocrypto (default) or fiat
assetstringcrypto modethe asset to charge, e.g. USDT. Not allowed together with fiat
fiatstringfiat modethe fiat currency the price is in (the is_fiat rows of getCurrencies)
accepted_assetsstring / arraynofiat mode only: the assets the payer may pay with — a comma-separated string ("USDT,GRAM") or a JSON array. Omitted = all supported assets
amountstringyes, unless open_amount is setpositive decimal string: asset units in crypto mode, fiat units (max 2 decimal places) in fiat mode
open_amountbooleannoextension, crypto mode only: no fixed amount — the payer enters one at pay time (donations and tips). Mutually exclusive with amount
descriptionstringnoup to 1024 characters, shown to the payer
hidden_messagestringnoup to 2048 characters, revealed to the payer only after payment
payloadstringnoup to 4096 characters of your own data, echoed back on the invoice and webhook
allow_commentsbooleannolet the payer attach a comment (default true)
allow_anonymousbooleannolet the payer hide their identity (default true)
paid_btn_namestringnopost-payment button: viewItem, openChannel, openBot, or callback
paid_btn_urlstringnothe button’s http(s) URL — required when paid_btn_name is set
swap_tostringnoauto-swap received payments into this asset. Best-effort: if the swap can’t run at pay time, the payment still succeeds unswapped
expires_inintegernoseconds until the invoice expires, up to 2678400 (31 days); omitted or 0 = never
rate_lock_secondsintegernoextension, fiat mode only: freeze the conversion rates at creation for this window (see below)

The result — and the payload of the invoice_paid webhook — is the invoice object. Its key fields:

  • Identity and state: invoice_id, hash (the public id inside pay_url), status (active / paid / expired), pay_url — the t.me link you send the payer (bot_invoice_url, mini_app_invoice_url, web_app_invoice_url are aliases of it).
  • Amounts: amount (the face value — fiat units on a fiat invoice, crypto otherwise; null while an open-amount invoice is unpaid), amount_minor, and on paid fiat invoices paid_asset / paid_amount / paid_fiat_rate — the crypto actually charged and the rate used.
  • Fee: fee_asset / fee_amount, stamped at payment — the authoritative figure for your books (fee and usd_rate are deprecated Crypto Bot aliases). See fees and limits.
  • Payer: paid_by_user_id (null when the payer chose anonymity), paid_anonymously, comment.
  • Refunds (extension): refunded_amount / refunded_minor (cumulative) and refunded_at, stamped once fully refunded.
  • Rate lock (extension): rate_lock_until and rate_lock_rates — the stamped per-asset rates, null when no lock was requested.
  • As created: description, hidden_message, payload, paid_btn_name / paid_btn_url, expiration_date (expires_at is an alias), the swap fields (swap_to, is_swapped, swapped_to, swapped_rate, swapped_output, …).

Errors: 400 invalid_currency (asset/fiat mixed up), 400 invalid_amount, 404 unknown_asset, 400 unsupported_fiat, 400 paid_btn_url_required, and for rate-lock requests 400 rate_lock_fiat_only, 409 ratelock_disabled, 409 rate_unavailable (no fresh rate for an accepted asset — retry).

How invoices get paid

The payer pays from their wallet balance in the Mini App — instantly, with no network fee. A payer can also fund the invoice from an external wallet: the app shows them a deposit address, their transfer lands in their own wallet, and the invoice settles automatically once it arrives. Either way you see the same thing: a normal paid invoice and an invoice_paid webhook — there are no extra parameters or fields to handle.

On a fiat invoice the crypto amount is computed at payment time, rounded up in your favor, so you never receive less than the fiat face value. If no fresh rate is available, the payment fails on the payer’s side rather than settling at a stale rate.

Locking the rate on a fiat invoice

Pass rate_lock_seconds to stamp the current rate of every accepted asset at creation. While the lock is live the payer sees exactly the stamped amounts, and the payment converts at the stamped rate — you take the rate risk for the window. The server clamps the window between 60 seconds and the platform maximum (currently 15 minutes).

When the lock lapses, the invoice stays payable and quietly reverts to pay-time conversion. If you want the invoice to die with the quote, set expires_in to the same value.

getInvoices

GET /pay/api/getInvoices — scope read. Filters: asset, fiat, invoice_ids (comma-separated), status (active / paid / expiredexpired is an extension; active excludes invoices already past their deadline), plus offset / count. Returns {"items": [invoice, …]}, newest first.

deleteInvoice

POST /pay/api/deleteInvoice — scope invoices. One parameter: invoice_id. Cancels an unpaid invoice and returns true. Errors: 404 invoice_not_found, 409 invoice_already_paid — a paid invoice can’t be deleted, the money already moved.

refundInvoice

POST /pay/api/refundInvoice — scope refunds, limit 30 per minute. An extension over Crypto Bot: returns a paid invoice’s face amount — or part of it — from your app balance to whoever paid it, anonymous payers included, without revealing who they were.

ParameterTypeRequiredMeaning
invoice_idintegeryesthe paid invoice
amountstringnothe amount to refund, in the invoice’s asset. Omitted = the full unrefunded remainder. Partial refunds accumulate up to the face amount
spend_idstringnoidempotency key — use one, so a timed-out retry replays instead of refunding twice

The result is the updated invoice object with the cumulative refunded_amount / refunded_minor; refunded_at is stamped once the invoice is fully refunded. The status stays paid. The platform fee is not returned. Each refund fires the opt-in refund_completed webhook.

Errors: 404 invoice_not_found, 409 invoice_not_paid, 409 already_refunded (nothing left to refund), 409 amount_too_big (more than the unrefunded remainder), 409 insufficient_funds, 400 invalid_amount, and the spend_id pair 409 idempotency_conflict / 409 idempotency_in_progress.