Paysell

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.

Salio linakaa kwetu na ndilo chanzo pekee cha ukweli. Lionyeshe, lakini usiwahi kutunza nakala ya pili kama yenye mamlaka — kaunta mbili daima hutengana mapema au baadaye, na hapo hakuna anayejua ni ipi sahihi.

Jinsi malipo yanavyofanya kazi#

Hatua sita, nyingi zikiwa zetu.

Hatua sita, nyingi zikiwa zetu:

  1. 1

    Mteja wako anabofya lipa

    Seva yako inaita API yetu ikiwa na kiasi na rejeleo lako la oda.

  2. 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. 3

    Mteja anatuma sarafu

    Anaskani msimbo wa QR au kunakili anwani. Watumie kwenye payment_url tunayorudisha na ukurasa unashughulikiwa kwa ajili yako — kiasi, anwani, QR, kihesabu muda, hali ya moja kwa moja.

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

    Yamewekwa, na umeambiwa

    Ada inakatwa, kilichobaki kinaingia kwenye salio lako, na webhook iliyosainiwa inatumwa kwa seva yako ikiwa na order_id yako.

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.

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

Malipo ya ziada yanawekwa kikamilifu — hatubaki na tofauti. Malipo pungufu yanaacha ankara wazi ili mnunuzi aweze kuongeza kwenye anwani ile ile.

Kuchukua sarafu kutoka anwani ya kupokelea kunagharimu gesi ya mtandao, na tunailipa sisi — sehemu hiyo haigusi salio lako kamwe. Kutoa fedha kwenda anwani yako mwenyewe ni jambo lingine: kuna ada yake, inayokatwa kutoka kiasi unachoomba, na namba kamili ziko kwenye orodha ya ada.

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

    Unda ankara

    Ombi moja kutoka seva yako, kiungo kimoja kinachorudi. Vipande vinne hapa chini vyote hutuma kitu kile kile.

  4. 4

    Mpeleke mnunuzi kwenye payment_url

    Huo 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. 5

    Subiri webhook

    Pesa zinapothibitishwa kwenye mnyororo na kuingizwa, tunatuma POST yenye tukio lililosainiwa payment.credited kwenye seva yako. Thibitisha sahihi, kisha weka oda kuwa imelipwa — lakini pale tu data.status ikiwa paid au overpaid. 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

Uthibitishaji#

Ufunguo wako wa API, na jinsi unavyotumika.

Kila ombi hubeba ufunguo wako kwenye kichwa cha Authorization:

http
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxA

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

Ufunguo huu unaunda ankara kwa jina lako. Uweke upande wa seva. Chochote kilicho kwenye JavaScript ya kivinjari ni cha umma, haijalishi kimefichwa vizuri kiasi gani.

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.

Unda ankara#

POST /api/merchant/v1/invoices

POST/api/merchant/v1/invoices

Mwili wa ombi

SehemuAinaInahitajikaMaelezo
assetstringndiyoIma TON au USDT_TON.
amountstringndiyoVipimo vya kawaida vya sarafu, kama string: "5" ni 5 USDT. Si desimali nyingi kuliko sarafu ilivyo nazo. Ona Kiasi.
order_idstringhapanaRejeleo lako mwenyewe, hadi herufi 200. Linarudi kwenye kila webhook — hivi ndivyo unavyolinganisha malipo na oda.
descriptionstringhapanaHadi herufi 1000. Inaonyeshwa kwa mnunuzi kwenye ukurasa wa malipo.
ttl_minutesnumberhapanaAnkara inabaki kulipika kwa dakika ngapi. 1–1440; usipoiweka, chaguo-msingi hutumika — saa 2 leo.
idempotency_keystringhapanaHadi 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

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"
}

Kuiweka kwenye oda yako

