Skip to content
For Telegram bots

Crypto payments for your Telegram bot

One API call makes the invoice. A signed webhook tells your bot it is paid. 0.5% per payment.

CoinsFlow is a crypto payment gateway with an API a Telegram bot can call. Shops, subscriptions and services run through a bot take Litecoin, USDT, Bitcoin and the other coins we take: your bot makes an invoice and sends a Pay button. A signed webhook tells it, within seconds, that a payment is on its way, and on Litecoin about 2.5 minutes later that it is paid. The buyer needs no account.

A shop or a service, sold by your bot

  1. A buyer sends your /buy command, or picks a product from your bot's menu. Your bot makes an invoice with POST /v1/invoices: the price in dollars, the coin, and the chat and user ids in metadata, which the buyer never sees.
  2. The bot answers with an inline button to the invoice's paymentUrl. The payment page shows the address, the exact amount, a QR code and a countdown.
  3. Seconds after the buyer sends Litecoin or Bitcoin, invoice.detected reaches your webhook: tell the buyer the payment is seen. Tick this event when you add the endpoint: it is not one of the defaults.
  4. At the first confirmation, about 2.5 minutes on Litecoin, invoice.paid arrives. Deliver the product or switch the service on, once per invoice.
  5. Withdraw to your own wallet when you like: by hand, automatically, or from the bot with POST /v1/withdrawals.

A $30 Litecoin order, in numbers

Price$30.00, asked in LTC at the current price, rounded up to 6 decimals
Payment seenseconds after the buyer sends it: invoice.detected
Paidat 1 confirmation, about 2.5 minutes: invoice.paid
Our fee$0.15 (0.5%), taken in LTC when it is credited
Buyer’s network feeusually a fraction of a cent, paid by the buyer on top
In your balanceabout $29.85 worth of LTC, withdrawable at once

The /buy command

POST /v1/invoices
import { Bot, InlineKeyboard } from "grammy";

const bot = new Bot(process.env.BOT_TOKEN);

