Skip to main content
T TON Adoption
Wallets DEV · 2026

Crypto Pay API: accept crypto payments in a Telegram bot

A developer's guide to Crypto Bot's Crypto Pay API: token issuance, createInvoice, HMAC webhook verification, supported assets

Author
TON Adoption Team · research desk
Published
7 min read

Crypto Pay is an HTTP API from Crypto Bot that lets a Telegram bot, a website or a mini-app accept payments in TON, USDT and ~15 other assets — without running your own backend wallet, without exchange integrations and without any on-chain infrastructure. Tens of thousands of merchants are built on it: sticker stores, bot subscriptions, info-products, VPN services. This article walks through a zero-to-production scenario for a developer: issuing a token, calling createInvoice, verifying webhooks, handling failures and shipping with a production checklist.

When Crypto Pay is the right choice

  • Telegram-native audience. Your users already hold balances inside @CryptoBot (tens of millions of accounts) — one-click checkout with no address copy-paste.
  • Micro-payments in the $1–50 range. Subscriptions, digital goods, tips, one-off services — anywhere on-chain network fees would kill the unit economics.
  • You don’t want to run a hot wallet. Crypto Pay is custodial; you don’t manage a seed phrase on your side — the keys live with the CG team.
  • Multi-asset baskets. A single invoice can be marked “TON or USDT or BTC accepted”, and the buyer picks at checkout.

Step 1. Create an app and get a token

  1. Open @CryptoBot in Telegram (short link t.me/send).
  2. Menu → Crypto PayMy AppsCreate App.
  3. Name the app (visible to you and on operation logs).
  4. The bot returns an API token like 12345:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Store it in a secret manager (.env, Vault, AWS Secrets) — it’s effectively the password to your app’s balance.

Base URLs:

  • Production: https://pay.crypt.bot/api/
  • Testnet: https://testnet-pay.crypt.bot/api/ — separate token, issued via @CryptoTestnetBot. Useful for CI / staging.

Auth: the Crypto-Pay-API-Token: <your-token> header on every request.

Step 2. First request — getMe

Verify the token is alive:

GET https://pay.crypt.bot/api/getMe
Crypto-Pay-API-Token: 12345:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Response:

{
  "ok": true,
  "result": {
    "app_id": 12345,
    "name": "My Test Shop",
    "payment_processing_bot_username": "CryptoBot"
  }
}

If "ok": false — inspect error.code. Most common:

  • 401 UNAUTHORIZED — token mismatch (typo, accidental whitespace).
  • 400 USER_ID_INVALID — not this method, but you’ll see it on transfer with a bad user_id.
  • 429 TOO_MANY_REQUESTSlimit is 100 requests/second; add backoff.

Step 3. Create an invoice

Standard scenario — a user is buying a “PRO” subscription for $9.99 in USDT:

POST https://pay.crypt.bot/api/createInvoice
Content-Type: application/json
Crypto-Pay-API-Token: 12345:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

{
  "currency_type": "crypto",
  "asset": "USDT",
  "amount": "9.99",
  "description": "PRO subscription, 30 days",
  "hidden_message": "Thanks! The bot will activate access within a minute.",
  "paid_btn_name": "openBot",
  "paid_btn_url": "https://t.me/your_bot?start=invoice_paid",
  "payload": "{\"user_id\":42,\"plan\":\"pro_30d\"}",
  "expires_in": 1800
}

Response:

{
  "ok": true,
  "result": {
    "invoice_id": 528347,
    "status": "active",
    "hash": "IVtN5sR1xVfH",
    "asset": "USDT",
    "amount": "9.99",
    "pay_url": "https://t.me/CryptoBot?start=IVtN5sR1xVfH",
    "bot_invoice_url": "https://t.me/CryptoBot?start=IVtN5sR1xVfH",
    "mini_app_invoice_url": "https://t.me/CryptoBot/app?startapp=IVtN5sR1xVfH",
    "web_app_invoice_url": "https://crypto-pay.io/invoice/...",
    "description": "PRO subscription, 30 days",
    "created_at": "2026-05-15T10:21:00Z",
    "expiration_date": "2026-05-15T10:51:00Z",
    "payload": "{\"user_id\":42,\"plan\":\"pro_30d\"}"
  }
}