SehemuCha kufanya nayo
invoice_idIhifadhi dhidi ya oda yako. Ndicho kinachotambulisha malipo kila mahali pengine.
payment_urlMpeleke mnunuzi hapa. Hakuna kingine cha kujenga.
addressTu ikiwa unatengeneza ukurasa wako wa malipo. Ionyeshe kama ilivyotolewa hasa — angalia onyo hapa chini.
amountKiasi katika vipimo vya kawaida, kama ulivyokituma hasa. Onyesha hiki.
amount_minorKiasi kile kile kama namba kamili katika kipimo kidogo zaidi. Hesabu kwa hiki.
expires_atOnyesha kihesabu muda. Baada ya kupita, anwani haifuatiliwi tena kwa ankara hii.
statusHapa daima pending. Mabadiliko halisi yanakuja kwa webhook.
Ukijenga ukurasa wako mwenyewe, chapisha anwani kama ilivyo hasa ilivyorudishwa. Iko katika fomu isiyoruka-rudi (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}

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

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

Inafunga 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

TukioLiniKilicho ndani ya mwili
payment.creditedUhamisho umethibitishwa kwenye mnyororo, ada yetu imekatwa, na kilichobaki kipo kwenye salio lako.Sehemu zilizoorodheshwa hapa chini.
payment.rejectedAmana 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

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"
  }
}

Ulinganisho wa sehemu

SehemuMaana
event_idYa kipekee kwa kila tukio; pia iko kwenye kichwa cha X-Paysell-Event-Id. Ihifadhi na upuuze marudio — angalia hapa chini.
data.order_idRejeleo lako. Tafuta oda yako kwa hii.
data.amountKiasi alichotuma mnunuzi katika uhamisho huu, katika kipimo kidogo zaidi — tofauti na API inayopokea vipimo vya kawaida.
data.feeKiasi tulichochukua, katika kipimo kidogo zaidi.
data.creditedKilichoingia kwenye salio lako: amount − fee, katika kipimo kidogo zaidi.
data.paid_minorJumla 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.assetSarafu iliyofika kweli. Si lazima iwe sarafu ambayo ankara iliomba.
data.asset_mismatchIpo, 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_assetHuja pamoja na asset_mismatch: sarafu ambayo ankara inaomba kweli.
data.statusHali ya ankara kwa sasa: pending, underpaid, paid, overpaid au expired. Linganisha na ulichotarajia.
data.tx_hashMuamala kwenye blockchain, kwa kumbukumbu zako na msaada.

Vichwa vinavyokuja kwenye kila utoaji

http
X-Paysell-Event:      payment.credited
X-Paysell-Event-Id:   99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp:  1789000000
X-Paysell-Signature:  sha256=6f1c0e6a…
KichwaMaana
X-Paysell-EventAina ya tukio: payment.credited au payment.rejected.
X-Paysell-Event-IdYa kipekee kwa kila tukio. Hii ndiyo thamani ya kuondolea marudio.
X-Paysell-TimestampWakati tulipotia sahihi, kwa sekunde za unix. Ni sehemu ya mfuatano uliosainiwa.
X-Paysell-Signaturesha256= 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:

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)
}

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 24

Majaribio 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, .lan au .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.1 baadaye pia hakufanyi kazi.
  • Uelekezaji upya ni kushindwa, si hatua. Hatuufuati: anwani uliyotupa ilikaguliwa, ile iliyo kwenye kichwa cha Location haikukaguliwa.
Uthibitishaji hufanyika mara mbili kwa makusudi — mara moja unapohifadhi URL, ili kosa la kuandika lijibiwe papo hapo badala ya kwa kutofika kimyakimya, na mara moja kabla ya kila utumaji, kwa sababu mmiliki wa kikoa anaweza kukielekeza kwenye anwani ya ndani wakati wowote. Endpoint yako ikihama, sasisha ufunguo kwanza: URL iliyokataliwa haitoi chochote wala haipangi foleni.

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.

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.

Rejeleo la hali#

Kila hali ya ankara na malipo, imefafanuliwa.

Ankara

HaliMaanaCha kufanya
pendingInasubiri malipo.Iache oda wazi.
paidImelipwa kikamilifu.Toa bidhaa.
overpaidKimefika zaidi ya kilichoombwa. Ziada inawekwa kwako kikamilifu.Toa bidhaa; rudisha tofauti ukipenda.
underpaidKimefika 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.
expiredDirisha 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.
cancelledUmeghairi wewe. Anwani inarudishwa kwenye hazina.Hakuna.

