tgpay cryptoAPI
crypto-payapiinvoicesrefunds

API reference: invoices and refunds

7 min readUpdated Nov 2, 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). Ignored in crypto mode — set currency_type: "fiat" to price in fiat
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, up to 512 characters — required when paid_btn_name is set, ignored without it
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, currently the same 31 days. 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 (always crypto minor units: on a crypto invoice the face value; on a fiat invoice null until payment, then the minor units of paid_asset; on an open-amount invoice null until payment), and on paid fiat invoices paid_asset / paid_amount / paid_fiat_rate — the crypto actually charged and the rate used. asset stays null on a fiat invoice, even after payment.
  • 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.
  • Timestamps and payer data: created_at, paid_at, paid_usd_rate (the USD rate recorded at payment).
  • As created: currency_type, accepted_assets, open_amount, is_anonymous, 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 (a missing asset in crypto mode; in fiat mode a missing fiat, an asset sent alongside it, or open_amount), 400 invalid_amount (also an amount that rounds to zero at the asset’s precision), 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). 400 invalid_request covers malformed parameters: an unknown currency_type or paid_btn_name, a text value over its length limit, an expires_in outside 0–2678400, an amount sent as a JSON number.

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 parameter takes 0 to 86400 seconds: 0 means no lock, and any other value is adjusted to the supported window — at least 60 seconds, at most the platform maximum. A value outside 0–86400 is rejected with 400 invalid_request. 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 (default 100, max 1000). Returns {"items": [invoice, …]}, newest first.

deleteInvoice

POST /pay/api/deleteInvoice — scope invoices. One parameter: invoice_id. Deletes an unpaid invoice — active or expired — and returns true. The invoice is removed, not marked: it disappears from getInvoices, its pay_url stops working, and no webhook fires. Record the deletion on your side if you need it. Errors: 404 invoice_not_found, 409 invoice_already_paid — a paid invoice can’t be deleted because the money has 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, 400 invalid_request (a missing invoice_id, a spend_id over 64 characters), 403 verification_required (the owner’s outgoing operations are restricted), 503 merchant_payouts_paused, and the spend_id pair 409 idempotency_conflict / 409 idempotency_in_progress.