tgpay cryptoAPI
crypto-payapiinvoicesrefunds

API reference: invoices and refunds

6 min readUpdated Oct 9, 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. Use this instead of 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 = the default lifetime. Read expiration_date from the result
rate_lock_secondsintegernoextension, fiat mode only: freeze the conversion rates at creation for this window (see below)

Description, after-payment content and the button may be hidden from payers while the content is under review or has not been approved. The invoice amount remains payable.

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, recorded 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, set once fully refunded.
  • Rate lock (extension): rate_lock_until and rate_lock_rates — the recorded 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, 400 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, subject to sufficient funds and payment confirmation. A payer can also fund the invoice from an external wallet: when this option is available, the app shows a deposit address and a payment deadline. The deposit credits the payer’s own wallet. Automatic payment requires a deposit to that address, enough available balance, no outgoing-payment restriction, and an unexpired payment request and invoice. A late deposit stays in the payer’s wallet; it does not prove the invoice was paid. Either way you see the same thing: a normal paid invoice and an invoice_paid event if a webhook URL is configured — there are no extra parameters or fields to handle.

On a fiat invoice the crypto amount is computed at payment time, rounded up to the smallest unit of the chosen asset. The merchant fee is then deducted from that crypto amount. If no valid locked rate or 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 adjusts the requested duration to the supported range. Read rate_lock_until from the result to find the actual deadline.

When the lock expires, an unexpired invoice remains payable at the rate at payment time. To have both deadlines coincide, use an expires_in within the supported lock range and compare expiration_date with rate_lock_until in the response.

getInvoices

GET /pay/api/getInvoices — scope read. Filters: asset, fiat, invoice_ids (comma-separated), status (active / paid / expired — expired 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 all or part of the crypto actually paid, from your app balance to the payer. This also works for anonymous payers without revealing their identity. On a fiat invoice, use paid_asset and paid_amount; the refund is not a new fiat conversion.

ParameterTypeRequiredMeaning
invoice_idintegeryesthe paid invoice
amountstringnothe crypto amount to refund, in the asset originally paid (paid_asset for fiat invoices). Omitted = the full unrefunded remainder. Partial refunds accumulate up to the original crypto amount paid
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. Your app needs enough balance in the original payment asset, even if swap_to converted the receipt into another asset. 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.