How to accept Litecoin payments in a Discord bot (2026 guide)
CoinsFlow ·
A step-by-step guide for bot builders: make the invoice with one API call, post a Pay button, grant the role when a signed webhook says it is paid, and test the whole thing without spending a coin.
A shop bot, an autobuy bot or a ticket bot needs three things to take Litecoin: a way to make an invoice, a link the buyer can pay, and a signal that says "paid" so the bot can deliver. This guide builds all three with CoinsFlow. The buyer needs no account.
The full code, in discord.js and discord.py, is on the Discord bots page. This article explains each step and the mistakes bot builders run into.
Why Litecoin for a bot
Litecoin's blocks arrive about every 2.5 minutes. That target is written into Litecoin Core itself: nPowTargetSpacing = 2.5 * 60 seconds (chainparams.cpp, read 2026-10-24). The buyer's network fee is usually a fraction of a cent. On CoinsFlow a Litecoin invoice counts as paid at one confirmation, so a buyer usually has their role or key within minutes. The fee is 0.5% of each payment, with no monthly or setup fee.
What you need
- A CoinsFlow account and an API key (
cf_live_...), made in the dashboard under Developers. - Somewhere to keep that key: an environment variable on your bot's server. Any API key can withdraw your balance, so it never goes into code you share.
- For webhooks, a public
https://address your bot can answer on. If your bot cannot take requests, it can ask for the invoice's status instead withGET /v1/invoices/{id}.
The flow
- The buyer runs
/buyor opens a ticket. - Your bot calls
POST /v1/invoicesand gets back apaymentUrl. - The bot answers with a link button to that page. The page shows the address, the exact amount, a QR code and a countdown.
- Seconds after the buyer sends,
invoice.detectedreaches your webhook. Post "payment seen" in the ticket. - At the first confirmation,
invoice.paidarrives. Deliver, once per invoice.
Step 1: the invoice call
One request makes the invoice. Price it in dollars and let CoinsFlow work out the Litecoin amount:
curl -X POST https://api.coinsflow.app/v1/invoices \
-H "Authorization: Bearer $COINSFLOW_API_KEY" \
-H "Idempotency-Key: 1298765432109876543" \
-H "Content-Type: application/json" \
-d '{
"chain": "litecoin", "asset": "LTC",
"priceAmount": "20.00",
"description": "VIP role",
"metadata": { "userId": "2233445566778899001", "guildId": "1122334455667788990" }
}'What each part does:
priceAmountis dollars, as a string. Giveamountinstead if you want to name the Litecoin amount yourself. Exactly one of the two.Idempotency-Keymakes a retry safe. Use the Discord interaction id: if your bot times out and tries again, it gets the same invoice back, not a second one.metadatatakes up to 20 key and value pairs. The buyer never sees them, and every webhook carries them back, so put the Discord ids here. Store them as strings: Discord snowflakes exceed JavaScript's safe integer range and would be mangled as numbers.
The answer is 201 with the invoice: id, paymentUrl, address, amountDue (as a string), status of pending, expiresAt, and confirmationsRequired, which is 1 for Litecoin.
About the amount: it is rounded up to six decimals so the buyer can type it. And a dollar-priced invoice follows the Litecoin price once an hour until it is paid, with a 15-minute grace period for the old amount. So your bot should send the link, not a number: the page always shows what is due.
Step 2: the Pay button
In discord.js, a link button (ButtonStyle.Link) pointing at invoice.paymentUrl is enough. Reply ephemerally, so only the buyer sees it, and defer the reply first: Discord gives you three seconds.
Step 3: the webhook
Add an endpoint in the dashboard under Developers and tick invoice.paid and invoice.detected; the detected event is not a default, so it has to be ticked. You get a secret that starts with whsec_.
Every delivery carries a CoinsFlow-Signature header: t=<unix seconds>,v1=<hex>. The v1 is an HMAC-SHA256 of <t>.<raw body> with your secret. Check it over the raw bytes, before anything parses the JSON:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/webhooks/coinsflow", express.raw({ type: "application/json" }), async (req, res) => {
const parts = Object.fromEntries(String(req.get("CoinsFlow-Signature") ?? "").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 res.sendStatus(400);
const expected = crypto.createHmac("sha256", process.env.COINSFLOW_WEBHOOK_SECRET).update(`${t}.`).update(req.body).digest();
const given = Buffer.from(String(parts.v1 ?? ""), "hex");
if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
if (event.test && !process.env.TESTING) return res.sendStatus(200);
const { id, metadata } = event.data;
if (event.type === "invoice.paid" && !(await delivered(id))) {
await grantRole(metadata.guildId, metadata.userId);
await markDelivered(id);
}
res.sendStatus(200);
});Three rules keep this safe. Deliver on invoice.paid, never on invoice.detected: a payment that is only in the mempool can still be dropped. Deliver once per invoice: webhooks can arrive more than once, so keep a list of invoice ids you have fulfilled. Answer 2xx within ten seconds, or CoinsFlow will try again (after 3 seconds, 30 seconds, 5 minutes, 1 hour, 6 hours and 24 hours).
Step 4: test it without money
Add "test": true to the invoice call. The invoice has the same fields and the same payment page, but a placeholder address: nothing can be sent to it. Then press the buttons on the payment page, or call POST /v1/invoices/{id}/simulate with {"outcome": "paid"}. Your bot receives the same signed webhooks, marked "test": true. The code above acts on them only while TESTING is set, so a test never grants a role on your live bot. You can make 500 test invoices a day and simulate 30 outcomes a minute.
The mistakes to avoid
- Wrong amounts. A payment within 0.5% of the amount counts as paid. If less arrives,
invoice.underpaidtells you how much is missing inamountRemaining; the buyer can send the rest to the same address, or you accept what came withPOST /v1/invoices/{id}/accept. If more arrives, all of it is credited to you. - Open invoices piling up. An invoice stays open for at least 3 days (at most 7, with
ttlSeconds). Cancel the ones from closed tickets withPOST /v1/invoices/{id}/cancel, while nothing has been sent to them. One account can have 10,000 open at once. - Rate limits. 120 invoices a minute for one merchant, 240 for one account. Make invoices on
/buy, not on every message. - The key. It can withdraw. If it may have leaked, rotate it in the dashboard; the old one stops at once.
A $20 order, in numbers
The buyer pays $20.00 worth of LTC plus their own network fee, a fraction of a cent. CoinsFlow's fee is $0.10 (0.5%), taken in LTC when the payment is credited. About $19.90 worth of LTC lands in your balance at the first confirmation, and you can withdraw it at once, by hand or from the bot with POST /v1/withdrawals. The smallest Litecoin withdrawal is 0.0001 LTC.
One more thing. 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). The rest is in the API docs.
More articles
- Webhooks for crypto payments: verifying the signature and handling retries
- Test crypto payments without spending a coin
- Crypto payment gateway fees compared (October 2026)
- USDT on BNB Smart Chain vs TRON vs Ethereum: what a $20 payment really costs
- How long does a Litecoin payment take to confirm, and why CoinsFlow counts one confirmation