API reference: transfers and checks
The payout methods. A transfer sends funds from your app balance
to a Telegram user’s wallet, or holds them until a new recipient joins;
a check is a claimable link you
fund up front. Conventions (auth, envelope, amounts, spend_id) are on the
Merchant API reference page.
All three money-out methods obey the app’s
payout mode: a new app pays anyone
from the start; if the owner tightened it, only listed recipients can be
paid (403 recipient_not_allowed) or nothing at all
(403 payouts_disabled). The owner sets the mode in the Mini App; the API
cannot. What the payouts may be for is set by the
Merchant API terms.
transfer
POST /pay/api/transfer — scope payouts, limit 30 per minute. A known
recipient receives the payout immediately. An unknown username creates a
pending transfer: funds wait in escrow until that user opens the bot with
that username. Unclaimed funds are returned to the app balance after the 30-day expiry
(expires_at). Confirm the return through status: "returned" before
using the funds in your accounting.
Forward invite_url yourself — the bot cannot message someone who has never
started it. The final status is completed or returned; subscribe to the
transfer_completed and transfer_returned webhooks or poll
getTransfers.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
user_id | integer | exactly one of user_id / username | the recipient’s Telegram ID; must already be a user of the app |
username | string | exactly one of user_id / username | recipient username, with or without @; an unknown username creates a pending payout |
asset | string | yes | asset code |
amount | string | yes | positive decimal string; also bounded by the platform’s per-transfer minimum and maximum (a US-dollar-equivalent estimate at current rates) |
spend_id | string | yes | idempotency key, 1–64 characters, unique per payout |
comment | string | no | up to 1024 characters, shown to the recipient |
disable_send_notification | boolean | no | true suppresses the recipient notification for an immediate payout; claiming a pending transfer still sends a notification |
The result is the transfer object: transfer_id, hash, user_id,
recipient_username, asset, amount, amount_minor, spend_id,
comment, status (pending / completed / returned), created_at,
completed_at, expires_at, invite_url. user_id may be null while
waiting for a new recipient; completed_at is null until completion.
Errors: 403 payouts_disabled / 403 recipient_not_allowed (the
payout mode), 404 user_not_found
(the id is unknown or the recipient is unavailable), 400 invalid_recipient
(invalid recipient username), 400 amount_too_small / 400 amount_too_big
(outside the per-transfer bounds), 409 insufficient_funds,
400 unknown_asset, 400 invalid_amount, and the spend_id pair
409 idempotency_conflict / 409 idempotency_in_progress.
transferBatch
POST /pay/api/transferBatch — scope payouts, limit 10 per minute.
An extension for mass payouts: up to 100 transfers in one call.
One parameter: items — an array where each item is a full transfer
parameter set (user_id or username, asset, amount, spend_id, optional
comment and disable_send_notification). spend_ids must be unique
within the batch, or the whole call fails with 400 duplicate_spend_id
before anything executes. Invalid item structure, the app’s payout mode
or its daily limit can also reject the whole request before any payout.
Once accepted, results are returned in result.items.
Items settle independently, in order — one failed item never rolls
back the others. The call returns HTTP 200 with ok: true even when some
items failed, so always check each item:
- success:
{"ok": true, "spend_id": "…", "result": <transfer object>} - failure:
{"ok": false, "spend_id": "…", "error": {"code": …, "name": "…"}}with the same error names as a singletransfer.
Batch items share the idempotency namespace with single transfers:
retrying the whole batch — or re-sending one item as a single transfer
with the same spend_id — replays instead of paying twice.
getTransfers
GET /pay/api/getTransfers — scope read. Filters: asset,
transfer_ids (comma-separated), spend_id (exact match — look up a
payout by your own key), status (pending / completed / returned),
plus offset / count. Returns
{"items": [transfer, …]}, newest first.
createCheck
POST /pay/api/createCheck — scope checks, limit 60 per minute.
By default, creates a single-use check from your app balance. Anyone
with the link, or only the pinned user, can claim it. The recipients
extension creates a named list with a separate amount for each recipient.
The total is locked at creation (available moves to onhold in
getBalance). Check creation has the same amount and daily payout limits
as transfers.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
asset | string | yes | asset code |
amount | string | unless recipients is set | positive decimal string; with recipients, the total is calculated from the list |
pin_to_user_id | integer | no | only this Telegram ID may claim |
pin_to_username | string | no | only this @username may claim (the @ is optional; ignored when pin_to_user_id is also set). The username must belong to an existing user of the app |
spend_id | string | no | idempotency key (extension) — use one |
recipients | array | no | extension: 1–100 items, each with user_id or username and a positive decimal-string amount. Cannot be combined with a pinned user |
password | string | no | extension: password required to claim; up to 64 characters |
expires_in | integer | no | extension: lifetime in seconds; omitted or 0 = no expiry |
The result is the check object: check_id, hash, asset,
amount, amount_minor, bot_check_url (the t.me claim link),
status (active / activated), pin_to_user_id, created_at,
activated_at, expires_at, and recipients (null for an ordinary
check; otherwise each entry has user_id, username, amount,
amount_minor, claimed_at). Each claim fires the opt-in
check_activated webhook; a named-list claim also
includes activated_slot. Expiry returns the unclaimed remainder; a
named-list expiry can send check_expired.
The current response maps every check that is not fully claimed to
status: "active", including an expired check. Check expires_at as
well; do not interpret active alone as proof the link is still claimable.
Errors: 403 payouts_disabled / 403 recipient_not_allowed (the
payout mode — under the allowlist
mode every pinned or named recipient must be listed), 400 unknown_asset,
400 invalid_amount, 404 user_not_found (the pinned username matches
nobody), 409 insufficient_funds, and the spend_id pair.
deleteCheck
POST /pay/api/deleteCheck — scope checks. One parameter: check_id.
Cancels an active check and returns its unclaimed remainder to your app
balance; returns true. Errors: 404 check_not_found,
409 check_not_active (fully claimed, expired or deleted).
getChecks
GET /pay/api/getChecks — scope read. Filters: asset, check_ids
(comma-separated), status (active / activated), plus offset /
count. Returns {"items": [check, …]}, newest first; deleted checks are
never returned.
Was this article helpful?
Thanks for the feedback.