The integration-relevant fields:

  • pay_url — what you hand the user as a “Pay” button. Opens @CryptoBot with the invoice pre-loaded.
  • mini_app_invoice_url — for Telegram Mini Apps (opens inside the bot without leaving).
  • payload — up to 4096 chars, your own memo. Put user_id, order_id, anything you need echoed back in the webhook. JSON-stringify it — Crypto Pay treats it as opaque.
  • expires_in — TTL in seconds. Default is forever; for micro-payments I recommend 600–3600.

Multi-asset invoice

Want to give the user “TON or USDT” choice? Use the fiat-anchored variant:

{
  "currency_type": "fiat",
  "fiat": "USD",
  "amount": "9.99",
  "accepted_assets": "TON,USDT,USDC",
  "description": "PRO subscription, 30 days",
  "payload": "..."
}

Crypto Pay converts $9.99 into TON and USDT at the moment-of-payment rate — the buyer picks with a button.

Step 4. Webhook on payment

Crypto Pay doesn’t push webhooks by default — you enable them per app. In @CryptoBot → My Apps → your app → Webhooks, set a URL like https://api.example.com/cryptopay/webhook.

Incoming request format:

POST /cryptopay/webhook
Content-Type: application/json
crypto-pay-api-signature: <hex-signature>

{
  "update_id": 7723,
  "update_type": "invoice_paid",
  "request_date": "2026-05-15T10:25:11Z",
  "payload": {
    "invoice_id": 528347,
    "status": "paid",
    "hash": "IVtN5sR1xVfH",
    "asset": "USDT",
    "amount": "9.99",
    "paid_amount": "9.99",
    "paid_asset": "USDT",
    "paid_fiat_rate": "1",
    "fee": "0",
    "paid_anonymously": false,
    "paid_btn_name": "openBot",
    "paid_btn_url": "https://t.me/your_bot?start=invoice_paid",
    "comment": null,
    "payload": "{\"user_id\":42,\"plan\":\"pro_30d\"}",
    "paid_at": "2026-05-15T10:25:05Z"
  }
}

Signature verification — non-negotiable

The signature is HMAC-SHA-256 over the raw request body, with a secret that itself is SHA-256 of your API token. In Node.js:

import crypto from 'node:crypto';

