Paysell

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.

Az egyenlegek nálunk vannak, és ezek az egyetlen hiteles forrás. Jelenítse meg őket, de soha ne tartson egy második, mérvadónak tekintett másolatot — két számláló előbb-utóbb mindig eltér egymástól, és akkor senki sem tudja, melyik a helyes.

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. 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. 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. 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_url címre, és az oldalt mi kezeljük Ön helyett — összeg, cím, QR-kód, visszaszámláló, élő állapot.

  4. 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. 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. 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_id azonosí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.

example
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 USDT

A 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.

Az érmék elmozdítása egy fogadó címről hálózati gázba kerül, és ezt mi fizetjük — ez a rész soha nem érinti az egyenlegét. A saját címére történő kifizetés más kérdés: annak saját díja van, amelyet a kért összegből vonunk le, a pontos számok pedig a díjszabásban találhatók.

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. 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. 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. 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. 4

    Küldje a vásárlót a payment_url címre

    Ez 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. 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.credited eseményt POST-olunk a szerverére. Ellenőrizze az aláírást, majd jelölje a rendelést fizetettnek — de csak akkor, ha a data.status értéke paid vagy overpaid. 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

Hitelesítés#

Az API-kulcsa, és hogyan használjuk.

Minden kérés hordozza az Ön kulcsát az Authorization fejlécben:

http
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxA

Az 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.

Ez a kulcs az Ön nevében hoz létre számlákat. Tartsa szerveroldalon. Bármi, ami a böngésző JavaScriptjében van, nyilvános, bármilyen jól is legyen elrejtve.

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.

EndpointMethodAuthWhat it does
/invoicesPOSTAPI keyOpen an invoice and get a payment link. Details.
/invoices/{invoice_id}GETAPI keyRead one invoice's current state. Details.
/invoices/{invoice_id}/cancelPOSTAPI keyClose an invoice that is still open and free its address. Details.
/public/invoices/{invoice_id}GETnoneWhat 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

POST/api/merchant/v1/invoices

Kérés törzse

MezőTípusKötelezőLeírás
assetstringigenTON vagy USDT_TON.
amountstringigenAz é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_idstringnemAz Ön saját hivatkozása, legfeljebb 200 karakter. Minden webhookban visszaérkezik — így párosítja a fizetést a rendeléshez.
descriptionstringnemLegfeljebb 1000 karakter. A vásárlónak jelenik meg a fizetési oldalon.
ttl_minutesnumbernemMeddig marad fizethető a számla, percben. 1–1440; ha kihagyja, az alapértelmezés érvényes — ma 2 óra.
idempotency_keystringnemLegfeljebb 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

