Terima pembayaran kripto
Paysell menyelesaikan TON dan USDT pada rangkaian TON. Anda mencipta invois, kami berikan pautan, dan anda menerima panggilan balik yang ditandatangani sebaik sahaja wang disahkan di rantaian dan dikreditkan ke baki anda.
Gambaran keseluruhan#
Apa yang Paysell lakukan, dan apa yang tidak.
Paysell ialah pemproses pembayaran, bukan dompet. Anda tidak pernah mengendalikan kunci peribadi, memantau blockchain, atau menentukan bila transaksi menjadi muktamad — itu bahagian yang kami ambil alih.
Setiap invois mempunyai alamat penerimaannya sendiri. Apabila pembeli membayarnya, kami menunggu rangkaian mengesahkan pemindahan, menolak yuran kami, dan mengkreditkan bakinya ke baki anda. Anda boleh mengeluarkan ke mana-mana alamat yang anda suka.
Cara pembayaran berfungsi#
Enam langkah, kebanyakannya milik kami.
Enam langkah, kebanyakannya milik kami:
- 1
Pelanggan anda klik bayar
Pelayan anda memanggil API kami dengan jumlah dan rujukan pesanan anda sendiri.
- 2
Kami berikan alamat
Alamat penerimaan baharu diambil daripada kolam yang telah dijana lebih awal dan dikaitkan dengan invois ini. Satu alamat dimiliki oleh tepat satu invois terbuka, dan begitulah cara pembayaran dipadankan semula kepadanya.
- 3
Pelanggan menghantar syiling
Mereka mengimbas kod QR atau menyalin alamat. Hantar mereka ke
payment_urlyang kami kembalikan dan halaman itu diuruskan untuk anda — jumlah, alamat, QR, kiraan detik, status langsung. - 4
Kami mengesan pemindahan
Dua sumber data blockchain yang bebas ditanya, dan jawapan mereka dibandingkan. Jika tidak sepakat, kami berhenti dan bukannya memilih jawapan yang lebih mudah.
- 5
Kami menunggu kemuktamadan
Kemasukan dalam masterchain ditambah tiga blok di atasnya. Lebih kurang lima belas saat — pembayaran yang kelihatan selesai tetapi kemudian hilang akan menjadi kerugian anda, jadi kami tidak mengambil risiko itu.
- 6
Dikreditkan, dan anda diberitahu
Yuran ditolak, bakinya masuk ke baki anda, dan webhook yang ditandatangani dihantar ke pelayan anda membawa
order_idanda.
Kira-kira seminit dari pembayaran hingga panggilan balik: lebih kurang lima belas saat pengesahan rangkaian, selebihnya adalah imbasan kami terhadap alamat yang dipantau.
Ke mana wang pergi#
Yuran, dan asas pengiraannya.
Yuran ialah 0.2%, ditetapkan untuk kedai anda pada saat ia didaftarkan. Jika kadar standard berubah kemudian, kadar anda tidak berubah — ia ditulis ke dalam setiap invois sebagai nombor, bukan sebagai rujukan kepada tetapan.
Yuran diambil daripada apa yang sebenarnya tiba, bukan daripada apa yang diminta oleh invois. Bilkan 5 USDT dan terima 20, yuran dikira berdasarkan 20. Kurang bayar, dan ia dikira berdasarkan apa yang masuk.
Invoice: 5.000000 USDT
Received: 20.000000 USDT (the buyer sent more)
Fee 0.2%: 0.040000 USDT (on 20, not on 5)
Credited: 19.960000 USDTLebihan bayaran dikreditkan sepenuhnya — kami tidak menyimpan bakinya. Kurang bayar membiarkan invois terbuka supaya pembeli boleh menambah baki ke alamat yang sama.
Mula pantas#
Lima minit ke invois pertama anda.
Lima langkah. Dua ialah klik dalam kawasan akaun anda, satu ialah satu permintaan daripada pelayan anda, dan dua yang terakhir berlaku dengan sendirinya.
- 1
Cipta kedai
Dalam kawasan akaun anda. Ia mula menerima pembayaran serta-merta — tiada menunggu semakan. Pengesahan berlaku secara senyap di latar belakang dan hanya menyekat pengeluaran, bukan pembayaran masuk.
- 2
Keluarkan kunci API
Kedai anda → Kunci API → Kunci baharu. Kunci dan rahsia webhook dipaparkan sekali sahaja dan tidak akan ditunjukkan lagi. Simpan seperti anda menyimpan kata laluan pangkalan data, dan jangan sekali-kali hantarkannya ke pelayar.
- 3
Cipta invois
Satu permintaan daripada pelayan anda, satu pautan dikembalikan. Keempat-empat petikan kod di bawah menghantar perkara yang sama.
- 4
Hantar pembeli ke
payment_urlItulah keseluruhan proses pembayaran — jumlah, alamat, kod QR, kiraan detik, status langsung — dan tiada apa yang perlu dibina. Lihat Halaman pembayaran untuk apa yang pembeli benar-benar lihat.
- 5
Tunggu webhook
Sebaik sahaja wang disahkan pada rantaian dan dikreditkan, kami POST peristiwa
payment.creditedyang ditandatangani ke pelayan anda. Sahkan tandatangan, kemudian tandakan pesanan sebagai dibayar — tetapi hanya apabiladata.statusialahpaidatauoverpaid. Lihat Webhook.
The same request, four ways
curl -X POST https://paysell.me/api/merchant/v1/invoices \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"asset": "USDT_TON",
"amount": "5",
"order_id": "order-1042",
"idempotency_key": "order-1042"
}'Alihkan pembeli ke payment_url dalam respons. Anda selesai — selebihnya akan tiba sebagai webhook.
What to do next
- Write the webhook receiver — Webhooks and A complete receiver. Nothing else on this page matters as much: it is what turns a payment into a paid order.
- Handle
underpaidandoverpaid, not justpaid— see Status reference. - Read Typical mistakes, then walk the go-live checklist before you point real customers at it.
Pengesahan#
Kunci API anda, dan cara ia digunakan.
Setiap permintaan membawa kunci anda dalam pengepala Authorization:
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxASetiap kunci yang dikeluarkan di sini bermula dengan sk_live_. Awalan sk_test_ hanya wujud pada pemasangan yang menghala ke rangkaian ujian, dan pemasangan sedemikian tidak ditawarkan — lihat Ujian. Kami menyimpan cincang sehala, bukan kunci itu sendiri, jadi tiada siapa, termasuk kami, boleh menunjukkannya kepada anda semula. Hilang? Keluarkan yang baharu dan batalkan yang lama.
Kedai diperoleh daripada kunci, itulah sebabnya tiada permintaan yang mengambil id kedai. Kunci hanya boleh bertindak ke atas kedainya sendiri.
Laluan membawa versi: /api/merchant/v1/…. Di dalam satu versi kami hanya menambah medan — tiada apa dinamakan semula dan tiada apa bertukar makna secara senyap. Perubahan yang akan merosakkan kod anda mendapat awalan baharu, /v2, dan /v1 terus berfungsi untuk tempoh yang diumumkan.
Endpoints at a glance#
Four calls, three of them authenticated.
This is the whole merchant API. Balances, payouts and history are not in it — they live in your account area, where a person is looking at them.
| Endpoint | Method | Auth | What it does |
|---|---|---|---|
| /invoices | POST | API key | Open an invoice and get a payment link. Details. |
| /invoices/{invoice_id} | GET | API key | Read one invoice's current state. Details. |
| /invoices/{invoice_id}/cancel | POST | API key | Close an invoice that is still open and free its address. Details. |
| /public/invoices/{invoice_id} | GET | none | What the hosted checkout page reads. Only needed if you build your own. Details. |
Every path is relative to https://paysell.me/api/merchant/v1. There is no list endpoint and no refund endpoint — see Refunds.
Cipta invois#
POST /api/merchant/v1/invoices
/api/merchant/v1/invoicesBadan permintaan
| Medan | Jenis | Diperlukan | Penerangan |
|---|---|---|---|
| asset | string | ya | Sama ada TON atau USDT_TON. |
| amount | string | ya | Unit biasa syiling, sebagai rentetan: "5" ialah 5 USDT. Tidak lebih banyak tempat perpuluhan daripada yang ada pada syiling. Lihat Jumlah. |
| order_id | string | tidak | Rujukan anda sendiri, sehingga 200 aksara. Kembali dalam setiap webhook — inilah cara anda memadankan pembayaran dengan pesanan. |
| description | string | tidak | Sehingga 1000 aksara. Dipaparkan kepada pembeli pada halaman pembayaran. |
| ttl_minutes | number | tidak | Berapa lama invois kekal boleh dibayar, dalam minit. 1–1440; tinggalkannya dan nilai lalai digunakan — 2 jam buat masa ini. |
| idempotency_key | string | tidak | Sehingga 200 aksara. Hantar nilai yang sama semasa mencuba semula dan anda mendapat invois yang sama dikembalikan, bukan yang kedua. Ia medan dalam badan permintaan, bukan pengepala Idempotency-Key — pengepala itu tidak dibaca di sini. |
Respons · 201
{
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"payment_url": "https://paysell.me/pay/12c22c1a-a496-4c1e-abe3-72661ef8706e",
"address": "UQAvDJp7QDwqRcuNQBiK2GhBt71Xh1_UMYPCzMkQAoBPmZKl",
"asset": "USDT_TON",
"amount": "5",
"amount_minor": "5000000",
"status": "pending",
"paid": "0",
"paid_minor": "0",
"order_id": "order-1042",
"description": "Pro subscription",
"expires_at": "2026-09-06T17:20:55Z",
"created_at": "2026-09-06T15:20:55Z"
}Memetakannya kepada pesanan anda
| Medan | Apa yang perlu dilakukan |
|---|---|
| invoice_id | Simpan berbanding pesanan anda. Inilah yang mengenal pasti pembayaran di tempat lain. |
| payment_url | Alihkan pembeli ke sini. Tiada apa lagi untuk dibina. |
| address | Hanya jika anda merender halaman pembayaran anda sendiri. Paparkan tepat seperti diberikan — lihat amaran di bawah. |
| amount | Jumlah dalam unit biasa, tepat seperti yang anda hantar. Paparkan yang ini. |
| amount_minor | Jumlah yang sama sebagai integer dalam unit terkecil. Kira dengan yang ini. |
| expires_at | Papar kiraan detik. Selepas ia berlalu alamat berhenti dipantau untuk invois ini. |
| status | Sentiasa pending di sini. Perubahan sebenar tiba melalui webhook. |
UQ… pada mainnet, 0Q… pada testnet). Tukarkannya, cantikkannya, atau tukar kepada pengekodan lain bagi alamat yang sama, dan syiling yang dihantar ke dompet yang belum digunakan akan melantun kembali kepada penghantar.Baca invois#
GET /api/merchant/v1/invoices/{invoice_id}
/api/merchant/v1/invoices/{invoice_id}Bentuk yang sama seperti di atas, dengan status, paid dan paid_minor mencerminkan keadaan semasa: paid ialah berapa banyak yang sudah tiba dalam unit biasa, paid_minor jumlah yang sama sebagai integer dalam unit terkecil. Berguna sebagai sandaran apabila webhook terlepas, atau pada halaman terima kasih.
Tanyakan sekurang-kurangnya setiap beberapa saat, dan anggap webhook sebagai saluran utama. Invois yang dimiliki oleh kedai lain menjawab 404 — bukan 403, supaya id tidak boleh diselidik kewujudannya.
Batalkan invois#
POST /api/merchant/v1/invoices/{invoice_id}/cancel
/api/merchant/v1/invoices/{invoice_id}/cancelMenutup invois yang masih terbuka — pending atau underpaid — dan melepaskan alamatnya. Gunakan apabila pelanggan meninggalkan proses pembayaran: alamat adalah sumber terhad, dan mengembalikannya mengekalkan kolam sihat.
Invois yang tidak lagi terbuka menjawab 409. Membatalkan invois underpaid tidak memulangkan syiling kepada sesiapa: wang yang sudah dikreditkan kekal pada baki anda, dan yang ditutup hanyalah penerimaan bayaran tambahan.
Webhook#
Apa yang tiba, dan cara mengesahkannya.
Tetapkan URL webhook semasa membuat kunci. Kami POST ke situ apabila pembayaran dikreditkan — dan apabila deposit yang ditahan untuk semakan tambahan ditolak. Setiap penghantaran ditandatangani, dan kami terus mencuba semula selama kira-kira sehari setengah sehingga anda menjawab 2xx. Lepaskan barang pada status: paid atau overpaid, bukan hanya kerana panggilan itu sampai.
Peristiwa
| Peristiwa | Bila | Apa yang dibawa badan |
|---|---|---|
| payment.credited | Pemindahan disahkan di rantaian, yuran kami diambil, dan bakinya masuk ke baki anda. | Medan yang disenaraikan di bawah. |
| payment.rejected | Deposit yang ditahan untuk semakan tambahan (lihat Rujukan status) telah ditolak. Wang itu tidak akan sampai ke baki anda. | invoice_id, order_id, asset, amount, tx_hash dan reason. Jangan lepaskan barang; jika invois itu sudah paid daripada pemindahan terdahulu, peristiwa ini mengenai deposit tambahan, bukan mengenai pembayaran tersebut. |
Apa yang tiba
{
"event_id": "99f74f58-efbb-4af1-b0a3-76b0073f9e6b",
"type": "payment.credited",
"data": {
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"order_id": "order-1042",
"asset": "USDT_TON",
"amount": "5000000",
"credited": "4905000",
"fee": "95000",
"status": "paid",
"paid_minor": "5000000",
"tx_hash": "97a1f0…"
}
}{
"event_id": "0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
"type": "payment.rejected",
"data": {
"invoice_id": "12c22c1a-a496-4c1e-abe3-72661ef8706e",
"order_id": "order-1042",
"asset": "USDT_TON",
"amount": "5000000",
"tx_hash": "97a1f0…",
"reason": "could not be matched to any order"
}
}Pemetaan medan
| Medan | Maksud |
|---|---|
| event_id | Unik bagi setiap peristiwa; juga dalam pengepala X-Paysell-Event-Id. Simpan dan abaikan pengulangan — lihat di bawah. |
| data.order_id | Rujukan anda. Cari pesanan anda dengan ini. |
| data.amount | Berapa yang pembeli hantar dalam pemindahan ini, dalam unit terkecil — berbeza dengan API yang menerima unit biasa. |
| data.fee | Berapa yang kami ambil, dalam unit terkecil. |
| data.credited | Berapa yang masuk ke baki anda: amount − fee, dalam unit terkecil. |
| data.paid_minor | Jumlah keseluruhan yang diterima pada invois ini setakat ini, dalam unit terkecil. Medan yang penting pada underpaid: status memberitahu kurang yang sampai, ini memberitahu berapa kurangnya. |
| data.asset | Syiling yang benar-benar sampai. Tidak semestinya syiling yang diminta oleh invois. |
| data.asset_mismatch | Hadir, dan bernilai true, hanya apabila syiling yang sampai bukan syiling invois. Wang dikreditkan kepada anda, tetapi invois kekal belum dibayar dan status tidak akan sesekali menjadi paid. |
| data.invoice_asset | Datang bersama asset_mismatch: syiling yang sebenarnya diminta oleh invois. |
| data.status | Status invois sekarang: pending, underpaid, paid, overpaid atau expired. Bandingkan dengan apa yang anda jangkakan. |
| data.tx_hash | Transaksi di rantaian, untuk rekod dan sokongan anda. |
Pengepala pada setiap penghantaran
X-Paysell-Event: payment.credited
X-Paysell-Event-Id: 99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp: 1789000000
X-Paysell-Signature: sha256=6f1c0e6a…| Pengepala | Maksud |
|---|---|
| X-Paysell-Event | Jenis peristiwa: payment.credited atau payment.rejected. |
| X-Paysell-Event-Id | Unik bagi setiap peristiwa. Inilah nilai yang perlu digunakan untuk menyahduplikat. |
| X-Paysell-Timestamp | Bila kami menandatangani, dalam saat unix. Ia sebahagian daripada rentetan yang ditandatangani. |
| X-Paysell-Signature | sha256= diikuti HMAC heks. Lihat di bawah. |
Mengesahkan tandatangan
Setiap permintaan ditandatangani dengan rahsia webhook yang dipaparkan sekali semasa anda mencipta kunci. Tandatangan itu ialah HMAC-SHA256(secret, "{timestamp}.{raw_body}") — cap masa daripada X-Paysell-Timestamp, satu noktah literal, kemudian bait badan. Semaknya sebelum bertindak: tanpa ini, sesiapa yang mengetahui URL anda boleh menyerahkan kepada anda pesanan yang telah dibayar.
Python:
import hmac, hashlib, time
def is_ours(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # ±5 minutes
return False
signed = timestamp.encode() + b"." + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
# compare_digest, not ==: a plain comparison leaks the answer through timing
return hmac.compare_digest("sha256=" + expected, signature)Node.js:
const crypto = require("node:crypto")
function isOurs(body, signature, timestamp, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false // ±5 min
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(timestamp + ".").update(body) // body is the raw Buffer, not a parsed object
.digest("hex")
const a = Buffer.from(expected), b = Buffer.from(signature)
// timingSafeEqual throws when the lengths differ, so check that first
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Tandatangani bait mentah badan, tepat seperti diterima. Huraikan JSON dan siri semula ia dan bait berubah — susunan kunci, jarak — dan tandatangan tidak akan sepadan. Bandingkan dalam masa malar (hmac.compare_digest, crypto.timingSafeEqual): == biasa kembali lebih cepat apabila bait pertama sudah salah, dan perbezaan itu cukup untuk meneka tandatangan sebait demi sebait.
Tetingkap cap masa
Tolak apa sahaja yang cap masanya lebih daripada lima minit jauh daripada jam anda sendiri, ke mana-mana arah. Cap masa berada di dalam rentetan yang ditandatangani justeru supaya ia tidak boleh diubah tanpa merosakkan tandatangan; tetingkap inilah yang menjadikannya perlindungan. Tanpanya, permintaan yang pernah dirakam kekal sah selama-lamanya dan boleh dimainkan semula bila-bila masa — tandatangan itu sendiri tidak pernah luput. Kekalkan jam pelayan anda pada NTP, atau semakan ini akan mula menolak penghantaran yang sah.
Pertindihan
Peristiwa yang sama boleh tiba lebih daripada sekali. Itu bukan pepijat: kami mencuba semula sehingga anda menjawab 2xx, dan penghantaran yang berjaya tetapi responsnya tidak pernah sampai kepada kami akan dihantar semula. Rekodkan X-Paysell-Event-Id (ia juga datang sebagai event_id dalam badan) dan pastikan ketibaan kedua tidak melakukan apa-apa.
Cubaan semula
Percubaan pertama dihantar sebaik sahaja pembayaran dikreditkan. Jika ia gagal — tamat masa, sambungan ditolak, ralat TLS, pengalihan, atau apa-apa status bukan 2xx — kami mencuba semula mengikut jadual tetap:
1 min → 5 min → 15 min → 1 jam → 6 jam → 24 jamTujuh percubaan kesemuanya, tersebar dalam lebih kurang 31 jam. Yang awal rapat antara satu sama lain kerana puncanya biasanya penerima yang sedang dimulakan semula dan sudah pun kembali; yang lewat jarang-jarang kerana menghentam pelayan yang sudah sehari tumbang tidak membantu sesiapa.
Selepas percubaan terakhir, penghantaran ditandakan dropped dan kami berhenti dengan sendirinya. Ia tidak hilang: baris pembayaran dalam kawasan akaun anda menunjukkan keadaannya, bilangan percubaan dan kelas ralat, dengan butang Hantar semula yang memulakan pusingan baharu kesemua tujuh percubaan. Jalan lain anda ialah GET /api/merchant/v1/invoices/{invoice_id} — invois sentiasa tahu statusnya sendiri.
Rupa yang mesti ada pada URL webhook
URL disemak semasa anda menyimpannya, dan sekali lagi sebelum setiap satu penghantaran. URL yang gagal semakan dijawab dengan 422 dan code: "webhook_url_rejected" semasa penyimpanan, dan menandakan penghantaran sebagai failed — tanpa cubaan semula — jika ia mula gagal kemudian. Peraturannya:
- `https://` sahaja, dan port 443. Webhook membawa butiran pembayaran; dalam http biasa ia boleh dibaca oleh sesiapa sahaja di sepanjang laluan.
- Nama domain, bukan alamat IP. Anda memerlukan sijil bagaimanapun juga, dan sijil tidak dikeluarkan untuk IP kosong.
- Tiada `localhost`, dan tiada nama
.local,.internal,.corp,.lanatau.test— pelayan kami tidak dapat mencapai rangkaian anda, dan nama yang menyelesai ke dalam rangkaian kami ialah tepat apa yang tidak boleh kami panggil. - Tiada kelayakan dalam URL (
https://user:pass@…). Letakkan token anda sendiri dalam laluan atau parameter pertanyaan jika anda memerlukannya. - Setiap alamat yang diselesaikan oleh nama itu mesti awam — A dan AAAA kedua-duanya. Julat peribadi, gelung balik, link-local dan CGNAT ditolak, dan semakan diulang sebelum setiap penghantaran, jadi menghalakan rekod ke
127.0.0.1kemudian pun tidak berkesan. - Pengalihan ialah kegagalan, bukan lompatan. Kami tidak mengikutinya: alamat yang anda berikan kepada kami telah disemak, yang berada dalam pengepala
Locationtidak.
Jawab dengan pantas
Mana-mana 2xx sudah memadai, dalam masa sepuluh saat — itulah keseluruhan tamat masa kami, termasuk sambungan. Jawab dahulu, lakukan kerja yang perlahan selepas itu; titik akhir yang menunggu pangkalan datanya sendiri sebelum membalas akhirnya akan direkodkan sebagai tamat masa dan dicuba semula, dan anda akan memproses peristiwa yang sama dua kali. Apa-apa yang lain — 4xx, 5xx, pengalihan, tergantung — dikira sebagai percubaan yang gagal dan kembali ke jadual di atas.
Penghantaran, secara jujur
Yang dijamin ialah mekanisme penghantarannya: tujuh percubaan dalam lebih kurang 31 jam, penghantaran semula secara manual daripada kawasan akaun anda, dan titik akhir invois yang sentiasa tahu status sebenar. Bina aliran anda supaya webhook yang tidak pernah tiba tidak merugikan anda — baca invois pada halaman terima kasih anda, atau selaraskan invois terbuka sekali sejam. Webhook ialah laluan pantas, bukan satu-satunya laluan.
A complete receiver#
Signature, deduplication and a fast answer, end to end.
The snippets above verify one signature. This is the whole endpoint: raw body, signature check, deduplication by event_id, a fast 2xx, and the one condition that is allowed to mark an order paid.
Node.js with Express. express.raw is the part people get wrong: express.json() hands you a parsed object, and bytes you re-serialise from it are not the bytes we signed.
const express = require("express")
const crypto = require("node:crypto")
const app = express()
const SECRET = process.env.PAYSELL_WEBHOOK_SECRET
function isOurs(body, signature, timestamp) {
const sentAt = Number(timestamp)
if (!Number.isFinite(sentAt)) return false
if (Math.abs(Date.now() / 1000 - sentAt) > 300) return false // ±5 minutes
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(timestamp + ".").update(body) // raw Buffer, not a parsed object
.digest("hex")
const a = Buffer.from(expected), b = Buffer.from(signature ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
app.post(
"/paysell/webhook",
express.raw({ type: "application/json" }), // NOT express.json()
async (req, res) => {
const signature = req.get("X-Paysell-Signature")
const timestamp = req.get("X-Paysell-Timestamp")
if (!isOurs(req.body, signature, timestamp)) return res.sendStatus(401)
const event = JSON.parse(req.body.toString("utf8"))
// Answer first: 10 seconds is the whole timeout, connection included.
res.sendStatus(200)
// Deduplicate. In real code this is a unique column, not a Set.
if (await alreadyHandled(event.event_id)) return
await remember(event.event_id)
if (event.type !== "payment.credited") return
const { order_id, status, credited, asset, tx_hash } = event.data
// The only condition that may release the goods.
if (status !== "paid" && status !== "overpaid") return
await markOrderPaid(order_id, { credited, asset, tx_hash })
}
)Python with Flask. request.get_data() is the raw body; request.form and request.json are not.
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["PAYSELL_WEBHOOK_SECRET"]
def is_ours(body: bytes, signature: str, timestamp: str) -> bool:
try:
sent_at = int(timestamp)
except (TypeError, ValueError):
return False
if abs(time.time() - sent_at) > 300: # ±5 minutes
return False
signed = timestamp.encode() + b"." + body
expected = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + expected, signature)
@app.post("/paysell/webhook")
def paysell_webhook():
body = request.get_data() # raw bytes, unparsed
if not is_ours(body, request.headers.get("X-Paysell-Signature", ""),
request.headers.get("X-Paysell-Timestamp", "")):
return "", 401
event = json.loads(body)
# Deduplicate. In real code this is a unique column, not a set.
if already_handled(event["event_id"]):
return "", 200 # a repeat is still a success
remember(event["event_id"])
if event["type"] == "payment.credited":
data = event["data"]
# The only condition that may release the goods.
if data["status"] in ("paid", "overpaid"):
mark_order_paid(data["order_id"], data)
return "", 200 # 2xx within 10 secondsWhat the code is doing, and why
- Verify before anything acts on the body. An unsigned request that reaches your business logic is a paid order for whoever found your URL.
- Answer 2xx first, work afterwards. Ten seconds is the whole timeout, connection included. A handler that waits for its own database gets recorded as a timeout and retried, and you process the same event twice.
- Deduplicate on `event_id` in storage that survives a restart. The in-memory set in the examples keeps them short; a real one is a unique column in your database.
- Mark the order paid only on `status: paid` or `overpaid`.
underpaidmeans part of the money arrived and the invoice is still open, and a deposit in the wrong coin never makes an invoice paid either. - Answer 2xx to a duplicate too. A repeat that gets a 4xx looks like a failure to us and comes back again on the schedule.
payment.rejected arrives at the same endpoint. It means a deposit held for an additional check was declined and the money will not be credited: release nothing, and if the invoice was already paid by an earlier transfer, this event is about the extra deposit, not about that payment.
Rujukan status#
Setiap status invois dan pembayaran, dijelaskan.
Invois
| Status | Maksud | Apa yang perlu dilakukan |
|---|---|---|
| pending | Menunggu pembayaran. | Kekalkan pesanan terbuka. |
| paid | Dibayar sepenuhnya. | Lepaskan barangan. |
| overpaid | Lebih tiba daripada diminta. Lebihan dikreditkan kepada anda sepenuhnya. | Lepaskan barangan; bayar balik perbezaan jika anda mahu. |
| underpaid | Kurang tiba daripada diminta. Invois kekal terbuka dan mengekalkan alamatnya: pembeli boleh menambahnya ke tempat yang sama, dan paid_minor memberitahu berapa yang sudah masuk. Ia kekal boleh dibayar sepanjang baki hayatnya ditambah tempoh tangguh 24 jam selepas expires_at. | Tunggu tambahan itu, atau selesaikan dengan pelanggan. Jangan lepaskan barangan — invois belum dibayar. |
| expired | Tetingkap sudah ditutup, termasuk tempoh tangguh. Mungkin masih membawa wang: apa sahaja yang sampai kekal pada baki anda, dan paid_minor memberitahu berapa banyak. | Tawarkan invois baharu. Jangan terima pembayaran ke alamat lama: sebaik sahaja invois luput, alamat itu kembali ke dalam kolam, dan pemindahan yang sangat lewat menjadi kes sokongan, bukan kredit automatik. Semak paid_minor sebelum memberitahu pelanggan bahawa tiada apa-apa diterima. |
| cancelled | Dibatalkan oleh anda. Alamat dilepaskan semula ke kolam. | Tiada apa-apa. |
Pembayaran
Kelihatan dalam kawasan akaun anda; berguna semasa menyokong pelanggan pertengahan pembayaran.
| Status | Maksud |
|---|---|
| detected | Dilihat di rantaian, menunggu pengesahan. |
| confirmed | Rangkaian mengesahkannya. Pengkreditan seterusnya. |
| credited | Pada baki anda. Inilah masa webhook berlaku. |
| review | Ditahan untuk semakan tambahan — contohnya, syiling tiba pada alamat tanpa invois terbuka. |
| rejected | Tidak dikreditkan. Sebabnya direkodkan. |
Apabila pembayaran masuk ke `review`
Sesetengah deposit ditahan untuk semakan tambahan dan bukannya dikreditkan serta-merta: jumlah yang luar biasa besar, syiling yang tiba pada alamat tanpa invois terbuka, atau dua sumber blockchain yang kami tanya tidak sepakat tentang apa yang berlaku. Tiada apa yang hilang — wang itu menunggu keputusan, dan webhook dihantar sebaik keputusan dibuat, yang mungkin beberapa minit atau beberapa jam kemudian. Anggap panggilan balik yang tidak sampai pada pembayaran berstatus review sebagai perkara biasa, bukan kegagalan. Jika ia penting bagi sesuatu pesanan, tanya sokongan dan sebutkan tx_hash.
Jumlah#
Keluar dalam unit biasa, balik dalam unit terkecil.
Hantar jumlah dalam unit biasa syiling, sebagai rentetan — "1.5" bermaksud satu setengah. Bukan nombor JSON dan bukan unit terkecil.
| Aset | Perpuluhan | Anda hantar | amount_minor dalam respons |
|---|---|---|---|
| TON | 9 | "1.5" | "1500000000" |
| USDT_TON | 6 | "1.5" | "1500000" |
Rentetan dan bukan nombor, kerana nombor JSON ialah double IEEE-754 dan jumlah besar dalam nanoton tidak lagi muat di dalamnya dengan tepat. Lebih banyak tempat perpuluhan daripada yang ada pada syiling menghasilkan 422, bukan pembundaran senyap wang anda. Webhook berjalan sebaliknya: di sana amount, fee dan credited ialah integer dalam unit terkecil, kerana pihak itu dibaca oleh kod, bukan oleh manusia.
// Send amounts in the coin's normal units, as a string:
const amount = "1.5" // one and a half TON or USDT
// In responses, amount is that same human string; amount_minor is the
// integer in smallest units — use it for exact maths, as a string or BigInt:
BigInt(invoice.amount_minor) // e.g. 1500000nHad#
Minimum, maksimum, dan had kadar.
| Had | Nilai | Apabila dilanggar |
|---|---|---|
| Invois minimum | 0.1 TON · 3 USDT | 422 |
| Invois maksimum | 7000 TON · 10000 USDT | 422 |
| Invois sejam, setiap kedai | 60 | 429 |
| Invois terbuka serentak | 20, bertambah dengan setiap invois yang dibayar, sehingga 200 | 429 |
| Jangka hayat invois | 1 minit – 24 jam (lalai 2 jam) | 422 |
| Permintaan API setiap kunci | 120 seminit | 429 + Retry-After |
Minimum bukan birokrasi. Yuran kami adalah peratusan, tetapi mengutip pembayaran memerlukan kos tetap: memindahkan USDT keluar dari alamat penerimaan bermakna membiayainya dengan gas terlebih dahulu, daripada poket kami sendiri. Di bawah beberapa dolar yuran tidak menampung pengendalian, dan menerima pembayaran sedemikian bermakna mengkreditkan anda wang yang tidak ekonomik untuk dipindahkan.
Had maksimum bukan tentang peniaga besar — ia perangkap bagi kesilapan unit. Hantar "5000000" sedangkan maksud anda "5", dan jika tidak anda akan mendapat invois lima juta dolar: pembeli nampak angka yang mengarut lalu pergi. Pesanan sebenar tidak pernah mencecah siling ini; yang tersilap sentiasa mencecahnya. Kedua-dua siling ialah tetapan (invoice_max_ton, invoice_max_usdt) dan boleh dinaikkan untuk kedai anda — tanyakan sahaja.
Had sejam dan had invois terbuka kedua-duanya melindungi kolam alamat. Setiap invois terbuka memegang satu alamat penerimaan, dan gelung yang tidak terkawal pada satu laman jika tidak akan menghabiskan kolam untuk semua orang. Kedai baharu boleh memegang 20 invois terbuka serentak; elaun itu bertambah satu bagi setiap invois yang benar-benar telah dikutipnya, sehingga siling 200. underpaid dikira sebagai terbuka — ia masih memegang alamatnya, menunggu bakinya. Membatalkan invois yang ditinggalkan mengembalikan alamatnya serta-merta. Cubaan semula dengan idempotency_key yang sama tidak dikira terhadap had sejam.
Had permintaan ialah 120 seminit bagi setiap kunci API — dua panggilan sesaat, jauh melebihi mana-mana aliran pesanan sebenar. 429 membawa pengepala Retry-After dalam saat: tunggu selama itu dan bukannya mencuba semula dalam gelung ketat, yang hanya menolak tetingkap itu lebih jauh.
Ralat#
Kod status yang akan anda lihat sebenarnya.
Ralat kembali sebagai JSON, dalam dua bentuk. Apa sahaja yang diputuskan oleh kami atau oleh teras pemprosesan meletakkan pasangan {code, message} di bawah detail. Badan permintaan yang gagal pengesahan pula meletakkan senarai ralat medan di situ. Semak yang mana satu anda terima sebelum membaca detail.code — dan bercabang pada `code`, tidak sekali-kali pada `message`: perkataannya boleh berubah bila-bila masa, kodnya tidak.
{
"detail": {
"code": "invalid_input",
"message": "invoice amount below the minimum: 0.010000 USDT_TON, minimum 3.000000 USDT_TON"
}
}{
"detail": [
{
"type": "string_type",
"loc": ["body", "amount"],
"msg": "Input should be a valid string",
"input": 5
}
]
}| Status | Bila | Apa yang perlu dilakukan |
|---|---|---|
| 401 | Kunci hilang, salah, atau dibatalkan. | Semak pengepala. Keluarkan semula kunci jika ia telah dibatalkan. |
| 404 | Tiada invois sedemikian, atau ia milik kedai lain. | Semak id. Kedua-dua kes dijawab sama dengan sengaja, supaya id tidak boleh diselidik. |
| 409 | Invois berada dalam keadaan yang melarang ini. | Baca status semasanya dahulu. |
| 422 | Permintaan salah bentuk, atau jumlah berada di luar had invois. | Mesej menamakan kedua-dua nilai yang dihantar dan hadnya. |
| 429 | Terlalu banyak invois pada jam ini, terlalu banyak terbuka serentak, atau terlalu banyak permintaan. | Tunggu sehingga Retry-After tamat, kemudian cuba semula. |
| 502 | Kami tidak dapat menghubungi teras pemprosesan. | Cuba semula dengan idempotency key yang sama. |
Kod
Bentuk bagi keputusan pihak kami ialah {"detail": {"code": …, "message": …}}. Inilah kod yang dikembalikan oleh API peniaga.
| Kod | Status | Maksud |
|---|---|---|
| invalid_api_key | 401 | Kunci hilang, cacat bentuk, tidak dikenali atau dibatalkan. Keempat-empatnya menjawab serupa, jadi kunci tidak boleh diselidik. |
| not_found | 404 | Tiada objek sedemikian, atau ia milik kedai lain. |
| invalid_input | 422 | Permintaan tidak lulus pengesahan dalam teras — jumlah yang tidak sah, terlalu banyak tempat perpuluhan, jumlah di luar had invois. |
| conflict | 409 | Tindakan itu bercanggah dengan keadaan semasa, seperti membatalkan invois yang tidak lagi terbuka. |
| too_many_requests | 429 | Had kadar: invois sejam, invois terbuka, atau permintaan seminit. Retry-After memberitahu berapa lama perlu menunggu. |
| cbc_unreachable | 502 | Kami tidak dapat menghubungi teras pemprosesan. Cuba semula dengan idempotency_key yang sama. |
| webhook_url_rejected | 422 | Hanya semasa menyimpan kunci: URL webhook gagal semakan di atas. detail.reason menamakan peraturan yang mana — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials, dan seterusnya. |
502 tidak bermakna invois tidak dicipta — permintaan mungkin telah berjaya dengan jawapan hilang dalam perjalanan pulang. Cuba semula dengan idempotency_key yang sama dan anda sama ada mendapat invois sedia ada atau yang baharu, tidak pernah dua.
Checkout: what the buyer sees#
The hosted payment page, and when to build your own.
payment_url points at https://paysell.me/pay/{invoice_id}. One page, no account, no login, mobile first, and nothing for you to build.
On the page
- Your shop's name, the amount and the coin, large, with the invoice's description underneath.
- A countdown to
expires_at— plus the 24-hour grace period when the invoice isunderpaid. - The invoice's short id with a copy button, so a buyer can quote it to your support.
- Wallet buttons: Tonkeeper and MyTonWallet open with the address and the amount already filled in. Other reveals a QR code and the address with a copy button.
- A warning that only this invoice's coin, on the TON network, may be sent — anything else is lost.
How the page reacts
| Invoice | What the buyer sees |
|---|---|
| pending | “Waiting for payment”, with the wallet choices and the countdown. The page re-reads the invoice every five seconds. |
| underpaid | “Received X of Y”, the exact remainder still owed, and the same address to send it to. The wallet link is prefilled with what is missing, not with the original total — otherwise the buyer would pay twice. |
| paid · overpaid | “Payment received”, and a button back to your shop if the shop has a URL. |
| expired | “Payment window closed”. If money did arrive, the amount is named with a note to contact you — silence here would send the buyer looking for their coins. |
| cancelled | “Payment cancelled”, with a link back to your shop. |
If you build your own
You gain your own branding and take on all of the above: the exact address string, the right coin, the countdown with its grace period, the underpayment case, and polling. GET /api/merchant/v1/public/invoices/{invoice_id} is the same unauthenticated read the hosted page uses — rate limited per IP, so poll it no more often than every few seconds. Print the address exactly as returned.
Typical integration mistakes#
The handful that account for most broken integrations.
None of these are exotic. Every one of them has cost somebody a day.
Sending the amount as a number
{"amount": 5}is a422. It has to be the string"5": JSON numbers are IEEE-754 doubles, and a large sum in nanotons stops being exactly representable in one.Sending the smallest unit
"5000000"for 5 USDT is a mistake in units, and the upper invoice limit exists to catch it. Smallest units are what comes back in webhooks, not what goes out in requests.Treating the webhook's arrival as payment
Read
data.status.underpaidis not paid, and a deposit in a coin the invoice did not ask for never makes it paid either. Release the goods onpaidoroverpaid, on nothing else.Not checking the timestamp
A signature on its own never expires. Without the ±5 minute window on
X-Paysell-Timestamp, a delivery captured once can be replayed at any time and will still verify.Verifying the signature over re-serialised JSON
Parse the body and serialise it again and the bytes change — key order, spacing — and the HMAC no longer matches. Sign the raw bytes exactly as received.
No deduplication
The same
event_idwill arrive twice sooner or later: we retry until you answer 2xx, and a response lost on the way back looks like a failure from here. The second arrival must do nothing.Using the
Idempotency-KeyheaderThis API reads
idempotency_keyfrom the request body; the header is not read at all. A retry without the field opens a second invoice for the same order.Assuming
detailis always an objectIt is
{code, message}for anything we or the core decide, and a list of field errors when the body itself fails validation. Check which one you got before readingdetail.code.
Bayaran balik#
Cara memulangkan wang kepada pelanggan.
Bayaran balik dibuat melalui sokongan, bukan melalui panggilan API. Bayaran balik ialah pemindahan baharu ke alamat yang diberikan oleh seseorang, dan pemproses pembayaran yang menghantar wang kembali secara automatik atas panggilan API ialah pemproses pembayaran yang boleh dipaksa menghantar wang ke alamat penyerang. Jadi ia sengaja dibuat secara manual.
Untuk membayar balik pembeli, buka tiket sokongan daripada kawasan akaun anda dengan invoice_id atau tx_hash, jumlahnya, dan alamat untuk dihantar. Operator menyemak pembayaran, mengeluarkan wang daripada baki anda, dan menjawab dalam tiket yang sama. Jangkakan ini mengambil masa satu hari bekerja, bukan seminit.
Dua akibat yang berbaloi diambil kira semasa mereka bentuk. Lebihan bayaran dikreditkan kepada anda sepenuhnya — kami tidak menyimpan sesen pun daripadanya — jadi memulangkan perbezaan kepada pembeli yang menghantar terlalu banyak ialah keputusan anda dan mengikut laluan yang sama. Dan invois yang kurang dibayar bukan kes bayaran balik selagi ia masih terbuka: wang berada pada baki anda, alamat masih dipantau, dan pembeli boleh sekadar menambahnya. Hanya selepas tempoh tangguh, apabila invois menjadi expired dengan wang padanya, barulah ada keputusan untuk dibuat.
Ujian#
Cara menguji integrasi anda sebelum pelancaran.
Kunci di sini adalah kunci langsung: setiap kunci yang dikeluarkan ialah kunci sk_live_ terhadap teras pengeluaran dan mainnet TON. Tiada persekitaran ujian berasingan, dan itu ada kebaikannya: anda melalui laluan yang sama persis dengan laluan pesanan sebenar anda nanti.
Jadi ujilah seperti anda menguji apa-apa yang melibatkan wang sebenar: pada jumlah kecil. Cipta invois untuk jumlah minimum (0.1 TON atau 3 USDT), bayar daripada dompet anda sendiri, dan perhatikan keseluruhan laluan — halaman pembayaran, webhook, semakan tandatangan, pesanan anda bertukar kepada dibayar. Yuran tetap dikenakan, dan syiling benar-benar berpindah.
Bahagian yang boleh anda uji tanpa membelanjakan apa-apa: mencipta dan membaca invois, membatalkannya, 422 pada jumlah yang salah bentuk, 401 pada kunci yang salah, dan pengesahan tandatangan anda sendiri — tandatangani badan contoh dengan rahsia anda dan berikannya kepada pengendali anda sendiri. Yang benar-benar memerlukan pembayaran sebenar hanyalah langkah terakhir: webhook payment.credited yang sebenar.
Rancang integrasi supaya ia tidak bergantung pada sandbox atau pembayaran simulasi: laluan langsung lebih cepat — dan lebih jujur — untuk disahkan.
For AI agents and LLMs#
Machine-readable copies of this page, and a prompt to start from.
Everything on this page also exists in a form a model can read directly. Point your assistant at one of these instead of pasting screenshots of documentation into a chat.
The three files
| File | What it is | Use it for |
|---|---|---|
| /llms-full.txt | The whole documentation as one markdown file: endpoints, fields, statuses, limits, errors, webhooks with working verification code, the fee, the checkout page, the checklist. | Pasting into a model's context, or letting an agent fetch it. Start here. |
| /llms.txt | A short index in the llms.txt format: what Paysell is, the five rules that decide whether an integration works, and links to everything else. | Letting an agent discover the rest on its own. |
| /openapi.json | OpenAPI 3.1, generated from the running application's own models, both webhook events included. | Generating a client, or loading into anything that speaks OpenAPI. |
A prompt to start from
Copy this, replace the stack, and hand it to your assistant. It names the four things that go wrong most often, so the answer does not have to be corrected afterwards.
Read https://paysell.me/llms-full.txt and implement Paysell payments in my <stack>:
create invoices (POST /api/merchant/v1/invoices, Bearer sk_live_ key, amount as a
decimal string in normal units), redirect the buyer to payment_url, verify webhook
signatures (HMAC-SHA256 over "{timestamp}.{raw_body}", header X-Paysell-Signature,
reject anything whose X-Paysell-Timestamp is more than 300 seconds off), deduplicate
by event_id, answer 2xx within 10 seconds, and mark orders paid only on a
payment.credited event whose data.status is "paid" or "overpaid".Feeding it to a specific tool
- Agents with web access — Claude Code, Cursor, Windsurf and the like: give them the
/llms-full.txtlink. One fetch, no setup. - A chat window — ChatGPT, Claude, Gemini: paste the contents of
/llms-full.txtinto the conversation or attach it as a file. It is written to fit in one message. - OpenAPI tooling — client generators, Postman, an agent's tool schema: point it at
https://paysell.me/openapi.json. Itsserversentry already carries the production base URL, so generated calls go to the right place.
Senarai semak sebelum pelancaran#
Sepuluh perkara untuk disemak sebelum pelancaran.
- Kunci hanya di sisi pelayan, tidak sekali-kali dalam JavaScript pelayar.
- Tandatangan webhook disahkan terhadap
"{timestamp}.{raw_body}", dalam masa malar. - Penghantaran yang lebih lama daripada lima minit ditolak, dan jam pelayan berada pada NTP.
X-Paysell-Event-Idyang berulang tidak melakukan apa-apa pada kali kedua.- Webhook menjawab 2xx dalam masa sepuluh saat; kerja perlahan berlaku selepas itu.
- URL webhook ialah domain https:// pada port 443, tanpa pengalihan di hadapannya.
- Webhook yang terlepas boleh dihadapi: titik akhir invois dibaca pada halaman terima kasih atau semasa imbasan penyelarasan.
idempotency_keydijana sekali bagi setiap pesanan dan digunakan semula pada percubaan semula.- Jumlah keluar sebagai rentetan dalam unit biasa; angka webhook dibaca sebagai unit terkecil.
- Alamat dipaparkan tepat seperti dikembalikan, tanpa diubah suai.
overpaiddanunderpaiddikendalikan, bukan sekadarpaid;expiredmungkin masih membawapaid_minor.- Barang dilepaskan pada
status: paidatauoverpaid, tidak sekali-kali hanya kerana panggilan balik itu sampai. 429dikendalikan dengan menunggu sehinggaRetry-Aftertamat, bukan dengan mencuba semula serta-merta.- Baki dibaca daripada kami, bukan dijejaki secara berasingan sebagai kebenaran.
Ada yang tidak jelas?
Jika halaman ini tidak menjawab soalan anda, itu adalah jurang dalam dokumentasi dan patut diberitahu kepada kami. Tulis dari kawasan akaun anda dan kami akan membaiki halaman itu, bukan sekadar jawapannya.