Malipo

Yanaonekana kwenye eneo la akaunti yako; yanafaa unaposaidia mteja katikati ya malipo.

HaliMaana
detectedImeonekana kwenye chain, inasubiri uthibitisho.
confirmedMtandao umeithibitisha. Kuweka kunafuata.
creditedKwenye salio lako. Huu ndio wakati webhook inapiga.
reviewImezuiliwa kwa ukaguzi wa ziada — kwa mfano, sarafu zinazofika kwenye anwani isiyo na ankara wazi.
rejectedHaikuwekwa. 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.

SarafuDesimaliUnatumaamount_minor katika jibu
TON9"1.5""1500000000"
USDT_TON6"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.

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

Mipaka#

Kiwango cha chini, cha juu, na mipaka ya kasi.

KikomoThamaniIkikiukwa
Ankara ya chini kabisa0.1 TON · 3 USDT422
Ankara ya juu kabisa7000 TON · 10000 USDT422
Ankara kwa saa, kwa kila duka60429
Ankara wazi kwa wakati mmoja20, zikiongezeka kwa kila ankara iliyolipwa, hadi 200429
Muda wa uhai wa ankaradakika 1 – saa 24 (chaguo-msingi saa 2)422
Maombi ya API kwa kila ufunguo120 kwa dakika429 + 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.

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
    }
  ]
}
HaliLiniCha kufanya
401Ufunguo haupo, si sahihi, au umebatilishwa.Angalia kichwa. Toa upya ufunguo ikiwa ulibatilishwa.
404Hakuna ankara kama hiyo, au ni ya duka lingine.Angalia kitambulisho. Hali hizo mbili hujibiwa sawasawa kwa makusudi, ili kitambulisho kisiweze kupelelezwa.
409Ankara iko katika hali inayozuia hili.Soma hali yake ya sasa kwanza.
422Ombi lina umbo baya, au kiasi kiko nje ya mipaka ya ankara.Ujumbe unataja thamani iliyotumwa na kikomo.
429Ankara nyingi mno saa hii, nyingi mno wazi kwa wakati mmoja, au maombi mengi mno.Subiri Retry-After iishe, kisha jaribu tena.
502Hatukuweza 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.

MsimboHaliMaana
invalid_api_key401Ufunguo haupo, umeharibika, haujulikani au umebatilishwa. Hali zote nne hujibu vivyo hivyo, hivyo ufunguo hauwezi kupimwa kwa kubahatisha.
not_found404Hakuna kitu kama hicho, au ni cha duka lingine.
invalid_input422Ombi halikupita uthibitishaji ndani ya kiini — kiasi kibovu, desimali nyingi mno, kiasi kilicho nje ya mipaka ya ankara.
conflict409Kitendo kinapingana na hali ya sasa, kama vile kughairi ankara ambayo tayari haiko wazi.
too_many_requests429Kikomo cha kasi: ankara kwa saa, ankara wazi, au maombi kwa dakika. Retry-After inasema usubiri kwa muda gani.
cbc_unreachable502Hatukuweza kufikia kiini cha uchakataji. Jaribu tena ukitumia idempotency_key ile ile.
webhook_url_rejected422Wakati 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 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.

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.

Chukulia oda yako ya kwanza halisi kama jaribio la kweli: chagua kiasi kidogo, iache ankara wazi kwenye dashibodi, na angalia safu ya malipo na hali ya webhook kabla ya kuwaelekeza wateja halisi hapo.

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.

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-Id inayorudiwa 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_key inazalishwa 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.
  • overpaid na underpaid zinashughulikiwa, si paid tu; expired bado inaweza kubeba paid_minor.
  • Bidhaa hutolewa kwa status: paid au overpaid, si kwa sababu tu wito umefika.
  • 429 hushughulikiwa kwa kusubiri Retry-After iishe, 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.