Skip to main content

Crypto Payments

Four libraries accept crypto payments: NOWPayments (receive-only, decimal amounts, hosted invoices), Apirone (wallet/account model, smallest units), and the hosted gateways OxaPay and Cryptomus (decimal amounts, hosted pay pages). If a brief just says "crypto" or "payment," default to NOWPayments.

Telegram Stars

For in-app purchases paid in Telegram Stars (no external gateway), use send.invoice with the on pre_checkout and on successful_payment triggers โ€” no library needed.

danger

Keep API keys and IPN secrets in global.* variables (set in the Variables panel). Never hardcode them. Pass them as parameters to the library calls.

NOWPaymentsโ€‹

import nowpayments from "nowpayments"

Receive-only: create payments and invoices, then confirm them via a verified IPN webhook. Every call takes api_key (sent as the x-api-key header). Amounts are decimal in the named currency.

Subflowsโ€‹

SubflowSignature (key params) โ†’ outputs
get_status(api_key) โ†’ {ok, message}
get_currencies(api_key) โ†’ {currencies}
get_available_currencies(api_key) โ†’ {currencies}
min_amount(api_key, currency_from, currency_to) โ†’ {min_amount, โ€ฆ}
estimate_price(api_key, amount, currency_from, currency_to) โ†’ {estimated_amount, โ€ฆ}
create_payment(api_key, price_amount, price_currency, pay_currency, order_id, order_description, ipn_callback_url) โ†’ {payment_id, pay_address, pay_amount, pay_currency, payment_status}
create_invoice(api_key, price_amount, price_currency, order_id, order_description, ipn_callback_url, success_url, cancel_url) โ†’ {invoice_id, invoice_url}
create_invoice_payment(api_key, invoice_id, pay_currency, order_description) โ†’ {payment_id, pay_address, pay_amount, โ€ฆ}
get_payment_status(api_key, payment_id) โ†’ {payment_status, actually_paid, pay_amount, price_amount, outcome_amount, order_id}
handle_ipn(api_key, payment_id, reported_status) โ†’ {confirmed, finished, status, payment_status, actually_paid, pay_amount, price_amount, order_id}

Receive a paymentโ€‹

import nowpayments from "nowpayments"

bot Shop {
global nowpayments_api_key: String = ""
global nowpayments_ipn_secret: String = ""
user last_payment_id: Number = 0
user pending_plan: String = ""

flow buy on command "/buy" {
// Encode the buyer's id into order_id so the IPN can route the unlock.
set { flow.order_id = `${user.id}-monthly` }
let { payment_id, pay_address, pay_amount, pay_currency } = await nowpayments.create_payment(
api_key: global.nowpayments_api_key,
price_amount: 9.99,
price_currency: "usd",
pay_currency: "btc",
order_id: flow.order_id,
order_description: "Monthly subscription",
ipn_callback_url: `${bot.webhook_base}nowpayments`
) on error {
send.text("โŒ Couldn't start a payment. Try again shortly.")
stop
}
set { user.last_payment_id = payment_id }
send.text(`๐Ÿงพ Send *${pay_amount} ${pay_currency}* to:\n\`${pay_address}\`\n\nYou'll unlock automatically once it confirms.`, parse_mode: "Markdown")
}
}

Confirm via IPN webhookโ€‹

NOWPayments signs IPNs with HMAC-SHA512 over sorted JSON. Re-verify with handle_ipn (trust-but-verify) and deliver only when finished:

flow nowpayments_ipn on webhook("nowpayments",
verify: hmac, sig_algo: sha512, sig_payload: sorted_json,
header: "x-nowpayments-sig", secret: global.nowpayments_ipn_secret,
methods: [post], bind: request.body.order_id
) with body: { payment_id: Number, payment_status: String, order_id: String } {
let { finished, status, order_id } = await nowpayments.handle_ipn(
api_key: global.nowpayments_api_key,
payment_id: request.body.payment_id,
reported_status: request.body.payment_status
) on error {
respond.json({ ok: true }) // still ack so NOWPayments stops retrying
stop
}

when !finished {
respond.json({ ok: true })
stop
}

// Recover the buyer's chat id encoded in order_id ("<uid>-<plan>").
set { flow.buyer_uid = types.int(str.split(order_id, "-")[0]) }
send.text("โœ… Payment received โ€” access unlocked!", chat_id: flow.buyer_uid)
respond.json({ ok: true })
}

payment_status values: waiting, confirming, confirmed, sending, partially_paid, finished, failed, expired, refunded. Deliver only on finished.

note

NOWPayments IPNs carry order_id but no arbitrary data echo. Encode routing (like the chat id) into order_id and parse it back, or persist an order_id โ†’ chat_id map at create time.

Reference blueprint: nowpayments_shop.botgami.

Apironeโ€‹

import apirone from "apirone"

Wallet/account model. Amounts are in smallest units: BTC/LTC/BCH/DOGE โ†’ satoshi, TRX โ†’ sun.

Subflowsโ€‹

