Connect a Telegram bot or a website.
Follow the steps for your platform. Every step shows what to click and the exact code to run.
Step 0 · Setup (both platforms)
Add your website in the admin panel
Open Admin → log in with your admin token → Websites → Add website. A Telegram bot is also a “website” here — use your bot’s name and any URL (for example https://t.me/your_bot).
Fill in the Webhook URL (where your bot or site receives payment confirmations) and tick the webhook fields you need. Tick Metadata (and External order ID) so you can tell which customer paid.
Copy your three secrets
After saving you see a Live API key (pk_primepay_…), a Sandbox key (pk_test_…) and a Webhook secret (whsec_…). You can copy them again any time from the Websites table.
Store them on your server
Put them in environment variables on the machine that runs your bot or website backend — never in browser code, an app, or a public repository.
# .env on your bot / website server (never commit this file, never put it in browser JavaScript) PRIME_PAY_URL=https://your-prime-pay-domain.com PRIME_PAY_API_KEY=pk_test_xxxxxxxxxxxxxxxx # sandbox first, swap for pk_primepay_... when going live PRIME_PAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx BOT_TOKEN=123456:ABC-your-telegram-bot-token # Telegram bots only
Try it in the API Playground
Admin → API Playground, choose your website’s sandbox key, enter an amount and press Send request. You should see HTTP 200 and a checkout_url. Use Test webhook next to the site to confirm your endpoint answers with HTTP 200.
Telegram bot
Create the bot
In Telegram open @BotFather → /newbot → copy the bot token into BOT_TOKEN.
Run the bot + webhook receiver
The code below does both jobs in one program: it shows products, creates a Prime Pay checkout when a customer taps one, and receives the signed webhook to confirm payment.
# pip install pyTelegramBotAPI flask requests
import os, time, json, hmac, hashlib, threading
import requests, telebot
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
from flask import Flask, request, abort
PRIME_PAY = os.environ["PRIME_PAY_URL"].rstrip("/") # https://your-prime-pay-domain
API_KEY = os.environ["PRIME_PAY_API_KEY"] # use pk_test_... while testing
SECRET = os.environ["PRIME_PAY_WEBHOOK_SECRET"] # whsec_...
bot = telebot.TeleBot(os.environ["BOT_TOKEN"])
app = Flask(__name__)
PRODUCTS = {"d520": ("520 Diamonds", 499), "elite": ("Elite Pass", 149)}
delivered = set() # use a database in production
@bot.message_handler(commands=["start", "buy"])
def buy(m):
kb = InlineKeyboardMarkup()
for pid, (name, price) in PRODUCTS.items():
kb.add(InlineKeyboardButton(f"{name} - Rs {price}", callback_data=f"buy:{pid}"))
bot.send_message(m.chat.id, "Choose a product:", reply_markup=kb)
@bot.callback_query_handler(func=lambda c: c.data.startswith("buy:"))
def create_order(c):
pid = c.data.split(":")[1]
name, price = PRODUCTS[pid]
ref = f"TG-{c.from_user.id}-{int(time.time())}"
r = requests.post(f"{PRIME_PAY}/api/v1/checkout", timeout=15,
headers={"x-prime-pay-key": API_KEY},
json={"amount": price, "item_name": name, "external_order_id": ref, "idempotency_key": ref,
"customer_name": c.from_user.first_name,
"metadata": {"chat_id": c.message.chat.id, "product_id": pid}})
data = r.json()
if r.status_code != 200:
return bot.answer_callback_query(c.id, data.get("error", "Payment error"), show_alert=True)
kb = InlineKeyboardMarkup()
kb.add(InlineKeyboardButton("Pay now", url=data["checkout_url"]))
bot.send_message(c.message.chat.id, f"{name} - Rs {data['order']['amount']}\nTap below to pay. Your item arrives here automatically.", reply_markup=kb)
bot.answer_callback_query(c.id)
@app.post("/payment/webhook")
def webhook():
raw = request.get_data()
sig = request.headers.get("x-prime-pay-signature", "")
expected = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
ev = json.loads(raw)
if ev.get("event") == "payment.verified" and ev["order_code"] not in delivered:
delivered.add(ev["order_code"])
chat_id = (ev.get("metadata") or {}).get("chat_id") # needs "Metadata" ticked in the site's webhook fields
if chat_id:
bot.send_message(chat_id, f"Payment received! Order {ev['order_code']}. Delivering your item...")
# TODO: deliver the product here (send code / activate plan / call your own API)
return "ok"
if __name__ == "__main__":
threading.Thread(target=bot.infinity_polling, daemon=True).start()
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8000)))// package.json -> { "type": "module" } npm i telegraf express
import { Telegraf, Markup } from "telegraf";
import express from "express";
import crypto from "node:crypto";
const { BOT_TOKEN, PRIME_PAY_URL, PRIME_PAY_API_KEY, PRIME_PAY_WEBHOOK_SECRET } = process.env;
const bot = new Telegraf(BOT_TOKEN);
const PRODUCTS = { d520: ["520 Diamonds", 499], elite: ["Elite Pass", 149] };
const delivered = new Set(); // use a database in production
bot.start(ctx => ctx.reply("Choose a product:", Markup.inlineKeyboard(
Object.entries(PRODUCTS).map(([id, [name, price]]) => [Markup.button.callback(`${name} - Rs ${price}`, `buy:${id}`)])
)));
bot.action(/^buy:(.+)$/, async ctx => {
const pid = ctx.match[1]; const [name, price] = PRODUCTS[pid];
const ref = `TG-${ctx.from.id}-${Date.now()}`;
const r = await fetch(`${PRIME_PAY_URL}/api/v1/checkout`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-prime-pay-key": PRIME_PAY_API_KEY },
body: JSON.stringify({ amount: price, item_name: name, external_order_id: ref, idempotency_key: ref,
customer_name: ctx.from.first_name, metadata: { chat_id: ctx.chat.id, product_id: pid } })
});
const data = await r.json();
if (!r.ok) return ctx.answerCbQuery(data.error || "Payment error", { show_alert: true });
await ctx.reply(`${name} - Rs ${data.order.amount}`, Markup.inlineKeyboard([Markup.button.url("Pay now", data.checkout_url)]));
await ctx.answerCbQuery();
});
const app = express();
app.post("/payment/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const sig = req.get("x-prime-pay-signature") || "";
const expected = crypto.createHmac("sha256", PRIME_PAY_WEBHOOK_SECRET).update(req.body).digest("hex");
if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);
const ev = JSON.parse(req.body);
if (ev.event === "payment.verified" && !delivered.has(ev.order_code)) {
delivered.add(ev.order_code);
const chatId = ev.metadata?.chat_id; // needs "Metadata" ticked in the site's webhook fields
if (chatId) await bot.telegram.sendMessage(chatId, `Payment received! Order ${ev.order_code}. Delivering your item...`);
// TODO: deliver the product here
}
res.send("ok");
});
bot.launch();
app.listen(process.env.PORT || 8000);Make the webhook reachable
Deploy the program somewhere with a public HTTPS URL (Render, Railway, a VPS…). Set the website’s Webhook URL in admin to https://your-bot-host/payment/webhook. Prime Pay refuses webhook URLs that point to private or internal addresses.
Test with the sandbox key
Start with PRIME_PAY_API_KEY=pk_test_…. Send /start to your bot and tap a product: the order is created already verified (no real money), and your webhook fires immediately with is_test: true so you can test the delivery message.
Go live
Replace the key with your Live key and restart. A real order shows the QR checkout; once the customer pays, Prime Pay matches the payment and your bot receives payment.verified.
Website
Create the checkout from your backend
When the customer presses “Buy”, call POST /api/v1/checkout from your server and redirect the customer to the returned checkout_url. Always save your own order ID first and send it as external_order_id.
// npm i express (set "type": "module" in package.json)
import express from "express";
import crypto from "node:crypto";
const app = express();
const { PRIME_PAY_URL, PRIME_PAY_API_KEY, PRIME_PAY_WEBHOOK_SECRET } = process.env;
// 1) Customer clicks "Buy" -> create the checkout on your SERVER and redirect
app.post("/buy", express.json(), async (req, res) => {
const orderId = "ORDER-" + Date.now(); // save this against the user in YOUR database first
const r = await fetch(`${PRIME_PAY_URL}/api/v1/checkout`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-prime-pay-key": PRIME_PAY_API_KEY },
body: JSON.stringify({ amount: 499, item_name: "520 Diamonds", external_order_id: orderId, idempotency_key: orderId,
success_url: "https://yoursite.com/payment/success", cancel_url: "https://yoursite.com/payment/cancel",
metadata: { user_id: req.body.userId } })
});
const data = await r.json();
if (!r.ok) return res.status(502).json({ error: data.error });
res.json({ checkout_url: data.checkout_url }); // front-end: window.location = checkout_url
});
// 2) Prime Pay calls this when the payment is verified
app.post("/api/payment/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.get("x-prime-pay-signature") || "";
const expected = crypto.createHmac("sha256", PRIME_PAY_WEBHOOK_SECRET).update(req.body).digest("hex");
if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);
const ev = JSON.parse(req.body);
if (ev.event === "payment.verified") {
// look up your order by ev.external_order_id (tick "External order ID" in webhook fields) and mark it paid ONCE
}
res.send("ok");
});
app.listen(3000);<?php
// create-payment.php -- runs on your server only
$orderId = 'ORDER-' . time(); // store this against the user in your DB first
$ch = curl_init(getenv('PRIME_PAY_URL') . '/api/v1/checkout');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'x-prime-pay-key: ' . getenv('PRIME_PAY_API_KEY')],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 499, 'item_name' => '520 Diamonds',
'external_order_id' => $orderId, 'idempotency_key' => $orderId,
'success_url' => 'https://yoursite.com/payment/success',
'metadata' => ['user_id' => $_POST['user_id'] ?? null],
]),
]);
$res = json_decode(curl_exec($ch), true);
if (empty($res['checkout_url'])) { http_response_code(502); exit($res['error'] ?? 'Payment error'); }
header('Location: ' . $res['checkout_url']);
<?php
// webhook.php -- set this URL as the Webhook URL of your website in the admin panel
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_PRIME_PAY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, getenv('PRIME_PAY_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
$ev = json_decode($raw, true);
if (($ev['event'] ?? '') === 'payment.verified') {
// find your order by $ev['external_order_id'] and mark it paid ONCE, then deliver
}
echo 'ok';# pip install flask requests
import os, hmac, hashlib, json, time, requests
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
PRIME_PAY = os.environ["PRIME_PAY_URL"].rstrip("/")
API_KEY = os.environ["PRIME_PAY_API_KEY"]
SECRET = os.environ["PRIME_PAY_WEBHOOK_SECRET"]
@app.post("/buy")
def buy():
order_id = f"ORDER-{int(time.time())}" # save against the user in YOUR database first
r = requests.post(f"{PRIME_PAY}/api/v1/checkout", timeout=15, headers={"x-prime-pay-key": API_KEY}, json={
"amount": 499, "item_name": "520 Diamonds", "external_order_id": order_id, "idempotency_key": order_id,
"success_url": "https://yoursite.com/payment/success"})
data = r.json()
if r.status_code != 200:
return jsonify(error=data.get("error")), 502
return jsonify(checkout_url=data["checkout_url"])
@app.post("/api/payment/webhook")
def webhook():
raw = request.get_data()
expected = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("x-prime-pay-signature", ""), expected):
abort(401)
ev = json.loads(raw)
if ev.get("event") == "payment.verified":
pass # find your order by ev["external_order_id"], mark it paid ONCE, then deliver
return "ok"Receive the webhook
Set the website’s Webhook URL in admin (for example https://yoursite.com/api/payment/webhook). Verify the HMAC signature on the raw request body, mark the order paid once, then deliver.
Show a success page
Set Success URL. After verification the checkout page sends the customer there with ?order_code=…&status=verified. Do not deliver based on this redirect alone — wait for the webhook.
Sandbox, then live
Develop with the pk_test_… key (instant “paid” orders and test webhooks), then swap in the live key.
Webhook reference
Prime Pay signs the exact raw body with your webhook secret (HMAC-SHA256, hex) and sends it as x-prime-pay-signature. If your endpoint does not answer with HTTP 2xx within 10 seconds, delivery is retried automatically with exponential backoff (about 30s, 1m, 2m, 4m… up to 8 attempts). See attempts in Admin → Webhooks.
# Example webhook body (only the fields you ticked are included; "event" is always sent)
POST https://your-site.com/payment/webhook
x-prime-pay-event: payment.verified
x-prime-pay-signature: <hex HMAC-SHA256 of the raw body>
{
"event": "payment.verified",
"order_code": "PP-123456789-1234",
"external_order_id": "TG-5550001-1767000000",
"amount": 588.82,
"status": "verified",
"utr": "FMPIB123456789",
"metadata": { "chat_id": 5550001, "product_id": "d520" },
"is_test": true // only present for sandbox (pk_test_) orders
}| order_code, amount, status, utr | Sent by default. |
| metadata, external_order_id, device_info, base_amount, tax_amount, paid_at | Tick them in the website’s Webhook payload builder to receive them. |
| amount | The total the customer paid (includes tax when the site has a tax rate). |
Before you go live
Troubleshooting
| 401 on checkout | Wrong or disabled key. Check the key and that the website is Active. |
| 503 maintenance | Maintenance mode is on (Admin → Settings). New checkouts are paused. |
| 429 | Per-website rate limit reached (default 60 requests/minute) — raise it in the website’s settings. |
| 403 blocked | The caller’s IP is blacklisted — see Admin → Settings → Blocked IPs. |
| Webhook not arriving | URL must be public HTTPS and answer 2xx. Use Test webhook and check Admin → Webhooks for the response code. |
| No metadata in webhook | Tick “Metadata” in the website’s Webhook payload builder. |
| Customer paid but still pending | Check Gmail linking and the Gmail logs; the customer can also enter the UTR on the checkout page. |