json
{
  "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_idTárolja el a rendeléséhez. Ez azonosítja a fizetést mindenhol máshol.
payment_urlIrányítsa ide a vásárlót. Semmi mást nem kell felépíteni.
addressCsak 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.
amountAz összeg normál egységben, pontosan úgy, ahogy küldted. Ezt jelenítsd meg.
amount_minorUgyanaz az összeg egész számként, a legkisebb egységben. Ezzel számolj.
expires_atMutasson egy visszaszámlálót. Ennek elteltével a cím megszűnik figyelve lenni ehhez a számlához.
statusItt mindig pending. A valódi változások webhookon keresztül érkeznek.
Ha a saját oldalát jeleníti meg, a címet pontosan úgy írja ki, ahogy visszakapta. Nem visszapattanó (non-bounceable) formában van (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}

GET/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

POST/api/merchant/v1/invoices/{invoice_id}/cancel

Lezá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ényMikorMit tartalmaz a törzs
payment.creditedAz á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.rejectedEgy 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

json
{
  "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…"
  }
}
json
{
  "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_idEsemé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_idAz Ön hivatkozása. Ez alapján keresse meg a rendelését.
data.amountAmennyit 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.feeAmennyit levontunk, a legkisebb egységben.
data.creditedAmennyi az egyenlegére került: amount − fee, a legkisebb egységben.
data.paid_minorAz 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.assetAz érme, amely ténylegesen megérkezett. Nem feltétlenül az, amit a számla kért.
data.asset_mismatchCsak 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_assetAz asset_mismatch mellett érkezik: az az érme, amelyet a számla valójában kér.
data.statusA számla mostani állapota: pending, underpaid, paid, overpaid vagy expired. Hasonlítsa össze azzal, amire számított.
data.tx_hashA láncon lévő tranzakció, az Ön nyilvántartásához és a supporthoz.

Fejlécek minden kézbesítésben

http
X-Paysell-Event:      payment.credited
X-Paysell-Event-Id:   99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp:  1789000000
X-Paysell-Signature:  sha256=6f1c0e6a…
FejlécJelentés
X-Paysell-EventAz esemény típusa: payment.credited vagy payment.rejected.
X-Paysell-Event-IdEseményenként egyedi. Ez az az érték, amire deduplikálni kell.
X-Paysell-TimestampMikor írtuk alá, unix másodpercben. Része az aláírt sztringnek.
X-Paysell-Signaturesha256=, 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:

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:

javascript
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, .lan vagy .test né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 Location fejlécben áll.
Az ellenőrzés szándékosan kétszer történik — egyszer az URL mentésekor, hogy egy elgépelés azonnal választ kapjon, ne néma kézbesítetlenséget, és egyszer minden küldés előtt, mert egy domain tulajdonosa bármelyik pillanatban belső címre irányíthatja át. Ha a végpontja költözik, előbb a kulcsot frissítse: az elutasított URL semmit nem kézbesít, és nem is sorol be semmit.

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.

javascript
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.

python
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 seconds

What 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`. underpaid means 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

ÁllapotJelentésMit tegyen
pendingFizetésre vár.Tartsa nyitva a rendelést.
paidTeljesen kifizetve.Adja ki az árut.
overpaidTö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.
underpaidKevesebb é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.
expiredAz 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.

ÁllapotJelentés
detectedLátható a láncon, megerősítésekre vár.
confirmedA hálózat megerősítette. Következik a jóváírás.
creditedAz egyenlegén van. Ekkor sül el a webhook.
reviewTovábbi ellenőrzésre visszatartva — például, ha érmék érkeztek egy címre, amelyhez nem tartozik nyitott számla.
rejectedNem 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özTizedesjegyekTe küldödamount_minor a válaszban
TON9"1.5""1500000000"
USDT_TON6"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.

javascript
// 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. 1500000n

Korlátok#

Minimumok, maximumok, és sebességkorlátok.

KorlátÉrtékTúllépés esetén
Minimum számla0.1 TON · 3 USDT422
Maximum számla7000 TON · 10000 USDT422
Számlák óránként, áruházanként60429
Egyszerre nyitott számlák20, minden kifizetett számlával nő, egészen 200-ig429
Számla élettartama1 perc – 24 óra (alapértelmezett 2 óra)422
API-kérések kulcsonként120 percenként429 + 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.

json
{
  "detail": {
    "code": "invalid_input",
    "message": "invoice amount below the minimum: 0.010000 USDT_TON, minimum 3.000000 USDT_TON"
  }
}
json
{
  "detail": [
    {
      "type": "string_type",
      "loc": ["body", "amount"],
      "msg": "Input should be a valid string",
      "input": 5
    }
  ]
}
StátuszMikorMit tegyen
401A kulcs hiányzik, hibás, vagy vissza lett vonva.Ellenőrizze a fejlécet. Állítson ki új kulcsot, ha vissza lett vonva.
404Nincs 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.
409A számla olyan állapotban van, amely ezt tiltja.Először olvassa el a jelenlegi állapotát.
422A 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.
429Tú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.
502Nem 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ódStátuszJelentés
invalid_api_key401A 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_found404Nincs ilyen objektum, vagy másik áruházhoz tartozik.
invalid_input422A 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.
conflict409A művelet ellentmond a jelenlegi állapotnak, például egy már nem nyitott számla lemondása.
too_many_requests429Sebessé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_unreachable502Nem sikerült elérnünk a feldolgozó magot. Próbálja újra ugyanazzal az idempotency_key értékkel.
webhook_url_rejected422Csak 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 is underpaid.
  • 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

InvoiceWhat 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 a 422. 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. underpaid is not paid, and a deposit in a coin the invoice did not ask for never makes it paid either. Release the goods on paid or overpaid, 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_id will 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-Key header

    This API reads idempotency_key from the request body; the header is not read at all. A retry without the field opens a second invoice for the same order.

  • Assuming detail is always an object

    It 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 reading detail.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ő.

Tekintse az első éles rendelését az igazi tesztnek: válasszon kis összeget, tartsa nyitva a számlát a vezérlőpulton, és ellenőrizze a fizetés sorát és a webhook állapotát, mielőtt valódi vásárlókat irányítana ide.

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

FileWhat it isUse it for
/llms-full.txtThe 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.txtA 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.jsonOpenAPI 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.

prompt
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.txt link. One fetch, no setup.
  • A chat window — ChatGPT, Claude, Gemini: paste the contents of /llms-full.txt into 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. Its servers entry 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-Id má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_key rendelé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 és underpaid kezelve van, nem csak a paid; az expired is hordozhat paid_minor értéket.
  • Az áru status: paid vagy overpaid esetén megy ki, sosem pusztán a hívás megérkezésére.
  • A 429 kezelése a Retry-After kivá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.