Fogadjon kriptovaluta-fizetéseket
A Paysell TON-t és USDT-t számol el a TON hálózaton. Ön létrehoz egy számlát, mi adunk egy linket, és egy aláírt visszahívást (callback) kap, amint a pénz megerősítést kapott a láncon, és jóváírásra került az egyenlegén.
Áttekintés#
Mit csinál a Paysell, és mit nem.
A Paysell fizetési szolgáltató, nem tárca (wallet). Soha nem kezel privát kulcsokat, nem figyeli a blokkláncot, és nem dönt arról, mikor véglegesült egy tranzakció — ezt mi vállaljuk magunkra.
Minden számlához saját fogadó cím tartozik. Amikor a vásárló kifizeti, megvárjuk, amíg a hálózat megerősíti az átutalást, levonjuk a díjunkat, és a maradékot jóváírjuk az Ön egyenlegén. Bármilyen címre kifizetheti.
Hogyan zajlik egy fizetés#
Hat lépés, többségük a miénk.
Hat lépés, többségük a miénk:
- 1
Az Ön ügyfele a fizetésre kattint
Az Ön szervere meghívja API-nkat az összeggel és a saját rendelésazonosítójával.
- 2
Kiadunk egy címet
Egy friss fogadó cím kerül kiválasztásra egy előre generált készletből, és hozzárendelésre kerül ehhez a számlához. Egy cím pontosan egy nyitott számlához tartozik — így párosítjuk a fizetést a számlához.
- 3
Az ügyfél elküldi az érméket
Beolvassa a QR-kódot, vagy kimásolja a címet. Küldje az általunk visszaadott
payment_urlcímre, és az oldalt mi kezeljük Ön helyett — összeg, cím, QR-kód, visszaszámláló, élő állapot. - 4
Észleljük az átutalást
Két független blokklánc-adatforrást kérdezünk le, és összehasonlítjuk a válaszaikat. Ha nem egyeznek, megállunk, ahelyett hogy a kényelmesebb választ fogadnánk el.
- 5
Megvárjuk a véglegességet
Bekerülés a masterchainbe, plusz három blokk azon felül. Nagyjából tizenöt másodperc — egy fizetés, amely rendezettnek tűnik, majd később eltűnik, az Ön vesztesége lenne, ezért nem vállaljuk ezt a kockázatot.
- 6
Jóváírva, és Ön értesítést kap
A díj levonásra kerül, a maradék az egyenlegén landol, és egy aláírt webhook érkezik a szerverére az Ön
order_idazonosítójával.
Nagyjából egy perc a fizetéstől a visszahívásig: körülbelül tizenöt másodperc a hálózati megerősítésekre, a többi a figyelt címek átvizsgálása a részünkről.
Hova kerül a pénz#
A díj, és mi alapján számítjuk.
A díj 0,2%, rögzítve az Ön áruháza számára a regisztráció pillanatában. Ha a standard díj később megváltozik, az Öné nem — ez számként van rögzítve minden számlában, nem egy beállításra való hivatkozásként.
A díjat abból vonjuk le, ami ténylegesen megérkezett, nem abból, amit a számla kért. Számlázzon 5 USDT-t, és kapjon 20-at — a díj a 20-ból számolódik. Alulfizetés esetén abból számolódik, ami megérkezett.
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 USDTA túlfizetés teljes egészében jóváírásra kerül — nem tartjuk meg a különbözetet. Az alulfizetés nyitva hagyja a számlát, hogy a vásárló feltölthesse ugyanarra a címre.
Gyors kezdés#
Öt perc az első számlájáig.
Öt lépés. Kettő kattintás a fiókterületén, egy darab kérés a szerveréről, az utolsó kettő pedig magától történik.
- 1
Hozzon létre egy áruházat
A fiókterületén. Azonnal elkezdi fogadni a fizetéseket — felülvizsgálatra várás nélkül. A hitelesítés csendben, a háttérben zajlik, és csak a kifizetéseket korlátozza, nem a bejövő fizetéseket.
- 2
Állítson ki egy API-kulcsot
Az Ön áruháza → API-kulcsok → Új kulcs. A kulcs és a webhook-titok egyszer jelenik meg, többé soha. Tárolja őket úgy, ahogyan egy adatbázis-jelszót tárolna, és soha ne küldje el őket böngészőnek.
- 3
Hozzon létre egy számlát
Egy kérés a szerveréről, egy link válaszul. Az alábbi négy példa pontosan ugyanazt küldi.
- 4
Küldje a vásárlót a
payment_urlcímreEz maga a teljes fizetési oldal — összeg, cím, QR-kód, visszaszámlálás, élő állapot —, és nincs mit felépíteni. Hogy pontosan mit lát a vásárló, lásd: Fizetési oldal.
- 5
Várja meg a webhookot
Amint a pénz a láncon megerősítést nyer és jóváíródik, egy aláírt
payment.creditedeseményt POST-olunk a szerverére. Ellenőrizze az aláírást, majd jelölje a rendelést fizetettnek — de csak akkor, ha adata.statusértékepaidvagyoverpaid. Lásd Webhookok.
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"
}'Irányítsa át a vásárlót a válaszban lévő payment_url címre. Ennyi — a többi webhookként érkezik.
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.
Hitelesítés#
Az API-kulcsa, és hogyan használjuk.
Minden kérés hordozza az Ön kulcsát az Authorization fejlécben:
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxAAz itt kiadott minden kulcs sk_live_ előtaggal kezdődik. Az sk_test_ előtag csak a teszthálózatra irányított telepítésen létezik, ilyet pedig nem kínálunk — lásd Tesztelés. Egyirányú hash-t tárolunk, nem magát a kulcsot, így senki, minket is beleértve, nem tudja azt Önnek újra megmutatni. Elveszett? Állítson ki egy újat, és vonja vissza a régit.
Az áruházat a kulcsból vezetjük le, ezért egyetlen kérés sem vesz fel áruház-azonosítót. Egy kulcs csak a saját áruházában járhat el.
Az útvonal verziót hordoz: /api/merchant/v1/…. Egy verzión belül csak mezőket adunk hozzá — semmit nem nevezünk át, és semmi nem változtat csendben jelentést. Az a változás, ami eltörné a kódodat, új előtagot kap, /v2-t, a /v1 pedig bejelentett ideig tovább működik.
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.
Számla létrehozása#
POST /api/merchant/v1/invoices
/api/merchant/v1/invoicesKérés törzse
| Mező | Típus | Kötelező | Leírás |
|---|---|---|---|
| asset | string | igen | TON vagy USDT_TON. |
| amount | string | igen | Az érme normál egységei, stringként: "5" az 5 USDT. Nem több tizedesjegy, mint amennyi az érmének van. Lásd Összegek. |
| order_id | string | nem | Az Ön saját hivatkozása, legfeljebb 200 karakter. Minden webhookban visszaérkezik — így párosítja a fizetést a rendeléshez. |
| description | string | nem | Legfeljebb 1000 karakter. A vásárlónak jelenik meg a fizetési oldalon. |
| ttl_minutes | number | nem | Meddig marad fizethető a számla, percben. 1–1440; ha kihagyja, az alapértelmezés érvényes — ma 2 óra. |
| idempotency_key | string | nem | Legfeljebb 200 karakter. Küldje ugyanazt az értéket újrapróbálkozáskor, és ugyanazt a számlát kapja vissza, nem egy másodikat. Ez a törzs egyik mezője, nem az Idempotency-Key fejléc — azt a fejlécet itt nem olvassuk. |
Válasz · 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"
}Hozzárendelés a rendeléséhez
| Mező | Mit kezdjen vele |
|---|---|
| invoice_id | Tárolja el a rendeléséhez. Ez azonosítja a fizetést mindenhol máshol. |
| payment_url | Irányítsa ide a vásárlót. Semmi mást nem kell felépíteni. |
| address | Csak akkor, ha a saját fizetési oldalát jeleníti meg. Mutassa meg pontosan úgy, ahogy kapta — lásd a figyelmeztetést alább. |
| amount | Az összeg normál egységben, pontosan úgy, ahogy küldted. Ezt jelenítsd meg. |
| amount_minor | Ugyanaz az összeg egész számként, a legkisebb egységben. Ezzel számolj. |
| expires_at | Mutasson egy visszaszámlálót. Ennek elteltével a cím megszűnik figyelve lenni ehhez a számlához. |
| status | Itt mindig pending. A valódi változások webhookon keresztül érkeznek. |
UQ… az éles hálózaton, 0Q… a teszthálózaton). Ha átalakítja, csinosítja, vagy ugyanazon cím másik kódolására cseréli, a még nem telepített tárcára küldött érmék visszapattannak a küldőhöz.Számla lekérdezése#
GET /api/merchant/v1/invoices/{invoice_id}
/api/merchant/v1/invoices/{invoice_id}Ugyanaz a szerkezet, mint fent, azzal, hogy a status, a paid és a paid_minor a jelenlegi állapotot tükrözi: a paid azt mutatja, mennyi érkezett normál egységben, a paid_minor pedig ugyanezt egész számként, a legkisebb egységben. Hasznos tartalék, ha egy webhook elveszett, vagy egy köszönőoldalon.
Legfeljebb néhány másodpercenként kérdezze le, és tekintse a webhookokat elsődleges csatornának. A más áruházhoz tartozó számlák 404-gyel válaszolnak — nem 403-mal, így egy azonosító létezése nem vizsgálható ki.
Számla lemondása#
POST /api/merchant/v1/invoices/{invoice_id}/cancel
/api/merchant/v1/invoices/{invoice_id}/cancelLezár egy még nyitott számlát — pending vagy underpaid —, és felszabadítja a címét. Használja, amikor a vásárló megszakítja a fizetést: a címek véges erőforrás, és a visszaadásuk egészségesen tartja a készletet.
Az a számla, amely már nincs nyitva, 409-cel válaszol. Egy underpaid számla lemondása senkinek nem ad vissza érméket: a már jóváírt pénz az egyenlegén marad, és mindössze a feltöltés lehetősége zárul le.
Webhookok#
Mi érkezik, és hogyan ellenőrizze.
A kulcs létrehozásakor adjon meg egy webhook-URL-t. POST-ot küldünk rá, amikor egy fizetés jóváíródik — és amikor egy további ellenőrzésre visszatartott befizetést elutasítunk. Minden kézbesítés alá van írva, és nagyjából másfél napon át újrapróbálkozunk, amíg 2xx választ nem kapunk. Az árut status: paid vagy overpaid esetén adja ki, ne pusztán a hívás megérkezésére.
Események
| Esemény | Mikor | Mit tartalmaz a törzs |
|---|---|---|
| payment.credited | Az átutalás megerősítve a láncon, a jutalékunkat levontuk, a többi az egyenlegén van. | Az alább felsorolt mezők. |
| payment.rejected | Egy további ellenőrzésre visszatartott befizetést (lásd Állapot-referencia) elutasítottak. A pénz nem kerül az egyenlegére. | invoice_id, order_id, asset, amount, tx_hash és reason. Az árut ne adja ki; ha a számla egy korábbi átutalástól már paid volt, ez az esemény a többletbefizetésről szól, nem arról a fizetésről. |
Mi érkezik
{
"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"
}
}Mezők hozzárendelése
| Mező | Jelentés |
|---|---|
| event_id | Eseményenként egyedi; az X-Paysell-Event-Id fejlécben is szerepel. Tárolja el, és hagyja figyelmen kívül az ismétléseket — lásd alább. |
| data.order_id | Az Ön hivatkozása. Ez alapján keresse meg a rendelését. |
| data.amount | Amennyit a vevő ebben az átutalásban küldött, a legkisebb egységben — ellentétben az API-val, amely normál egységeket vár. |
| data.fee | Amennyit levontunk, a legkisebb egységben. |
| data.credited | Amennyi az egyenlegére került: amount − fee, a legkisebb egységben. |
| data.paid_minor | Az eddig összesen beérkezett összeg ezen a számlán, a legkisebb egységben. underpaid esetén ez a lényeges mező: az állapot azt mondja, kevesebb érkezett, ez pedig azt, hogy mennyivel kevesebb. |
| data.asset | Az érme, amely ténylegesen megérkezett. Nem feltétlenül az, amit a számla kért. |
| data.asset_mismatch | Csak akkor van jelen, és akkor true, ha a beérkezett érme nem a számla érméje. A pénz jóváíródik Önnek, de a számla fizetetlen marad, és a status sosem lesz paid. |
| data.invoice_asset | Az asset_mismatch mellett érkezik: az az érme, amelyet a számla valójában kér. |
| data.status | A számla mostani állapota: pending, underpaid, paid, overpaid vagy expired. Hasonlítsa össze azzal, amire számított. |
| data.tx_hash | A láncon lévő tranzakció, az Ön nyilvántartásához és a supporthoz. |
Fejlécek minden kézbesítésben
X-Paysell-Event: payment.credited
X-Paysell-Event-Id: 99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp: 1789000000
X-Paysell-Signature: sha256=6f1c0e6a…| Fejléc | Jelentés |
|---|---|
| X-Paysell-Event | Az esemény típusa: payment.credited vagy payment.rejected. |
| X-Paysell-Event-Id | Eseményenként egyedi. Ez az az érték, amire deduplikálni kell. |
| X-Paysell-Timestamp | Mikor írtuk alá, unix másodpercben. Része az aláírt sztringnek. |
| X-Paysell-Signature | sha256=, majd a hexadecimális HMAC. Lásd alább. |
Az aláírás ellenőrzése
Minden kérés a kulcs létrehozásakor egyetlen alkalommal megjelenített webhook-titokkal van aláírva. Az aláírás HMAC-SHA256(secret, "{timestamp}.{raw_body}") — az időbélyeg az X-Paysell-Timestamp fejlécből, egy szó szerinti pont, majd a törzs bájtjai. Ellenőrizze, mielőtt cselekedne: enélkül bárki, aki megtudja az Ön URL-jét, kifizetett rendelést tud Önnek beadni.
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)
}A nyers törzs bájtjait írja alá, pontosan úgy, ahogy megkapta. Ha feldolgozza a JSON-t, majd újraszerializálja, a bájtok megváltoznak — kulcssorrend, szóközök —, és az aláírás nem fog egyezni. Konstans időben hasonlítson (hmac.compare_digest, crypto.timingSafeEqual): az egyszerű == hamarabb tér vissza, ha már az első bájt rossz, és ez a különbség elég ahhoz, hogy egy aláírást bájtonként kitaláljanak.
Az időbélyeg-ablak
Utasítson vissza mindent, aminek az időbélyege öt percnél távolabb van a saját órájától, bármelyik irányban. Az időbélyeg pontosan azért van az aláírt sztringen belül, hogy ne lehessen szerkeszteni az aláírás elrontása nélkül; védelemmé az ablak teszi. Nélküle egy egyszer elkapott kérés örökre érvényes marad, és bármikor visszajátszható — az aláírás önmagában sosem jár le. Tartsa a szerver óráját NTP-n, különben ez az ellenőrzés jó kézbesítéseket kezd majd elutasítani.
Duplikátumok
Ugyanaz az esemény többször is megérkezhet. Ez nem hiba: addig próbálkozunk újra, amíg 2xx választ nem kapunk, és egy olyan kézbesítés, amely sikeres volt, de a válasza soha nem ért el hozzánk, újra elmegy. Rögzítse az X-Paysell-Event-Id értékét (a törzsben event_id néven is megérkezik), és tegye, hogy a második érkezés semmit ne csináljon.
Újrapróbálkozások
Az első kísérlet azonnal indul, amint a fizetés jóváíródik. Ha elbukik — időtúllépés, elutasított kapcsolat, TLS-hiba, átirányítás vagy bármilyen nem 2xx státusz —, rögzített ütemezés szerint próbálkozunk újra:
1 perc → 5 perc → 15 perc → 1 ó → 6 ó → 24 óÖsszesen hét kísérlet, nagyjából 31 órára elosztva. A koraiak sűrűn követik egymást, mert a szokásos ok egy éppen újrainduló fogadó, amely már vissza is tért; a későbbiek ritkák, mert egy napja halott szerver ostromlása senkinek sem segít.
Az utolsó kísérlet után a kézbesítés dropped jelölést kap, és magunktól leállunk. Nem vész el: a fiókterületén a fizetés sora mutatja az állapotot, a kísérletek számát és a hiba osztályát, egy Küldés újra gombbal, amely mind a hét kísérletet újraindítja. A másik lehetősége a GET /api/merchant/v1/invoices/{invoice_id} — a számla mindig tudja a saját állapotát.
Hogyan kell kinéznie egy webhook-URL-nek
Az URL-t mentéskor ellenőrizzük, majd minden egyes kézbesítés előtt újra. Az ellenőrzésen elbukó URL mentéskor 422 választ és code: "webhook_url_rejected" hibát kap, és a kézbesítést failed állapotúra jelöli — újrapróbálkozás nélkül —, ha később kezd el bukni. A szabályok:
- Csak `https://`, és 443-as port. A webhook fizetési adatokat visz; sima http-ben ezeket bárki elolvashatja az útvonalon.
- Domainnév, nem IP-cím. Tanúsítványra amúgy is szükség van, tanúsítványt pedig csupasz IP-re nem adnak ki.
- Semmi `localhost`, és semmilyen
.local,.internal,.corp,.lanvagy.testnév — a szervereink nem érik el az Ön hálózatát, egy olyan név pedig, amely a miénken belül oldódik fel, pontosan az, amit nem szabad hívnunk. - Semmilyen hitelesítő adat az URL-ben (
https://user:pass@…). Ha kell, a saját tokenjét tegye az útvonalba vagy egy query paraméterbe. - Minden címnek, amelyre a név feloldódik, publikusnak kell lennie — A és AAAA egyaránt. A privát, loopback, link-local és CGNAT tartományokat elutasítjuk, és az ellenőrzés minden kézbesítés előtt megismétlődik, így a rekord utólagos
127.0.0.1-re állítása sem működik. - Az átirányítás bukás, nem ugrás. Nem követjük: az általunk ellenőrzött cím az, amit megadott, nem az, ami egy
Locationfejlécben áll.
Válaszoljon gyorsan
Bármilyen 2xx megfelel, tíz másodpercen belül — ennyi az egész időkorlátunk, a kapcsolódással együtt. Előbb válaszoljon, a lassú munkát utána végezze; az a végpont, amely a saját adatbázisára vár a válasz előtt, előbb-utóbb időtúllépésként kerül rögzítésre és újrapróbálkozást kap, Ön pedig kétszer dolgozza fel ugyanazt az eseményt. Minden más — 4xx, 5xx, átirányítás, beragadás — sikertelen kísérletnek számít, és visszakerül a fenti ütemezésbe.
A kézbesítésről őszintén
Ami garantált, az a kézbesítési mechanizmus: hét kísérlet nagyjából 31 óra alatt, kézi újraküldés a fiókterületről, és egy számla-végpont, amely mindig tudja a valós állapotot. Úgy építse fel a folyamatot, hogy egy soha meg nem érkező webhook semmibe se kerüljön — olvassa be a számlát a köszönőoldalon, vagy egyeztessen óránként egyszer a nyitott számlák végigfutásával. A webhook a gyors út, nem az egyetlen út.
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.
Állapot-referencia#
Minden számla- és fizetési állapot, elmagyarázva.
Számla
| Állapot | Jelentés | Mit tegyen |
|---|---|---|
| pending | Fizetésre vár. | Tartsa nyitva a rendelést. |
| paid | Teljesen kifizetve. | Adja ki az árut. |
| overpaid | Több érkezett, mint amit kértek. A többlet teljes egészében jóváírásra kerül Önnek. | Adja ki az árut; ha akarja, térítse vissza a különbözetet. |
| underpaid | Kevesebb érkezett, mint amit kértek. A számla nyitva marad, és megtartja a címét: a vevő ugyanoda töltheti fel, a paid_minor pedig megmondja, mennyi van már benn. Az élettartama hátralévő részében fizethető marad, plusz egy 24 órás türelmi idő az expires_at után. | Várja meg a feltöltést, vagy egyezzen meg a vásárlóval. Ne adja ki az árut — a számla nincs kifizetve. |
| expired | Az időablak lezárult, a türelmi idővel együtt. Még így is lehet rajta pénz: ami megérkezett, az az egyenlegén maradt, és a paid_minor megmondja, mennyi. | Ajánljon fel egy új számlát. Ne fogadjon el fizetést a régi címre: amint egy számla lejár, a cím visszakerül a készletbe, és egy nagyon késői átutalás supportos eset, nem automatikus jóváírás. Ellenőrizze a paid_minor értékét, mielőtt azt mondaná a vásárlónak, hogy semmi nem érkezett. |
| cancelled | Ön lemondta. A cím visszakerül a készletbe. | Semmi. |
Fizetés
Látható a fiókterületén; hasznos, ha egy vásárlót támogat fizetés közben.
| Állapot | Jelentés |
|---|---|
| detected | Látható a láncon, megerősítésekre vár. |
| confirmed | A hálózat megerősítette. Következik a jóváírás. |
| credited | Az egyenlegén van. Ekkor sül el a webhook. |
| review | További ellenőrzésre visszatartva — például, ha érmék érkeztek egy címre, amelyhez nem tartozik nyitott számla. |
| rejected | Nem került jóváírásra. Az ok rögzítve van. |
Amikor egy fizetés `review` állapotba kerül
Néhány befizetést azonnali jóváírás helyett további ellenőrzésre tartunk vissza: szokatlanul nagy összeg, nyitott számla nélküli címre érkező érmék, vagy az a helyzet, amikor a két lekérdezett blokklánc-forrás nem ért egyet abban, mi történt. Semmi nem vész el — a pénz megvárja a döntést, és a webhook azonnal elindul, amint megszületik; ez perceket vagy órákat is igénybe vehet. A review állapotban mutatott fizetésnél az elmaradó visszahívás normális, nem hiba. Ha egy rendelés szempontjából számít, forduljon a supporthoz, és adja meg a tx_hash értékét.
Összegek#
Kifelé normál egység, visszafelé a legkisebb.
Az összegeket az érme normál egységeiben, stringként küldd — "1.5" az másfél. Nem JSON-szám és nem a legkisebb egység.
| Eszköz | Tizedesjegyek | Te küldöd | amount_minor a válaszban |
|---|---|---|---|
| TON | 9 | "1.5" | "1500000000" |
| USDT_TON | 6 | "1.5" | "1500000" |
String, nem szám, mert a JSON-számok IEEE-754 double-ök, és egy nagy összeg nanotonban már nem fér el bennük pontosan. Több tizedesjegy, mint amennyi az érmének van, 422, és sosem a pénzed csendes kerekítése. A webhookoknál fordítva van: ott az amount, a fee és a credited egész szám a legkisebb egységben, mert azt az oldalt kód olvassa, nem ember.
// 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. 1500000nKorlátok#
Minimumok, maximumok, és sebességkorlátok.
| Korlát | Érték | Túllépés esetén |
|---|---|---|
| Minimum számla | 0.1 TON · 3 USDT | 422 |
| Maximum számla | 7000 TON · 10000 USDT | 422 |
| Számlák óránként, áruházanként | 60 | 429 |
| Egyszerre nyitott számlák | 20, minden kifizetett számlával nő, egészen 200-ig | 429 |
| Számla élettartama | 1 perc – 24 óra (alapértelmezett 2 óra) | 422 |
| API-kérések kulcsonként | 120 percenként | 429 + Retry-After |
A minimum nem bürokrácia. A díjunk százalékos, de egy fizetés fogadása fix összegbe kerül: az USDT elmozdítása egy fogadó címről azt jelenti, hogy előbb gázzal kell feltölteni — a saját zsebünkből. Néhány dollár alatt a díj nem fedezi a kezelést, és egy ilyen fizetés elfogadása azt jelentené, hogy olyan pénzt írunk jóvá Önnek, amelyet nem gazdaságos mozgatni.
A maximum nem a nagy kereskedők ellen szól — csapda az egységtévesztésre. Ha "5000000"-t küld oda, ahol "5"-öt gondolt, egyébként ötmillió dolláros számla születne: a vevő abszurd összeget lát és elmegy. Valódi rendelés soha nem éri el ezt a plafont, hibás mindig. Mindkét plafon beállítás (invoice_max_ton, invoice_max_usdt), és az Ön áruházára megemelhető — csak kérnie kell.
Az óránkénti és az egyszerre nyitott számlákra vonatkozó korlát egyaránt a címkészletet védi. Minden nyitott számla lefoglal egy fogadó címet, és egy elszabadult hurok egy oldalon máskülönben kimerítené a készletet mindenki számára. Egy új áruház egyszerre 20 számlát tarthat nyitva; a keret minden ténylegesen beszedett számla után eggyel nő, egészen 200-as plafonig. Az underpaid nyitottnak számít — még mindig fogja a címét, és a maradékra vár. Egy elhagyott számla lemondása azonnal visszaadja a címét. Az ugyanazzal az idempotency_key-vel történő újrapróbálkozások nem számítanak bele az óránkénti korlátba.
A kéréskorlát API-kulcsonként 120 percenként — másodpercenként két hívás, jóval bármilyen valós rendelési forgalom fölött. A 429 egy Retry-After fejlécet hoz másodpercben: várja ki, ahelyett hogy szoros hurokban próbálkozna újra, ami csak még kijjebb tolja az ablakot.
Hibák#
A státuszkódok, amelyekkel ténylegesen találkozni fog.
A hibák JSON formában érkeznek vissza, kétféle alakban. Amiről mi vagy a feldolgozó mag dönt, az egy {code, message} párt tesz a detail alá. Az a kéréstörzs, amely elbukik a validáción, ehelyett a mezőhibák listáját teszi oda. Nézze meg, melyiket kapta, mielőtt a detail.code értéket olvasná — és a `code` alapján ágazzon el, soha ne a `message` alapján: a megfogalmazás bármikor változhat, a kód nem.
{
"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
}
]
}| Státusz | Mikor | Mit tegyen |
|---|---|---|
| 401 | A kulcs hiányzik, hibás, vagy vissza lett vonva. | Ellenőrizze a fejlécet. Állítson ki új kulcsot, ha vissza lett vonva. |
| 404 | Nincs ilyen számla, vagy másik áruházhoz tartozik. | Ellenőrizze az azonosítót. A két eset szándékosan egyformán válaszol, hogy egy azonosítót ne lehessen kitapogatni. |
| 409 | A számla olyan állapotban van, amely ezt tiltja. | Először olvassa el a jelenlegi állapotát. |
| 422 | A kérés hibás felépítésű, vagy az összeg kívül esik a számla határain. | Az üzenet megnevezi mind a küldött értéket, mind a korlátot. |
| 429 | Túl sok számla ebben az órában, túl sok nyitott egyszerre, vagy túl sok kérés. | Várja ki a Retry-After értéket, majd próbálja újra. |
| 502 | Nem sikerült elérnünk a feldolgozó magot. | Próbálja újra ugyanazzal az idempotencia-kulccsal. |
Kódok
Az általunk eldöntött alak: {"detail": {"code": …, "message": …}}. Ezeket a kódokat adja vissza a kereskedői API.
| Kód | Státusz | Jelentés |
|---|---|---|
| invalid_api_key | 401 | A kulcs hiányzik, hibás felépítésű, ismeretlen vagy vissza lett vonva. Mind a négy eset egyformán válaszol, így egy kulcsot nem lehet kitapogatni. |
| not_found | 404 | Nincs ilyen objektum, vagy másik áruházhoz tartozik. |
| invalid_input | 422 | A kérés nem ment át a mag validációján — rossz összeg, túl sok tizedesjegy, a számla határain kívüli összeg. |
| conflict | 409 | A művelet ellentmond a jelenlegi állapotnak, például egy már nem nyitott számla lemondása. |
| too_many_requests | 429 | Sebességkorlát: óránkénti számlák, nyitott számlák vagy percenkénti kérések. A Retry-After megmondja, mennyit kell várni. |
| cbc_unreachable | 502 | Nem sikerült elérnünk a feldolgozó magot. Próbálja újra ugyanazzal az idempotency_key értékkel. |
| webhook_url_rejected | 422 | Csak kulcs mentésekor: a webhook-URL elbukott a fenti ellenőrzéseken. A detail.reason megnevezi, melyik szabályon — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials és így tovább. |
Az 502 nem jelenti azt, hogy a számla nem jött létre — előfordulhat, hogy a kérés átment, de a válasz elveszett visszafelé. Próbálja újra ugyanazzal az idempotency_key-vel, és vagy a meglévő számlát kapja, vagy egy újat — de sosem kettőt.
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.
Visszatérítések#
Hogyan térítsen vissza pénzt egy vásárlónak.
A visszatérítés a támogatáson keresztül történik, nem API-hívással. A visszatérítés új átutalás egy olyan címre, amelyet egy ember adott meg, és az a fizetési szolgáltató, amely API-hívásra automatikusan visszaküld pénzt, olyan szolgáltató, amelyet rá lehet venni, hogy egy támadó címére küldjön. Ezért szándékosan kézi.
Egy vevő visszatérítéséhez nyisson support-jegyet a fiókterületéről az invoice_id vagy a tx_hash értékkel, az összeggel és a célcímmel. Egy operátor ellenőrzi a fizetést, kiviszi a pénzt az egyenlegéből, és ugyanabban a jegyben válaszol. Számítson rá, hogy ez egy munkanapot vesz igénybe, nem egy percet.
Két következmény, amelyre érdemes tervezni. A túlfizetés teljes egészében Önnek íródik jóvá — mi semmit nem tartunk meg belőle —, így a különbözet visszaadása a túl sokat küldő vevőnek az Ön döntése, és ugyanezen az úton megy. Illetve egy underpaid számla nem visszatérítési eset, amíg nyitva van: a pénz az egyenlegén van, a címet továbbra is figyeljük, a vevő pedig egyszerűen feltöltheti. Csak a türelmi idő után, amikor a számla pénzzel a hátán expired állapotba kerül, van egyáltalán döntenivaló.
Tesztelés#
Hogyan tesztelje az integrációt indulás előtt.
Az itteni kulcsok élesek: minden kiadott kulcs sk_live_ kulcs, amely az éles mag és a TON mainnet ellen dolgozik. Külön tesztkörnyezet nincs, aminek megvan az előnye: pontosan azt az utat járja végig, amelyen a valódi rendelései is menni fognak.
Tesztelje tehát úgy, ahogy bármit tesztelne, ami valódi pénzhez ér: kis összegekkel. Hozzon létre egy minimumra szóló számlát (0.1 TON vagy 3 USDT), fizesse ki a saját tárcájából, és nézze végig az egész utat — a fizetési oldalt, a webhookot, az aláírás-ellenőrzést, a rendelése átbillenését fizetettre. A díj érvényes, és az érmék valóban mozognak.
Amit költés nélkül végigpróbálhat: számla létrehozása és olvasása, lemondása, a 422 hibás formátumú összegre, a 401 rossz kulcsra, és a saját aláírás-ellenőrzése — írjon alá egy mintatörzset a titkával, és adja oda a saját kezelőjének. Ami valóban valódi fizetést igényel, az csak az utolsó lépés: egy tényleges payment.credited webhook.
Úgy tervezze az integrációt, hogy ne függjön sandboxtól vagy szimulált fizetéstől: az éles út gyorsabban — és hitelesebben — ellenőrizhető.
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.
Indulás előtti ellenőrzőlista#
Tíz dolog indulás előtt.
- A kulcs kizárólag szerveroldalon van, soha a böngésző JavaScriptjében.
- A webhook aláírása a
"{timestamp}.{raw_body}"alapján, konstans időben van ellenőrizve. - Az öt percnél régebbi kézbesítéseket elutasítja, és a szerver órája NTP-n van.
- Egy ismétlődő
X-Paysell-Event-Idmásodszor semmit sem csinál. - A webhook tíz másodpercen belül 2xx választ ad; a lassú munka utána történik.
- A webhook-URL egy https:// domain a 443-as porton, átirányítás nélkül.
- Egy elmaradt webhook túlélhető: a számla-végpontot a köszönőoldal vagy egy egyeztető végigfutás beolvassa.
- Az
idempotency_keyrendelésenként egyszer generálódik, és újrapróbálkozáskor újrafelhasználásra kerül. - Az összegek stringként, normál egységben mennek ki; a webhook számait a legkisebb egységként kell olvasni.
- A cím pontosan úgy jelenik meg, ahogy visszakapták, módosítás nélkül.
- Az
overpaidésunderpaidkezelve van, nem csak apaid; azexpiredis hordozhatpaid_minorértéket. - Az áru
status: paidvagyoverpaidesetén megy ki, sosem pusztán a hívás megérkezésére. - A
429kezelése aRetry-Afterkivárása, nem az azonnali újrapróbálkozás. - Az egyenlegeket tőlünk olvassák, nem külön nyilvántartva, mint igazságforrás.
Valami nem világos?
Ha ez az oldal nem válaszolta meg a kérdését, az hiányosság a dokumentációban, és érdemes jelezni nekünk. Írjon a fiókterületéről, és mi kijavítjuk az oldalt, nem csak a választ.