SubflowSignature (key params) โ†’ outputs
create_wallet(currency) โ†’ {wallet, transfer_key} โ€” transfer_key is returned once; store it immediately
new_address(wallet, callback_url, chat_id, order_id, secret) โ†’ {address}
account_new_address(account, currency, callback_url, chat_id, order_id, secret) โ†’ {address}
wallet_balance(wallet) โ†’ {available, total}
address_balance(wallet, address) โ†’ {available, total}
account_address_balance(account, address) โ†’ {available, total}
payout(wallet, transfer_key, address, amount) โ†’ {id, txid}
create_invoice(account, currency, amount, callback_url) โ†’ {invoice, address, invoice_url, status}
list_currencies() โ†’ {networks}
handle_callback(account, address, reported_value, confirmations, order_id, chat_id) โ†’ {confirmed, amount, address, order_id, chat_id, status}

Generate a deposit addressโ€‹

import apirone from "apirone"

bot Wallet {
global apirone_secret: String = ""
global wallet_id: String = ""

flow pay on command "/pay" {
let { address } = await apirone.new_address(
wallet: global.wallet_id,
callback_url: `${bot.webhook_base}payment_cb`,
chat_id: user.id,
order_id: `${user.id}-access`,
secret: global.apirone_secret
) on error {
send.text("โŒ Couldn't generate an address. Try again.")
stop
}
send.text(`Send BTC to:\n\`${address}\``, parse_mode: "Markdown")
}
}

Confirm via callback webhookโ€‹

Apirone callbacks carry no HMAC signature โ€” they echo back the data object you supplied. Use verify: data and respond with exactly *ok* so Apirone stops retrying:

flow payment_cb on webhook("payment_cb",
verify: data,
secret: global.apirone_secret,
data_field: "data.secret",
methods: [post]
) with body: {
value: Number,
confirmations: Number,
data: { chat_id: Number, order_id: String, secret: String }
} {
respond.text("*ok*")
let { confirmed, status } = await apirone.handle_callback(
account: global.wallet_id,
address: "",
reported_value: request.body.value,
confirmations: request.body.confirmations,
order_id: request.body.data.order_id,
chat_id: request.body.data.chat_id
) on error {
stop
}
when confirmed {
send.text(`โœ… Payment of ${request.body.value} sat received!`, chat_id: request.body.data.chat_id)
}
}

Reference blueprints: crypto_wallet_bot.botgami, payment_concierge.botgami.

OxaPayโ€‹

import oxapay from "oxapay"

A receive-only wrapper around the OxaPay Merchant API. Amounts are decimal in the named currency. Pass api_key (the merchant API key) as a parameter.

Subflowsโ€‹

SubflowSignature (key params) โ†’ outputs
create_invoice(api_key, amount, currency, order_id, callback_url, description) โ†’ {track_id, payment_url}
payment_info(api_key, track_id) โ†’ {status, paid}
handle_callback(api_key, track_id) โ†’ {paid, status}
import oxapay from "oxapay"

bot Shop {
global oxapay_api_key: String = ""

flow buy on command "/buy" {
set { flow.order_id = `${user.id}-pro` }
let { payment_url } = await oxapay.create_invoice(
api_key: global.oxapay_api_key,
amount: 19.99, currency: "USD",
order_id: flow.order_id,
callback_url: `${bot.webhook_base}oxapay`,
description: "Pro plan"
) on error {
send.text("โŒ Couldn't start a payment.")
stop
}
send.text(`๐Ÿ’ณ Pay here:\n${payment_url}`)
}
}

OxaPay POSTs status updates to callback_url. Wire a webhook and re-verify with handle_callback (trust-but-verify) before delivering goods.

Cryptomusโ€‹

import cryptomus from "cryptomus"

Receive crypto via the Cryptomus Merchant API (100+ coins). Amounts are strings (e.g. "19.99"). Pass both api_key and merchant_id as parameters.

Subflowsโ€‹

SubflowSignature (key params) โ†’ outputs
create_invoice(api_key, merchant_id, amount, currency, order_id, url_callback) โ†’ {uuid, url, status}
payment_info(api_key, merchant_id, uuid) โ†’ {status, paid}
handle_webhook(api_key, merchant_id, uuid) โ†’ {confirmed, status}
import cryptomus from "cryptomus"

bot Shop {
global cryptomus_api_key: String = ""
global cryptomus_merchant: String = ""

flow buy on command "/buy" {
set { flow.order_id = `${user.id}-pro` }
let { url } = await cryptomus.create_invoice(
api_key: global.cryptomus_api_key,
merchant_id: global.cryptomus_merchant,
amount: "19.99", currency: "USD",
order_id: flow.order_id,
url_callback: `${bot.webhook_base}cryptomus`
) on error {
send.text("โŒ Couldn't start a payment.")
stop
}
send.text(`๐Ÿ’ณ Pay here:\n${url}`)
}
}

Cryptomus POSTs to url_callback. Re-verify with handle_webhook before delivering goods.

See alsoโ€‹