bot.command("buy", async (ctx) => {
  const res = await fetch("https://api.coinsflow.app/v1/invoices", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.COINSFLOW_API_KEY}`,
      "Idempotency-Key": `tg-${ctx.chat.id}-${ctx.msg.message_id}`, // a retry never makes a second invoice
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      chain: "litecoin", asset: "LTC", priceAmount: "30.00",
      description: "Premium, 1 month",
      metadata: { chatId: String(ctx.chat.id), userId: String(ctx.from.id) },
    }),
  });
  if (!res.ok) return ctx.reply("Payments are not available right now. Try again in a minute.");
  const invoice = await res.json();
  await ctx.reply("Your invoice for Premium. The page shows the address, the amount and a QR code.", {
    reply_markup: new InlineKeyboard().url("Pay in Litecoin", invoice.paymentUrl),
  });
});

bot.start(); // long polling; or webhookCallback(bot, "express") in the Express app below

The amount follows the Litecoin price once an hour until it is paid, so the bot sends the link, not a number: the page always shows what is due. The Idempotency-Key makes a retried command safe: the same key gets the same invoice back. Every field is in the API docs.

The webhook: seen, then paid

POST /webhooks/coinsflow
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.COINSFLOW_WEBHOOK_SECRET; // whsec_...
const app = express();

function verified(header, body) {
  try {
    const parts = Object.fromEntries(String(header ?? "").split(",").map((p) => p.split("=", 2)));
    const t = String(parts.t ?? "");
    if (!/^\d{1,12}$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
    const given = Buffer.from(String(parts.v1 ?? ""), "hex");
    const expected = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(body).digest();
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  } catch {
    return false;
  }
}

app.post("/webhooks/coinsflow", express.raw({ type: "application/json" }), async (req, res) => {
  if (!Buffer.isBuffer(req.body) || !verified(req.get("CoinsFlow-Signature"), req.body)) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  if (!event.type.startsWith("invoice.")) return res.sendStatus(200);
  if (event.test && !process.env.TESTING) return res.sendStatus(200); // test events only while you test
  const { id, metadata } = event.data;
  if (!metadata?.chatId) return res.sendStatus(200); // not one of the bot's invoices
  try {
    if (event.type === "invoice.detected") {
      await bot.api.sendMessage(metadata.chatId, "Payment seen. It counts at the first confirmation.");
    }
    if (event.type === "invoice.paid" && !(await delivered(id))) { // your database: once per invoice
      await grantPremium(metadata.userId); // your bot's own records
      await markDelivered(id);
      await bot.api.sendMessage(metadata.chatId, "Paid. Premium is on.");
    }
    res.sendStatus(200);
  } catch {
    res.sendStatus(500); // CoinsFlow sends it again later
  }
});

app.listen(3000);

Add the endpoint in the dashboard, under Developers, with invoice.detected and invoice.paid ticked. It must be an https:// address on the public internet, which a bot on long polling does not otherwise need: the Node example puts it in the bot's own Express app, the Python one runs as a small server beside the bot and sends its messages through the Bot API. A bot that cannot take requests at all can ask GET /v1/invoices/{id} for the status instead. How the signature works is in the docs.

A middleman flow, run by your bot

If your bot holds a trade until both sides are done, your CoinsFlow balance can be where the money waits.

  1. Your bot makes an invoice for the trade, and the buyer pays it. The money is credited to your balance when the invoice is paid: on Litecoin, at the first confirmation.
  2. When the trade is done, your bot asks for a quote with GET /v1/withdrawals/quote, then pays the seller with POST /v1/withdrawals, with the quoted fee as maxFee and an Idempotency-Key.
  3. The seller receives exactly the amount sent; the fee comes from your balance on top. withdrawal.confirmed tells your bot the payout is confirmed on chain.

A buyer who holds the same coin in a CoinsFlow balance can pay from it with Pay with CoinsFlow: the invoice is paid and final at once, with no block to wait for.

This is your bot and your service: the money is in your balance, and your bot decides when it moves. Letting other sellers take payments through your bot as their own payment service needs our written agreement first (Terms, section 6).

Test it first

Make the invoice with "test": true, then press the buttons on its payment page, or call POST /v1/invoices/{id}/simulate. Your bot gets the same webhooks, marked "test": true, and no money moves. The webhook code above acts on them only while TESTING is set, so a test never switches Premium on for a real buyer. Follow Telegram's own rules for bots as well as ours.

Questions

Does the buyer need a CoinsFlow account?

No. They open the link and pay from any wallet or exchange that can send the coin. A buyer who already holds the coin in a CoinsFlow balance can also pay from it, with Pay with CoinsFlow.

Does the buyer pay inside Telegram?

The bot sends a button that opens the payment page. The buyer pays from any wallet, by scanning the QR code or copying the address and the amount. CoinsFlow is not a Telegram payment provider: the bot links to the page, and the payment is an ordinary transfer on the coin’s network.

Can the buyer pick the coin?

Yes. Leave out chain and asset and give priceAmount in dollars: the payment page lets the buyer pick Litecoin, Bitcoin, Ether, BNB, USDT or USDC. The webhook then names the coin they chose.

What if the buyer sends the wrong amount?

Within 0.5% it counts as paid. If less arrives, invoice.underpaid tells you how much is missing (amountRemaining): the buyer can send the rest to the same address, or you accept what came with POST /v1/invoices/{id}/accept. If more arrives, all of it is credited to you: invoice.paid, then invoice.overpaid.

Can my bot pay people out?

Yes. Any API key can withdraw: ask for a quote with GET /v1/withdrawals/quote, then POST /v1/withdrawals with the quoted fee as maxFee and an Idempotency-Key, so a retry never sends twice. The smallest Litecoin withdrawal is 0.0001 LTC.

How many invoices can my bot make?

120 a minute for one merchant and 240 for one account, with up to 10,000 waiting to be paid at once. An invoice stays open for at least 3 days, so cancel the ones from orders that were dropped with POST /v1/invoices/{id}/cancel, while nothing has been sent to them.

Can I test it without money?

Yes. Add "test": true when you make the invoice and simulate the payment, from the API or with the buttons on its payment page. The same signed webhooks arrive, marked "test": true, and no money moves. A merchant can make 500 test invoices a day.

Where do I keep the bot token and the API key?

In your bot’s server settings, such as environment variables, never in code you share. The CoinsFlow key can withdraw your balance. If it may have leaked, rotate it in the dashboard: the old one stops working at once.