API reference: invoices and refunds
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.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
currency_type | string | no | crypto (default) or fiat |
asset | string | crypto mode | the asset to charge, e.g. USDT. Not allowed together with fiat |
fiat | string | fiat mode | the fiat currency the price is in (the is_fiat rows of getCurrencies) |
accepted_assets | string / array | no | fiat mode only: the assets the payer may pay with — a comma-separated string ("USDT,GRAM") or a JSON array. Omitted = all supported assets |
amount | string | yes, unless open_amount is set | positive decimal string: asset units in crypto mode, fiat units (max 2 decimal places) in fiat mode |
open_amount | boolean | no | extension, crypto mode only: no fixed amount — the payer enters one at pay time (donations and tips). Mutually exclusive with amount |
description | string | no | up to 1024 characters, shown to the payer |
hidden_message | string | no | up to 2048 characters, revealed to the payer only after payment |
payload | string | no | up to 4096 characters of your own data, echoed back on the invoice and webhook |
allow_comments | boolean | no | let the payer attach a comment (default true) |
allow_anonymous | boolean | no | let the payer hide their identity (default true) |
paid_btn_name | string | no | post-payment button: viewItem, openChannel, openBot, or callback |
paid_btn_url | string | no | the button’s http(s) URL — required when paid_btn_name is set |
swap_to | string | no | auto-swap received payments into this asset. Best-effort: if the swap can’t run at pay time, the payment still succeeds unswapped |
expires_in | integer | no | seconds until the invoice expires, up to 2678400 (31 days); omitted or 0 = never |
rate_lock_seconds | integer | no | extension, 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 insidepay_url),status(active/paid/expired),pay_url— thet.melink you send the payer (bot_invoice_url,mini_app_invoice_url,web_app_invoice_urlare aliases of it). - Amounts:
amount(the face value — fiat units on a fiat invoice, crypto otherwise;nullwhile an open-amount invoice is unpaid),amount_minor, and on paid fiat invoicespaid_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 (feeandusd_rateare deprecated Crypto Bot aliases). See fees and limits. - Payer:
paid_by_user_id(nullwhen the payer chose anonymity),paid_anonymously,comment. - Refunds (extension):
refunded_amount/refunded_minor(cumulative) andrefunded_at, stamped once fully refunded. - Rate lock (extension):
rate_lock_untilandrate_lock_rates— the stamped per-asset rates,nullwhen no lock was requested. - As created:
description,hidden_message,payload,paid_btn_name/paid_btn_url,expiration_date(expires_atis 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 /
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 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.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
invoice_id | integer | yes | the paid invoice |
amount | string | no | the amount to refund, in the invoice’s asset. Omitted = the full unrefunded remainder. Partial refunds accumulate up to the face amount |
spend_id | string | no | idempotency 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.
Was this article helpful?
Thanks for the feedback.