function verifyWebhook(rawBody, signature, apiToken) {
  const secret = crypto.createHash('sha256').update(apiToken).digest();
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

In Python:

import hashlib
import hmac

def verify_webhook(raw_body: bytes, signature: str, api_token: str) -> bool:
    secret = hashlib.sha256(api_token.encode()).digest()
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

Without this check, an attacker can POST a fake "status": "paid" body and fulfil an order without paying. Dozens of merchants got caught by exactly this between 2022 and 2024.

Idempotency

When delivery fails, Crypto Pay retries the webhook with growing backoff. That means the same update_id (or invoice_id) may arrive twice. Keep a processed-events table, or check order state before fulfilment:

if (await orderAlreadyFulfilled(invoiceId)) {
  return res.status(200).end(); // ack, do nothing
}
await fulfillOrder(invoiceId, payloadJson);
await markAsFulfilled(invoiceId);

Step 5. Polling as a fallback

If a webhook doesn’t reach you (your server was down), don’t lose the payment — poll getInvoices every few minutes:

GET https://pay.crypt.bot/api/getInvoices?status=paid&offset=0&count=100
Crypto-Pay-API-Token: ...

Returns the array of paid invoices. Diff against your DB, fulfil the missed ones. A reasonable cron interval is every 5 minutes for active orders.

Supported assets (2026)

At time of writing Crypto Pay accepts:

NetworkAssets
TONTON, USDT-TON, NOT, jUSDC, jUSDT, MAJOR, DOGS, HMSTR
BitcoinBTC
EthereumETH, USDT-ERC20, USDC-ERC20
TronUSDT-TRC20
BNB ChainBNB, USDT-BEP20
SolanaSOL, USDT-SPL
LitecoinLTC

The exact list moves — call getCurrencies before onboarding a merchant so your UI doesn’t expose assets that aren’t actually live.

Common errors and how to catch them

INVOICE_NOT_FOUND

Hits when you call getInvoices for an invoice_id created under a different app token. Each token only sees its own invoices.

EXPIRES_IN_INVALID

expires_in must be 1 to 2 678 400 (31 days). Higher values create the invoice without expiry but still return an error. Validate the range client-side.

Rate moves between create and pay

If you make a fiat invoice ($9.99) with accepted_assets: TON,USDT, the FX rate is locked at the moment of payment, not creation. Under volatility this creates micro-arbitrage — the buyer can wait for a favourable moment. For sensitive flows use a short expires_in (60–300 seconds).

Webhook arrives without a signature

That’s either a forgery or a very old Crypto Pay client. Reject any request that lacks crypto-pay-api-signature. All production webhooks have been signed since 2024.

App-level security

  • Never put the token into frontend / client-side bot code. A Crypto Pay token equals access to the balance. Server only.
  • HTTPS on the webhook endpoint. Without TLS the signature won’t help against MITM on the network path.
  • Log every webhook’s update_id — invaluable for dispute review and cron fallback.
  • Split environments by token. Production token stays in prod; staging gets a separate testnet token. An accidental refund from the prod balance is a bad day.
  • Enable 2FA on the owner’s Telegram account — compromising the owner equals compromising the balance.

Alternatives and combinations

  • xRocket Pay — direct competitor, very similar API (createInvoice / webhook), separate user base. Many shops integrate both: the buyer picks “Crypto Bot or xRocket”.
  • Telegram Stars — Telegram’s own internal currency, not crypto. Works for digital content inside Telegram, but monetisation and payout run through Telegram, not a crypto network.
  • Direct on-chain via TON Connect — non-custodial, no third-party dependency, but requires your own backend and UX work. Worth it for larger merchants who already run TON infrastructure.

A reasonable production strategy: Crypto Pay + xRocket Pay covers ~90% of the Telegram audience; direct on-chain via TON Connect handles users who avoid custodial services or transact in larger amounts.

Production checklist

Before flipping the switch on prod, walk through this list:

  • Token lives in .env / Vault, not committed to source control.
  • Webhook on HTTPS, signature verified via HMAC-SHA-256.
  • Idempotency by update_id or invoice_id.
  • Cron fallback via getInvoices for missed webhooks (every 5 minutes).
  • Raw-body logging of every webhook — for dispute resolution.
  • expires_in is set explicitly (short for fiat invoices, 60–300s).
  • Retries on createInvoice network errors with exponential backoff.
  • Alert on app balance dropping below threshold — so refunds aren’t held up.
  • Escalation playbook with @CryptoBot_Support.
  • Test scenario through testnet @CryptoTestnetBot passed end-to-end.

Wrapping up

Crypto Pay is the least painful way to start accepting crypto payments in a Telegram bot. From zero to the first confirmed invoice is a couple of hours of work: issue a token, write createInvoice, expose a webhook endpoint with signature verification. The custodial nature is the core trade-off — you save on infrastructure but hand reliability and regulatory exposure to Crypto Bot.

For pilots and micro-stores it’s almost always the right answer. For serious volume — pair it with direct on-chain acceptance via TON Connect, so you don’t depend on a single gateway.

Sources

Frequently asked

No. Basic account tiers don't require KYC — just open `@CryptoBot`, go to My Apps and create an app. Higher limits unlock after verification, but pilots and micro-shops can typically launch without it.
Crypto Pay is custodial and supports actual crypto assets (TON, USDT, BTC and more); payouts land on the app's internal balance. xRocket Pay is a direct competitor with a similar API and a separate user pool. Telegram Stars is Telegram's own internal currency — not crypto — and conversion into TON is one-way with platform fees. Crypto Pay wins when your audience already holds funds inside `@CryptoBot` (tens of millions of accounts).
Crypto Pay doesn't split an invoice — it's paid in full or it isn't. Overpayments (rare, only via manual transfer outside the invoice flow) credit the difference to the payer's internal balance and still mark the invoice paid. Best practice: read `paid_amount` from the webhook, not only `status`.
There's no direct refund endpoint. Refund manually with a transfer from the app balance to the buyer's @username — either through the `@CryptoBot` UI or via the `transfer` API method. For commerce, store the invoice → telegram_user_id mapping so you know who to pay back.
Yes. The signature is HMAC-SHA-256 over the raw request body, keyed by your API token, delivered in the `crypto-pay-api-signature` header. Without verification, an attacker can POST a fake paid-webhook from a spoofed IP and activate orders for free.
Creating invoices and accepting payments — 0%. Fees only show up on external withdrawals (standard network fee, around 0.05 TON on TON or ~1 USDT on USDT-TRC20) and on in-bot swaps between assets (~0.9%). That makes Crypto Pay one of the cheapest options for accepting crypto in micro-stores.

Related