Kubali malipo ya crypto
Paysell inarekebisha TON na USDT kwenye mtandao wa TON. Unaunda ankara, tunakupa kiungo, na unapata callback iliyosainiwa mara pesa inapothibitishwa kwenye blockchain na kuwekwa kwenye salio lako.
Muhtasari#
Anachofanya Paysell, na asichofanya.
Paysell ni processor ya malipo, si pochi. Hushughulikii funguo za faragha, huangalii blockchain, wala huamui ni lini muamala umekamilika kabisa — hicho ni sehemu tunayoibeba sisi.
Kila ankara ina anwani yake ya kupokea. Mnunuzi anapoilipia, tunasubiri mtandao uthibitishe uhamishaji, tunakata ada yetu, na kuweka salio lililobaki kwenye akaunti yako. Unatoa fedha kwenye anwani yoyote unayotaka.
Jinsi malipo yanavyofanya kazi#
Hatua sita, nyingi zikiwa zetu.
Hatua sita, nyingi zikiwa zetu:
- 1
Mteja wako anabofya lipa
Seva yako inaita API yetu ikiwa na kiasi na rejeleo lako la oda.
- 2
Tunatoa anwani
Anwani mpya ya kupokea inachukuliwa kutoka kwenye hazina iliyotayarishwa mapema na kufungwa kwa ankara hii. Anwani moja ni ya ankara moja tu iliyo wazi, na hivyo ndivyo malipo yanavyolinganishwa nayo.
- 3
Mteja anatuma sarafu
Anaskani msimbo wa QR au kunakili anwani. Watumie kwenye
payment_urltunayorudisha na ukurasa unashughulikiwa kwa ajili yako — kiasi, anwani, QR, kihesabu muda, hali ya moja kwa moja. - 4
Tunagundua uhamishaji
Vyanzo viwili huru vya data ya blockchain vinaulizwa, na majibu yao yanalinganishwa. Vikiwa tofauti, tunasimama badala ya kuchagua jibu rahisi zaidi.
- 5
Tunasubiri uthibitisho wa mwisho
Kujumuishwa kwenye masterchain pamoja na vizuizi vitatu zaidi. Takriban sekunde kumi na tano — malipo yanayoonekana yamekamilika lakini baadaye yanatoweka yangekuwa hasara yako, kwa hivyo hatuchukui hatari hiyo.
- 6
Yamewekwa, na umeambiwa
Ada inakatwa, kilichobaki kinaingia kwenye salio lako, na webhook iliyosainiwa inatumwa kwa seva yako ikiwa na
order_idyako.
Takriban dakika moja kutoka malipo hadi callback: takriban sekunde kumi na tano za uthibitisho wa mtandao, na sehemu iliyobaki ni ukaguzi wetu wa anwani zinazofuatiliwa.
Pesa inakoenda#
Ada, na inavyohesabiwa.
Ada ni 0.2%, imewekwa kwa duka lako wakati wa usajili. Ikiwa kiwango cha kawaida kitabadilika baadaye, chako hakibadiliki — kimeandikwa kwenye kila ankara kama nambari, si kama rejeleo la mpangilio.
Ada inatozwa kwa kile kilichofika kwa hakika, si kile ankara ilichoomba. Toa ankara ya USDT 5 na upokee 20, ada inahesabiwa kwa 20. Ukilipwa pungufu, inahesabiwa kwa kile kilichoingia.
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 USDTMalipo ya ziada yanawekwa kikamilifu — hatubaki na tofauti. Malipo pungufu yanaacha ankara wazi ili mnunuzi aweze kuongeza kwenye anwani ile ile.
Anza haraka#
Dakika tano hadi ankara yako ya kwanza.
Hatua tano. Mbili ni mibofyo katika eneo la akaunti yako, moja ni ombi moja tu kutoka seva yako, na mbili za mwisho hutokea zenyewe.
- 1
Unda duka
Katika eneo la akaunti yako. Linaanza kukubali malipo mara moja — hakuna kusubiri ukaguzi. Uthibitishaji unafanyika kimya nyuma ya pazia na unazuia tu utoaji wa fedha, si malipo yanayoingia.
- 2
Toa ufunguo wa API
Duka lako → Funguo za API → Ufunguo mpya. Ufunguo na siri ya webhook huonyeshwa mara moja tu na kamwe tena. Uhifadhi kama unavyohifadhi neno la siri la hifadhidata, na kamwe usizipeleke kwenye kivinjari.
- 3
Unda ankara
Ombi moja kutoka seva yako, kiungo kimoja kinachorudi. Vipande vinne hapa chini vyote hutuma kitu kile kile.
- 4
Mpeleke mnunuzi kwenye
payment_urlHuo ndio ukurasa mzima wa malipo — kiasi, anwani, msimbo wa QR, kihesabu cha muda, hali ya papo hapo — na hakuna cha kujenga. Ona Ukurasa wa malipo kwa kile mnunuzi anachokiona hasa.
- 5
Subiri webhook
Pesa zinapothibitishwa kwenye mnyororo na kuingizwa, tunatuma POST yenye tukio lililosainiwa
payment.creditedkwenye seva yako. Thibitisha sahihi, kisha weka oda kuwa imelipwa — lakini pale tudata.statusikiwapaidauoverpaid. Ona Webhooks.
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"
}'Mpeleke mnunuzi kwenye payment_url iliyo kwenye jibu. Umemaliza — sehemu iliyobaki itafika kama 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.
Uthibitishaji#
Ufunguo wako wa API, na jinsi unavyotumika.
Kila ombi hubeba ufunguo wako kwenye kichwa cha Authorization:
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxAKila ufunguo unaotolewa hapa huanza na sk_live_. Kiambishi sk_test_ kipo tu kwenye usakinishaji ulioelekezwa kwenye mtandao wa majaribio, na usakinishaji kama huo hautolewi — ona Majaribio. Tunahifadhi hashi ya njia moja, si ufunguo wenyewe, hivyo hakuna, hata sisi, anayeweza kukuonyesha tena. Umeupoteza? Toa mpya na ubatilishe wa zamani.
Duka linatokana na ufunguo, ndiyo maana hakuna ombi linalochukua kitambulisho cha duka. Ufunguo unaweza tu kutenda kwenye duka lake mwenyewe.
Njia inabeba toleo: /api/merchant/v1/…. Ndani ya toleo tunaongeza sehemu tu — hakuna kinachopewa jina jipya wala kubadilisha maana kimyakimya. Badiliko ambalo lingevunja msimbo wako hupata kiambishi kipya, /v2, na /v1 huendelea kufanya kazi kwa muda uliotangazwa.
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.
Unda ankara#
POST /api/merchant/v1/invoices
/api/merchant/v1/invoicesMwili wa ombi
| Sehemu | Aina | Inahitajika | Maelezo |
|---|---|---|---|
| asset | string | ndiyo | Ima TON au USDT_TON. |
| amount | string | ndiyo | Vipimo vya kawaida vya sarafu, kama string: "5" ni 5 USDT. Si desimali nyingi kuliko sarafu ilivyo nazo. Ona Kiasi. |
| order_id | string | hapana | Rejeleo lako mwenyewe, hadi herufi 200. Linarudi kwenye kila webhook — hivi ndivyo unavyolinganisha malipo na oda. |
| description | string | hapana | Hadi herufi 1000. Inaonyeshwa kwa mnunuzi kwenye ukurasa wa malipo. |
| ttl_minutes | number | hapana | Ankara inabaki kulipika kwa dakika ngapi. 1–1440; usipoiweka, chaguo-msingi hutumika — saa 2 leo. |
| idempotency_key | string | hapana | Hadi herufi 200. Tuma thamani ile ile unapojaribu tena na utapata ankara ile ile badala ya ya pili. Ni uga wa mwili wa ombi, si kichwa Idempotency-Key — kichwa hicho hakisomwi hapa. |
Jibu · 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"
}Kuiweka kwenye oda yako
| Sehemu | Cha kufanya nayo |
|---|---|
| invoice_id | Ihifadhi dhidi ya oda yako. Ndicho kinachotambulisha malipo kila mahali pengine. |
| payment_url | Mpeleke mnunuzi hapa. Hakuna kingine cha kujenga. |
| address | Tu ikiwa unatengeneza ukurasa wako wa malipo. Ionyeshe kama ilivyotolewa hasa — angalia onyo hapa chini. |
| amount | Kiasi katika vipimo vya kawaida, kama ulivyokituma hasa. Onyesha hiki. |
| amount_minor | Kiasi kile kile kama namba kamili katika kipimo kidogo zaidi. Hesabu kwa hiki. |
| expires_at | Onyesha kihesabu muda. Baada ya kupita, anwani haifuatiliwi tena kwa ankara hii. |
| status | Hapa daima pending. Mabadiliko halisi yanakuja kwa webhook. |
UQ… kwenye mtandao mkuu, 0Q… kwenye mtandao wa majaribio). Ukiibadilisha, kuipamba, au kuibadilisha na usimbaji mwingine wa anwani ile ile, sarafu zilizotumwa kwenye pochi ambayo haijasambazwa bado zitarudi kwa mtumaji.Soma ankara#
GET /api/merchant/v1/invoices/{invoice_id}
/api/merchant/v1/invoices/{invoice_id}Umbo lile lile kama hapo juu, na status, paid na paid_minor zikionyesha hali ya sasa: paid ni kiasi kilichofika katika vipimo vya kawaida, paid_minor ni kiasi kile kile kama namba nzima katika kipimo kidogo zaidi. Inafaa kama hifadhi mbadala wakati webhook imekosekana, au kwenye ukurasa wa shukrani.
Iulize mara chache tu kila baada ya sekunde chache, na uzichukulie webhooks kama kituo kikuu. Ankara zinazomilikiwa na duka lingine hujibu 404 — si 403, ili kitambulisho kisiweze kuchunguzwa kama kipo.
Ghairi ankara#
POST /api/merchant/v1/invoices/{invoice_id}/cancel
/api/merchant/v1/invoices/{invoice_id}/cancelInafunga ankara ambayo bado iko wazi — pending au underpaid — na kuachilia anwani yake. Itumie mteja anapoacha malipo katikati: anwani ni rasilimali chache, na kuzirudisha kunahifadhi hazina ikiwa na afya.
Ankara ambayo tayari haiko wazi hujibu 409. Kughairi ankara ya underpaid hakumrudishii mtu yeyote sarafu: pesa zilizoingizwa zinabaki kwenye salio lako, na kinachofungwa ni kupokea nyongeza pekee.
Webhooks#
Kinachofika, na jinsi ya kukithibitisha.
Weka URL ya webhook unapotengeneza ufunguo. Tunatuma POST hapo malipo yanapoingizwa — na pia amana iliyozuiliwa kwa ukaguzi wa ziada inapokataliwa. Kila utumaji umesainiwa, na tunaendelea kujaribu kwa takribani siku moja na nusu mpaka ujibu 2xx. Toa bidhaa kwa status: paid au overpaid, si kwa sababu tu wito umefika.
Matukio
| Tukio | Lini | Kilicho ndani ya mwili |
|---|---|---|
| payment.credited | Uhamisho umethibitishwa kwenye mnyororo, ada yetu imekatwa, na kilichobaki kipo kwenye salio lako. | Sehemu zilizoorodheshwa hapa chini. |
| payment.rejected | Amana iliyozuiliwa kwa ukaguzi wa ziada (ona Rejeleo la hali) imekataliwa. Pesa hazitafika kwenye salio lako. | invoice_id, order_id, asset, amount, tx_hash na reason. Usitoe bidhaa; ikiwa ankara ilikuwa tayari paid kutokana na uhamisho wa awali, tukio hili linahusu amana ya ziada, si malipo yale. |
Kinachofika
{
"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"
}
}Ulinganisho wa sehemu
| Sehemu | Maana |
|---|---|
| event_id | Ya kipekee kwa kila tukio; pia iko kwenye kichwa cha X-Paysell-Event-Id. Ihifadhi na upuuze marudio — angalia hapa chini. |
| data.order_id | Rejeleo lako. Tafuta oda yako kwa hii. |
| data.amount | Kiasi alichotuma mnunuzi katika uhamisho huu, katika kipimo kidogo zaidi — tofauti na API inayopokea vipimo vya kawaida. |
| data.fee | Kiasi tulichochukua, katika kipimo kidogo zaidi. |
| data.credited | Kilichoingia kwenye salio lako: amount − fee, katika kipimo kidogo zaidi. |
| data.paid_minor | Jumla iliyopokelewa kwenye ankara hii hadi sasa, katika kipimo kidogo zaidi. Ndilo eneo muhimu kwenye underpaid: hali inasema kimefika kidogo, hii inasema kidogo kiasi gani. |
| data.asset | Sarafu iliyofika kweli. Si lazima iwe sarafu ambayo ankara iliomba. |
| data.asset_mismatch | Ipo, na ni true, pale tu sarafu iliyofika si sarafu ya ankara. Pesa zinaingizwa kwako, lakini ankara inabaki haijalipwa na status haitakuwa paid kamwe. |
| data.invoice_asset | Huja pamoja na asset_mismatch: sarafu ambayo ankara inaomba kweli. |
| data.status | Hali ya ankara kwa sasa: pending, underpaid, paid, overpaid au expired. Linganisha na ulichotarajia. |
| data.tx_hash | Muamala kwenye blockchain, kwa kumbukumbu zako na msaada. |
Vichwa vinavyokuja kwenye kila utoaji
X-Paysell-Event: payment.credited
X-Paysell-Event-Id: 99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp: 1789000000
X-Paysell-Signature: sha256=6f1c0e6a…| Kichwa | Maana |
|---|---|
| X-Paysell-Event | Aina ya tukio: payment.credited au payment.rejected. |
| X-Paysell-Event-Id | Ya kipekee kwa kila tukio. Hii ndiyo thamani ya kuondolea marudio. |
| X-Paysell-Timestamp | Wakati tulipotia sahihi, kwa sekunde za unix. Ni sehemu ya mfuatano uliosainiwa. |
| X-Paysell-Signature | sha256= ikifuatiwa na HMAC kwa hex. Angalia hapa chini. |
Kuthibitisha sahihi
Kila ombi limesainiwa kwa siri ya webhook iliyoonyeshwa mara moja tu ulipotengeneza ufunguo. Sahihi ni HMAC-SHA256(secret, "{timestamp}.{raw_body}") — muhuri wa muda kutoka X-Paysell-Timestamp, nukta halisi, kisha baiti za mwili. Ikague kabla ya kutenda: bila hii, yeyote anayejua URL yako anaweza kukukabidhi oda iliyolipwa.
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)
}Saini baiti ghafi za mwili, kama zilivyopokelewa hasa. Ukichambua JSON na kuisimba upya baiti hubadilika — mpangilio wa funguo, nafasi — na sahihi haitalingana. Linganisha kwa muda usiobadilika (hmac.compare_digest, crypto.timingSafeEqual): == ya kawaida hurudi haraka zaidi baiti ya kwanza ikiwa si sahihi, na tofauti hiyo inatosha kubahatisha sahihi baiti moja baada ya nyingine.
Dirisha la muhuri wa muda
Kataa chochote ambacho muhuri wake wa muda uko zaidi ya dakika tano kutoka saa yako mwenyewe, upande wowote. Muhuri wa muda uko ndani ya mfuatano uliosainiwa hasa ili usiweze kubadilishwa bila kuvunja sahihi; dirisha ndilo linalogeuza hilo kuwa ulinzi. Bila hilo, ombi lililonaswa mara moja hubaki halali milele na linaweza kurudiwa wakati wowote — sahihi peke yake haiishi muda kamwe. Weka saa ya seva yako kwenye NTP, vinginevyo ukaguzi huu utaanza kukataa utoaji halali.
Marudio
Tukio lile lile linaweza kufika zaidi ya mara moja. Hilo si hitilafu: tunajaribu tena mpaka ujibu 2xx, na utoaji uliofanikiwa lakini ambao jibu lake halikutufikia hutumwa tena. Rekodi X-Paysell-Event-Id (pia huja kama event_id ndani ya mwili) na hakikisha kufika kwa pili hakufanyi chochote.
Marudio
Jaribio la kwanza hutoka mara tu malipo yanapoingizwa. Likishindwa — muda kwisha, muunganisho kukataliwa, hitilafu ya TLS, uelekezaji upya, au hali yoyote isiyo 2xx — tunajaribu tena kwa ratiba maalum:
dakika 1 → dakika 5 → dakika 15 → saa 1 → saa 6 → saa 24Majaribio saba kwa jumla, yakisambaa kwa takribani saa 31. Ya mwanzo yako karibu karibu kwa sababu sababu ya kawaida ni mpokeaji aliyekuwa akianza upya na tayari amerudi; ya mwisho yako mbalimbali kwa sababu kupiga seva iliyokufa kwa siku nzima hakumsaidii yeyote.
Baada ya jaribio la mwisho utoaji hutiwa alama dropped na tunasimama wenyewe. Haujapotea: safu ya malipo katika eneo la akaunti yako inaonyesha hali, idadi ya majaribio na aina ya hitilafu, ikiwa na kitufe cha Tuma tena kinachoanzisha mzunguko mpya wa majaribio yote saba. Njia yako nyingine ni GET /api/merchant/v1/invoices/{invoice_id} — ankara daima inajua hali yake yenyewe.
URL ya webhook inapaswa kuonekanaje
URL hukaguliwa unapoihifadhi, na tena kabla ya kila utoaji mmoja mmoja. URL isiyopita ukaguzi hujibiwa kwa 422 na code: "webhook_url_rejected" wakati wa kuhifadhi, na hutia utoaji alama failed — bila marudio — ikiwa itaanza kushindwa baadaye. Kanuni ni hizi:
- `https://` pekee, na bandari 443. Webhook inabeba maelezo ya malipo; kwenye http tupu yanasomeka na yeyote aliye njiani.
- Jina la kikoa, si anwani ya IP. Unahitaji cheti hata hivyo, na vyeti havitolewi kwa IP tupu.
- Hakuna `localhost`, wala jina la
.local,.internal,.corp,.lanau.test— seva zetu haziwezi kufikia mtandao wako, na jina linalotatuliwa ndani ya wetu ndilo hasa tusilopaswa kupiga. - Hakuna vitambulisho ndani ya URL (
https://user:pass@…). Weka tokeni yako mwenyewe kwenye njia au kigezo cha hoja ukihitaji. - Kila anwani ambayo jina hutatuliwa kuwa lazima iwe ya umma — A na AAAA zote mbili. Safu za binafsi, loopback, link-local na CGNAT zinakataliwa, na ukaguzi hurudiwa kabla ya kila utoaji, hivyo kuelekeza rekodi kwenye
127.0.0.1baadaye pia hakufanyi kazi. - Uelekezaji upya ni kushindwa, si hatua. Hatuufuati: anwani uliyotupa ilikaguliwa, ile iliyo kwenye kichwa cha
Locationhaikukaguliwa.
Jibu haraka
2xx yoyote inatosha, ndani ya sekunde kumi — huo ndio muda wetu wote, pamoja na muunganisho. Jibu kwanza, fanya kazi ya polepole baadaye; endpoint inayosubiri hifadhidata yake yenyewe kabla ya kujibu hatimaye itarekodiwa kama muda kwisha na kurudiwa, nawe utachakata tukio lile lile mara mbili. Kingine chochote — 4xx, 5xx, uelekezaji upya, kukwama — huhesabika kama jaribio lililoshindwa na hurudi kwenye ratiba iliyo hapo juu.
Utoaji, kwa uwazi
Kinachohakikishwa ni utaratibu wa utoaji: majaribio saba kwa takribani saa 31, kutuma upya kwa mkono kutoka eneo la akaunti yako, na endpoint ya ankara inayojua daima hali halisi. Jenga mtiririko wako ili webhook isiyofika kamwe isikugharimu chochote — soma ankara kwenye ukurasa wako wa shukrani, au linganisha ankara wazi mara moja kwa saa. Webhook ni njia ya haraka, si njia pekee.
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.
Rejeleo la hali#
Kila hali ya ankara na malipo, imefafanuliwa.
Ankara
| Hali | Maana | Cha kufanya |
|---|---|---|
| pending | Inasubiri malipo. | Iache oda wazi. |
| paid | Imelipwa kikamilifu. | Toa bidhaa. |
| overpaid | Kimefika zaidi ya kilichoombwa. Ziada inawekwa kwako kikamilifu. | Toa bidhaa; rudisha tofauti ukipenda. |
| underpaid | Kimefika kidogo kuliko kilichoombwa. Ankara inabaki wazi na inashikilia anwani yake: mnunuzi anaweza kuongeza mahali pale pale, na paid_minor inasema kiasi gani tayari kimeingia. Inabaki inalipika kwa muda wake wote uliobaki pamoja na kipindi cha neema cha saa 24 baada ya expires_at. | Subiri nyongeza, au patana na mteja. Usitoe bidhaa — ankara haijalipwa. |
| expired | Dirisha limefungwa, pamoja na kipindi cha neema. Bado inaweza kuwa na pesa: chochote kilichofika kilibaki kwenye salio lako, na paid_minor inasema kiasi gani. | Toa ankara mpya. Usikubali malipo kwenye anwani ya zamani: ankara inapoisha muda, anwani hurudi kwenye hazina, na uhamisho unaochelewa sana ni suala la huduma kwa wateja, si uingizaji wa kiotomatiki. Angalia paid_minor kabla ya kumwambia mteja hakuna kilichopokelewa. |
| cancelled | Umeghairi wewe. Anwani inarudishwa kwenye hazina. | Hakuna. |
Malipo
Yanaonekana kwenye eneo la akaunti yako; yanafaa unaposaidia mteja katikati ya malipo.
| Hali | Maana |
|---|---|
| detected | Imeonekana kwenye chain, inasubiri uthibitisho. |
| confirmed | Mtandao umeithibitisha. Kuweka kunafuata. |
| credited | Kwenye salio lako. Huu ndio wakati webhook inapiga. |
| review | Imezuiliwa kwa ukaguzi wa ziada — kwa mfano, sarafu zinazofika kwenye anwani isiyo na ankara wazi. |
| rejected | Haikuwekwa. Sababu imerekodiwa. |
Malipo yanapoenda kwenye `review`
Baadhi ya amana huzuiliwa kwa ukaguzi wa ziada badala ya kuingizwa mara moja: kiasi kikubwa kisicho cha kawaida, sarafu zinazofika kwenye anwani isiyo na ankara wazi, au vyanzo viwili vya blockchain tunavyouliza kutokubaliana kuhusu kilichotokea. Hakuna kinachopotea — pesa zinasubiri uamuzi, na webhook hupiga mara tu uamuzi unapotolewa, jambo linaloweza kuchukua dakika au saa. Chukulia kukosekana kwa wito kwenye malipo yanayoonyeshwa kama review kuwa jambo la kawaida, si kushindwa. Ikiwa ni muhimu kwa oda fulani, uliza msaada na utaje tx_hash.
Kiasi#
Kutoka vipimo vya kawaida, kurudi vipimo vidogo zaidi.
Tuma kiasi katika vipimo vya kawaida vya sarafu, kama string — "1.5" ni moja na nusu. Si namba ya JSON wala si kipimo kidogo zaidi.
| Sarafu | Desimali | Unatuma | amount_minor katika jibu |
|---|---|---|---|
| TON | 9 | "1.5" | "1500000000" |
| USDT_TON | 6 | "1.5" | "1500000" |
String badala ya namba, kwa sababu namba za JSON ni double za IEEE-754 na kiasi kikubwa kwa nanoton huacha kutoshea ndani yake kwa usahihi. Desimali nyingi kuliko sarafu ilivyo nazo ni 422, si kuzungusha pesa zako kimyakimya. Webhook huenda kinyume: huko amount, fee na credited ni namba kamili katika kipimo kidogo zaidi, kwa sababu upande huo unasomwa na msimbo, si na mtu.
// 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. 1500000nMipaka#
Kiwango cha chini, cha juu, na mipaka ya kasi.
| Kikomo | Thamani | Ikikiukwa |
|---|---|---|
| Ankara ya chini kabisa | 0.1 TON · 3 USDT | 422 |
| Ankara ya juu kabisa | 7000 TON · 10000 USDT | 422 |
| Ankara kwa saa, kwa kila duka | 60 | 429 |
| Ankara wazi kwa wakati mmoja | 20, zikiongezeka kwa kila ankara iliyolipwa, hadi 200 | 429 |
| Muda wa uhai wa ankara | dakika 1 – saa 24 (chaguo-msingi saa 2) | 422 |
| Maombi ya API kwa kila ufunguo | 120 kwa dakika | 429 + Retry-After |
Kiwango cha chini si urasimu. Ada yetu ni asilimia, lakini kupokea malipo kuna gharama fulani isiyobadilika: kuhamisha USDT kutoka anwani ya kupokea kunamaanisha kuijaza kwa gesi kwanza, kutoka mfukoni mwetu. Chini ya dola chache ada haigharamii uendeshaji, na kukubali malipo kama hayo kungemaanisha kukuwekea pesa isiyofaa kiuchumi kuihamisha.
Kikomo cha juu si dhidi ya wafanyabiashara wakubwa — ni mtego wa kosa la vipimo. Tuma "5000000" mahali ulipokusudia "5" na vinginevyo ungepata ankara ya dola milioni tano: mnunuzi anaona kiasi kisicho na maana na anaondoka. Agizo halisi haligusi dari hii kamwe; kosa hugusa kila mara. Dari zote mbili ni mipangilio (invoice_max_ton, invoice_max_usdt) na zinaweza kupandishwa kwa duka lako — tuulize.
Kikomo cha kila saa na kikomo cha ankara wazi vyote vinalinda hazina ya anwani. Kila ankara wazi inashikilia anwani ya kupokea, na mzunguko usiodhibitiwa kwenye tovuti moja vinginevyo ungemwaga hazina kwa kila mtu. Duka jipya laweza kushikilia ankara 20 wazi kwa wakati mmoja; kiwango hukua kwa moja kwa kila ankara ambayo kwa kweli imelipwa, hadi dari ya 200. underpaid huhesabika kama wazi — bado inashikilia anwani yake, ikisubiri kilichobaki. Kughairi ankara iliyotelekezwa hurudisha anwani yake mara moja. Marudio yenye idempotency_key ile ile hayahesabiwi dhidi ya kikomo cha kila saa.
Kikomo cha maombi ni 120 kwa dakika kwa kila ufunguo wa API — wito miwili kwa sekunde, juu zaidi ya mtiririko wowote halisi wa oda. 429 inabeba kichwa cha Retry-After kwa sekunde: subiri muda huo badala ya kujaribu tena kwenye mzunguko mkali, jambo linalosukuma tu dirisha mbele zaidi.
Hitilafu#
Misimbo ya hali utakayoiona kwa hakika.
Hitilafu zinarudi kama JSON, katika maumbo mawili. Chochote tunachoamua sisi au kiini cha uchakataji huweka jozi ya {code, message} chini ya detail. Mwili wa ombi usiopita uthibitishaji huweka hapo orodha ya hitilafu za uga badala yake. Angalia umepata umbo lipi kabla ya kusoma detail.code — na amua kwa `code`, kamwe si kwa `message`: maneno yanaweza kubadilika wakati wowote, msimbo hautabadilika.
{
"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
}
]
}| Hali | Lini | Cha kufanya |
|---|---|---|
| 401 | Ufunguo haupo, si sahihi, au umebatilishwa. | Angalia kichwa. Toa upya ufunguo ikiwa ulibatilishwa. |
| 404 | Hakuna ankara kama hiyo, au ni ya duka lingine. | Angalia kitambulisho. Hali hizo mbili hujibiwa sawasawa kwa makusudi, ili kitambulisho kisiweze kupelelezwa. |
| 409 | Ankara iko katika hali inayozuia hili. | Soma hali yake ya sasa kwanza. |
| 422 | Ombi lina umbo baya, au kiasi kiko nje ya mipaka ya ankara. | Ujumbe unataja thamani iliyotumwa na kikomo. |
| 429 | Ankara nyingi mno saa hii, nyingi mno wazi kwa wakati mmoja, au maombi mengi mno. | Subiri Retry-After iishe, kisha jaribu tena. |
| 502 | Hatukuweza kufikia kiini cha uchakataji. | Jaribu tena kwa idempotency key ile ile. |
Misimbo
Umbo tunaloamua sisi ni {"detail": {"code": …, "message": …}}. Hii ndiyo misimbo ambayo API ya mfanyabiashara hurudisha.
| Msimbo | Hali | Maana |
|---|---|---|
| invalid_api_key | 401 | Ufunguo haupo, umeharibika, haujulikani au umebatilishwa. Hali zote nne hujibu vivyo hivyo, hivyo ufunguo hauwezi kupimwa kwa kubahatisha. |
| not_found | 404 | Hakuna kitu kama hicho, au ni cha duka lingine. |
| invalid_input | 422 | Ombi halikupita uthibitishaji ndani ya kiini — kiasi kibovu, desimali nyingi mno, kiasi kilicho nje ya mipaka ya ankara. |
| conflict | 409 | Kitendo kinapingana na hali ya sasa, kama vile kughairi ankara ambayo tayari haiko wazi. |
| too_many_requests | 429 | Kikomo cha kasi: ankara kwa saa, ankara wazi, au maombi kwa dakika. Retry-After inasema usubiri kwa muda gani. |
| cbc_unreachable | 502 | Hatukuweza kufikia kiini cha uchakataji. Jaribu tena ukitumia idempotency_key ile ile. |
| webhook_url_rejected | 422 | Wakati wa kuhifadhi ufunguo pekee: URL ya webhook haikupita ukaguzi ulio hapo juu. detail.reason inataja kanuni ipi — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials, na kadhalika. |
502 haimaanishi ankara haikuundwa — ombi laweza kuwa lilipita huku jibu likipotea njiani kurudi. Jaribu tena kwa idempotency_key ile ile na utapata ima ankara iliyopo au mpya, kamwe si mbili.
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.
Marejesho#
Jinsi ya kumrudishia mteja pesa.
Marejesho hupitia msaada, si kwa wito wa API. Marejesho ni uhamisho mpya kwenda anwani aliyoitoa mtu, na mchakataji wa malipo anayerudisha pesa kiotomatiki kwa wito wa API ni mchakataji anayeweza kulazimishwa kutuma pesa kwenye anwani ya mshambuliaji. Ndiyo maana ni kwa mkono kwa makusudi.
Ili kumrejeshea mnunuzi, fungua tiketi ya msaada kutoka eneo la akaunti yako ukiwa na invoice_id au tx_hash, kiasi, na anwani ya kutuma. Mwendeshaji anakagua malipo, anatoa pesa kwenye salio lako, na anajibu kwenye tiketi hiyo hiyo. Tarajia hili lichukue siku moja ya kazi, si dakika moja.
Kuna matokeo mawili yanayostahili kuyapangia. Malipo ya ziada yanawekwa kwako kikamilifu — hatuchukui chochote kutoka kwake — hivyo kumrudishia tofauti mnunuzi aliyetuma zaidi ni uamuzi wako, na hufuata njia ile ile. Na ankara iliyolipwa pungufu si suala la marejesho ilhali bado iko wazi: pesa ziko kwenye salio lako, anwani bado inafuatiliwa, na mnunuzi anaweza tu kuongeza. Ni baada tu ya kipindi cha neema, ankara inapoenda expired ikiwa na pesa ndani yake, ndipo kuna uamuzi wa kufanya.
Kupima#
Jinsi ya kujaribu muunganisho wako kabla ya uzinduzi.
Funguo hapa ni halisi: kila ufunguo unaotolewa ni ufunguo wa sk_live_ dhidi ya kiini cha uzalishaji na mainnet ya TON. Hakuna mazingira tofauti ya majaribio, na hilo lina faida: unapitia njia ile ile hasa ambayo oda zako halisi zitapita.
Hivyo pima jinsi ungepima kitu chochote kinachogusa pesa halisi: kwa kiasi kidogo. Tengeneza ankara ya kiwango cha chini kabisa (0.1 TON au 3 USDT), ilipe kutoka pochi yako mwenyewe, na tazama njia nzima — ukurasa wa malipo, webhook, ukaguzi wa sahihi, oda yako ikigeuka kuwa imelipwa. Ada inatozwa, na sarafu zinahama kweli.
Sehemu unazoweza kujaribu bila kutumia chochote: kutengeneza na kusoma ankara, kughairi moja, 422 kwenye kiasi chenye umbo baya, 401 kwenye ufunguo usio sahihi, na uthibitishaji wako mwenyewe wa sahihi — saini mwili wa mfano kwa siri yako na uupeleke kwenye kishughulikiaji chako mwenyewe. Kinachohitaji malipo halisi kweli ni hatua ya mwisho pekee: webhook halisi ya payment.credited.
Panga muunganisho ili usitegemee sandbox wala malipo ya kuiga: njia halisi huthibitishwa haraka zaidi — na kwa uhakika zaidi.
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.
Orodha ya kukagua kabla ya uzinduzi#
Mambo kumi ya kukagua kabla ya kuanza.
- Ufunguo uko upande wa seva pekee, kamwe kwenye JavaScript ya kivinjari.
- Sahihi ya webhook inathibitishwa dhidi ya
"{timestamp}.{raw_body}", kwa muda usiobadilika. - Utoaji wenye umri zaidi ya dakika tano unakataliwa, na saa ya seva iko kwenye NTP.
X-Paysell-Event-Idinayorudiwa haifanyi chochote mara ya pili.- Webhook inajibu 2xx ndani ya sekunde kumi; kazi ya polepole inafanyika baadaye.
- URL ya webhook ni kikoa cha https:// kwenye bandari 443, bila uelekezaji upya mbele yake.
- Webhook iliyokosekana inavumilika: endpoint ya ankara inasomwa kwenye ukurasa wa shukrani au kwenye ukaguzi wa ulinganishaji.
idempotency_keyinazalishwa mara moja kwa kila oda na kutumika tena kwenye marudio.- Kiasi hutoka kama string katika vipimo vya kawaida; namba za webhook husomwa kama vipimo vidogo zaidi.
- Anwani inaonyeshwa kama ilivyorudishwa hasa, bila kubadilishwa.
overpaidnaunderpaidzinashughulikiwa, sipaidtu;expiredbado inaweza kubebapaid_minor.- Bidhaa hutolewa kwa
status: paidauoverpaid, si kwa sababu tu wito umefika. 429hushughulikiwa kwa kusubiriRetry-Afteriishe, si kwa kujaribu tena mara moja.- Salio linasomwa kutoka kwetu, si kufuatiliwa tofauti kama ukweli.
Kuna kisichoeleweka?
Ikiwa ukurasa huu haukujibu swali lako, hilo ni pengo katika nyaraka na linafaa kutuambia. Andika kutoka eneo la akaunti yako na tutarekebisha ukurasa, si jibu tu.