Paysell

Terima pembayaran kripto

Paysell menyelesaikan TON dan USDT pada rangkaian TON. Anda mencipta invois, kami berikan pautan, dan anda menerima panggilan balik yang ditandatangani sebaik sahaja wang disahkan di rantaian dan dikreditkan ke baki anda.

Gambaran keseluruhan#

Apa yang Paysell lakukan, dan apa yang tidak.

Paysell ialah pemproses pembayaran, bukan dompet. Anda tidak pernah mengendalikan kunci peribadi, memantau blockchain, atau menentukan bila transaksi menjadi muktamad — itu bahagian yang kami ambil alih.

Setiap invois mempunyai alamat penerimaannya sendiri. Apabila pembeli membayarnya, kami menunggu rangkaian mengesahkan pemindahan, menolak yuran kami, dan mengkreditkan bakinya ke baki anda. Anda boleh mengeluarkan ke mana-mana alamat yang anda suka.

Baki disimpan di pihak kami dan merupakan satu-satunya sumber kebenaran. Paparkannya, tetapi jangan sekali-kali menyimpan salinan kedua sebagai autoritatif — dua kaunter sentiasa akan tersasar akhirnya, dan pada masa itu tiada siapa tahu yang mana betul.

Cara pembayaran berfungsi#

Enam langkah, kebanyakannya milik kami.

Enam langkah, kebanyakannya milik kami:

  1. 1

    Pelanggan anda klik bayar

    Pelayan anda memanggil API kami dengan jumlah dan rujukan pesanan anda sendiri.

  2. 2

    Kami berikan alamat

    Alamat penerimaan baharu diambil daripada kolam yang telah dijana lebih awal dan dikaitkan dengan invois ini. Satu alamat dimiliki oleh tepat satu invois terbuka, dan begitulah cara pembayaran dipadankan semula kepadanya.

  3. 3

    Pelanggan menghantar syiling

    Mereka mengimbas kod QR atau menyalin alamat. Hantar mereka ke payment_url yang kami kembalikan dan halaman itu diuruskan untuk anda — jumlah, alamat, QR, kiraan detik, status langsung.

  4. 4

    Kami mengesan pemindahan

    Dua sumber data blockchain yang bebas ditanya, dan jawapan mereka dibandingkan. Jika tidak sepakat, kami berhenti dan bukannya memilih jawapan yang lebih mudah.

  5. 5

    Kami menunggu kemuktamadan

    Kemasukan dalam masterchain ditambah tiga blok di atasnya. Lebih kurang lima belas saat — pembayaran yang kelihatan selesai tetapi kemudian hilang akan menjadi kerugian anda, jadi kami tidak mengambil risiko itu.

  6. 6

    Dikreditkan, dan anda diberitahu

    Yuran ditolak, bakinya masuk ke baki anda, dan webhook yang ditandatangani dihantar ke pelayan anda membawa order_id anda.

Kira-kira seminit dari pembayaran hingga panggilan balik: lebih kurang lima belas saat pengesahan rangkaian, selebihnya adalah imbasan kami terhadap alamat yang dipantau.

Ke mana wang pergi#

Yuran, dan asas pengiraannya.

Yuran ialah 0.2%, ditetapkan untuk kedai anda pada saat ia didaftarkan. Jika kadar standard berubah kemudian, kadar anda tidak berubah — ia ditulis ke dalam setiap invois sebagai nombor, bukan sebagai rujukan kepada tetapan.

Yuran diambil daripada apa yang sebenarnya tiba, bukan daripada apa yang diminta oleh invois. Bilkan 5 USDT dan terima 20, yuran dikira berdasarkan 20. Kurang bayar, dan ia dikira berdasarkan apa yang masuk.

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

Lebihan bayaran dikreditkan sepenuhnya — kami tidak menyimpan bakinya. Kurang bayar membiarkan invois terbuka supaya pembeli boleh menambah baki ke alamat yang sama.

Mengeluarkan syiling daripada alamat penerimaan memerlukan gas rangkaian, dan kamilah yang membayarnya — bahagian itu tidak sekali-kali menyentuh baki anda. Pengeluaran ke alamat anda sendiri ialah perkara berbeza: ia membawa yurannya sendiri, ditolak daripada jumlah yang anda minta, dan angka tepatnya ada dalam Jadual Yuran.

Mula pantas#

Lima minit ke invois pertama anda.

Lima langkah. Dua ialah klik dalam kawasan akaun anda, satu ialah satu permintaan daripada pelayan anda, dan dua yang terakhir berlaku dengan sendirinya.

  1. 1

    Cipta kedai

    Dalam kawasan akaun anda. Ia mula menerima pembayaran serta-merta — tiada menunggu semakan. Pengesahan berlaku secara senyap di latar belakang dan hanya menyekat pengeluaran, bukan pembayaran masuk.

  2. 2

    Keluarkan kunci API

    Kedai anda → Kunci API → Kunci baharu. Kunci dan rahsia webhook dipaparkan sekali sahaja dan tidak akan ditunjukkan lagi. Simpan seperti anda menyimpan kata laluan pangkalan data, dan jangan sekali-kali hantarkannya ke pelayar.

  3. 3

    Cipta invois

    Satu permintaan daripada pelayan anda, satu pautan dikembalikan. Keempat-empat petikan kod di bawah menghantar perkara yang sama.

  4. 4

    Hantar pembeli ke payment_url

    Itulah keseluruhan proses pembayaran — jumlah, alamat, kod QR, kiraan detik, status langsung — dan tiada apa yang perlu dibina. Lihat Halaman pembayaran untuk apa yang pembeli benar-benar lihat.

  5. 5

    Tunggu webhook

    Sebaik sahaja wang disahkan pada rantaian dan dikreditkan, kami POST peristiwa payment.credited yang ditandatangani ke pelayan anda. Sahkan tandatangan, kemudian tandakan pesanan sebagai dibayar — tetapi hanya apabila data.status ialah paid atau overpaid. Lihat Webhook.

The same request, four ways

curl -X POST https://paysell.me/api/merchant/v1/invoices \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDT_TON",
    "amount": "5",
    "order_id": "order-1042",
    "idempotency_key": "order-1042"
  }'

Alihkan pembeli ke payment_url dalam respons. Anda selesai — selebihnya akan tiba sebagai webhook.

What to do next

Pengesahan#

Kunci API anda, dan cara ia digunakan.

Setiap permintaan membawa kunci anda dalam pengepala Authorization:

http
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxA

Setiap kunci yang dikeluarkan di sini bermula dengan sk_live_. Awalan sk_test_ hanya wujud pada pemasangan yang menghala ke rangkaian ujian, dan pemasangan sedemikian tidak ditawarkan — lihat Ujian. Kami menyimpan cincang sehala, bukan kunci itu sendiri, jadi tiada siapa, termasuk kami, boleh menunjukkannya kepada anda semula. Hilang? Keluarkan yang baharu dan batalkan yang lama.

Kedai diperoleh daripada kunci, itulah sebabnya tiada permintaan yang mengambil id kedai. Kunci hanya boleh bertindak ke atas kedainya sendiri.

Laluan membawa versi: /api/merchant/v1/…. Di dalam satu versi kami hanya menambah medan — tiada apa dinamakan semula dan tiada apa bertukar makna secara senyap. Perubahan yang akan merosakkan kod anda mendapat awalan baharu, /v2, dan /v1 terus berfungsi untuk tempoh yang diumumkan.

Kunci ini mencipta invois atas nama anda. Simpan di sisi pelayan. Apa sahaja dalam JavaScript pelayar adalah awam, tidak kira sebaik mana ia disembunyikan kelihatannya.

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.

Cipta invois#

POST /api/merchant/v1/invoices

POST/api/merchant/v1/invoices

Badan permintaan

MedanJenisDiperlukanPenerangan
assetstringyaSama ada TON atau USDT_TON.
amountstringyaUnit biasa syiling, sebagai rentetan: "5" ialah 5 USDT. Tidak lebih banyak tempat perpuluhan daripada yang ada pada syiling. Lihat Jumlah.
order_idstringtidakRujukan anda sendiri, sehingga 200 aksara. Kembali dalam setiap webhook — inilah cara anda memadankan pembayaran dengan pesanan.
descriptionstringtidakSehingga 1000 aksara. Dipaparkan kepada pembeli pada halaman pembayaran.
ttl_minutesnumbertidakBerapa lama invois kekal boleh dibayar, dalam minit. 1–1440; tinggalkannya dan nilai lalai digunakan — 2 jam buat masa ini.
idempotency_keystringtidakSehingga 200 aksara. Hantar nilai yang sama semasa mencuba semula dan anda mendapat invois yang sama dikembalikan, bukan yang kedua. Ia medan dalam badan permintaan, bukan pengepala Idempotency-Key — pengepala itu tidak dibaca di sini.

Respons · 201

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

Memetakannya kepada pesanan anda

MedanApa yang perlu dilakukan
invoice_idSimpan berbanding pesanan anda. Inilah yang mengenal pasti pembayaran di tempat lain.
payment_urlAlihkan pembeli ke sini. Tiada apa lagi untuk dibina.
addressHanya jika anda merender halaman pembayaran anda sendiri. Paparkan tepat seperti diberikan — lihat amaran di bawah.
amountJumlah dalam unit biasa, tepat seperti yang anda hantar. Paparkan yang ini.
amount_minorJumlah yang sama sebagai integer dalam unit terkecil. Kira dengan yang ini.
expires_atPapar kiraan detik. Selepas ia berlalu alamat berhenti dipantau untuk invois ini.
statusSentiasa pending di sini. Perubahan sebenar tiba melalui webhook.
Jika anda merender halaman anda sendiri, cetak alamat tepat seperti dikembalikan. Ia dalam bentuk tidak boleh lantun (UQ… pada mainnet, 0Q… pada testnet). Tukarkannya, cantikkannya, atau tukar kepada pengekodan lain bagi alamat yang sama, dan syiling yang dihantar ke dompet yang belum digunakan akan melantun kembali kepada penghantar.

Baca invois#

GET /api/merchant/v1/invoices/{invoice_id}

GET/api/merchant/v1/invoices/{invoice_id}

Bentuk yang sama seperti di atas, dengan status, paid dan paid_minor mencerminkan keadaan semasa: paid ialah berapa banyak yang sudah tiba dalam unit biasa, paid_minor jumlah yang sama sebagai integer dalam unit terkecil. Berguna sebagai sandaran apabila webhook terlepas, atau pada halaman terima kasih.

Tanyakan sekurang-kurangnya setiap beberapa saat, dan anggap webhook sebagai saluran utama. Invois yang dimiliki oleh kedai lain menjawab 404 — bukan 403, supaya id tidak boleh diselidik kewujudannya.

Batalkan invois#

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

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

Menutup invois yang masih terbuka — pending atau underpaid — dan melepaskan alamatnya. Gunakan apabila pelanggan meninggalkan proses pembayaran: alamat adalah sumber terhad, dan mengembalikannya mengekalkan kolam sihat.

Invois yang tidak lagi terbuka menjawab 409. Membatalkan invois underpaid tidak memulangkan syiling kepada sesiapa: wang yang sudah dikreditkan kekal pada baki anda, dan yang ditutup hanyalah penerimaan bayaran tambahan.

Webhook#

Apa yang tiba, dan cara mengesahkannya.

Tetapkan URL webhook semasa membuat kunci. Kami POST ke situ apabila pembayaran dikreditkan — dan apabila deposit yang ditahan untuk semakan tambahan ditolak. Setiap penghantaran ditandatangani, dan kami terus mencuba semula selama kira-kira sehari setengah sehingga anda menjawab 2xx. Lepaskan barang pada status: paid atau overpaid, bukan hanya kerana panggilan itu sampai.

Peristiwa

PeristiwaBilaApa yang dibawa badan
payment.creditedPemindahan disahkan di rantaian, yuran kami diambil, dan bakinya masuk ke baki anda.Medan yang disenaraikan di bawah.
payment.rejectedDeposit yang ditahan untuk semakan tambahan (lihat Rujukan status) telah ditolak. Wang itu tidak akan sampai ke baki anda.invoice_id, order_id, asset, amount, tx_hash dan reason. Jangan lepaskan barang; jika invois itu sudah paid daripada pemindahan terdahulu, peristiwa ini mengenai deposit tambahan, bukan mengenai pembayaran tersebut.

Apa yang tiba

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

Pemetaan medan

MedanMaksud
event_idUnik bagi setiap peristiwa; juga dalam pengepala X-Paysell-Event-Id. Simpan dan abaikan pengulangan — lihat di bawah.
data.order_idRujukan anda. Cari pesanan anda dengan ini.
data.amountBerapa yang pembeli hantar dalam pemindahan ini, dalam unit terkecil — berbeza dengan API yang menerima unit biasa.
data.feeBerapa yang kami ambil, dalam unit terkecil.
data.creditedBerapa yang masuk ke baki anda: amount − fee, dalam unit terkecil.
data.paid_minorJumlah keseluruhan yang diterima pada invois ini setakat ini, dalam unit terkecil. Medan yang penting pada underpaid: status memberitahu kurang yang sampai, ini memberitahu berapa kurangnya.
data.assetSyiling yang benar-benar sampai. Tidak semestinya syiling yang diminta oleh invois.
data.asset_mismatchHadir, dan bernilai true, hanya apabila syiling yang sampai bukan syiling invois. Wang dikreditkan kepada anda, tetapi invois kekal belum dibayar dan status tidak akan sesekali menjadi paid.
data.invoice_assetDatang bersama asset_mismatch: syiling yang sebenarnya diminta oleh invois.
data.statusStatus invois sekarang: pending, underpaid, paid, overpaid atau expired. Bandingkan dengan apa yang anda jangkakan.
data.tx_hashTransaksi di rantaian, untuk rekod dan sokongan anda.

Pengepala pada setiap penghantaran

http
X-Paysell-Event:      payment.credited
X-Paysell-Event-Id:   99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp:  1789000000
X-Paysell-Signature:  sha256=6f1c0e6a…
PengepalaMaksud
X-Paysell-EventJenis peristiwa: payment.credited atau payment.rejected.
X-Paysell-Event-IdUnik bagi setiap peristiwa. Inilah nilai yang perlu digunakan untuk menyahduplikat.
X-Paysell-TimestampBila kami menandatangani, dalam saat unix. Ia sebahagian daripada rentetan yang ditandatangani.
X-Paysell-Signaturesha256= diikuti HMAC heks. Lihat di bawah.

Mengesahkan tandatangan

Setiap permintaan ditandatangani dengan rahsia webhook yang dipaparkan sekali semasa anda mencipta kunci. Tandatangan itu ialah HMAC-SHA256(secret, "{timestamp}.{raw_body}") — cap masa daripada X-Paysell-Timestamp, satu noktah literal, kemudian bait badan. Semaknya sebelum bertindak: tanpa ini, sesiapa yang mengetahui URL anda boleh menyerahkan kepada anda pesanan yang telah dibayar.

Python:

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

Tandatangani bait mentah badan, tepat seperti diterima. Huraikan JSON dan siri semula ia dan bait berubah — susunan kunci, jarak — dan tandatangan tidak akan sepadan. Bandingkan dalam masa malar (hmac.compare_digest, crypto.timingSafeEqual): == biasa kembali lebih cepat apabila bait pertama sudah salah, dan perbezaan itu cukup untuk meneka tandatangan sebait demi sebait.

Tetingkap cap masa

Tolak apa sahaja yang cap masanya lebih daripada lima minit jauh daripada jam anda sendiri, ke mana-mana arah. Cap masa berada di dalam rentetan yang ditandatangani justeru supaya ia tidak boleh diubah tanpa merosakkan tandatangan; tetingkap inilah yang menjadikannya perlindungan. Tanpanya, permintaan yang pernah dirakam kekal sah selama-lamanya dan boleh dimainkan semula bila-bila masa — tandatangan itu sendiri tidak pernah luput. Kekalkan jam pelayan anda pada NTP, atau semakan ini akan mula menolak penghantaran yang sah.

Pertindihan

Peristiwa yang sama boleh tiba lebih daripada sekali. Itu bukan pepijat: kami mencuba semula sehingga anda menjawab 2xx, dan penghantaran yang berjaya tetapi responsnya tidak pernah sampai kepada kami akan dihantar semula. Rekodkan X-Paysell-Event-Id (ia juga datang sebagai event_id dalam badan) dan pastikan ketibaan kedua tidak melakukan apa-apa.

Cubaan semula

Percubaan pertama dihantar sebaik sahaja pembayaran dikreditkan. Jika ia gagal — tamat masa, sambungan ditolak, ralat TLS, pengalihan, atau apa-apa status bukan 2xx — kami mencuba semula mengikut jadual tetap:

1 min → 5 min → 15 min → 1 jam → 6 jam → 24 jam

Tujuh percubaan kesemuanya, tersebar dalam lebih kurang 31 jam. Yang awal rapat antara satu sama lain kerana puncanya biasanya penerima yang sedang dimulakan semula dan sudah pun kembali; yang lewat jarang-jarang kerana menghentam pelayan yang sudah sehari tumbang tidak membantu sesiapa.

Selepas percubaan terakhir, penghantaran ditandakan dropped dan kami berhenti dengan sendirinya. Ia tidak hilang: baris pembayaran dalam kawasan akaun anda menunjukkan keadaannya, bilangan percubaan dan kelas ralat, dengan butang Hantar semula yang memulakan pusingan baharu kesemua tujuh percubaan. Jalan lain anda ialah GET /api/merchant/v1/invoices/{invoice_id} — invois sentiasa tahu statusnya sendiri.

Rupa yang mesti ada pada URL webhook

URL disemak semasa anda menyimpannya, dan sekali lagi sebelum setiap satu penghantaran. URL yang gagal semakan dijawab dengan 422 dan code: "webhook_url_rejected" semasa penyimpanan, dan menandakan penghantaran sebagai failed — tanpa cubaan semula — jika ia mula gagal kemudian. Peraturannya:

  • `https://` sahaja, dan port 443. Webhook membawa butiran pembayaran; dalam http biasa ia boleh dibaca oleh sesiapa sahaja di sepanjang laluan.
  • Nama domain, bukan alamat IP. Anda memerlukan sijil bagaimanapun juga, dan sijil tidak dikeluarkan untuk IP kosong.
  • Tiada `localhost`, dan tiada nama .local, .internal, .corp, .lan atau .test — pelayan kami tidak dapat mencapai rangkaian anda, dan nama yang menyelesai ke dalam rangkaian kami ialah tepat apa yang tidak boleh kami panggil.
  • Tiada kelayakan dalam URL (https://user:pass@…). Letakkan token anda sendiri dalam laluan atau parameter pertanyaan jika anda memerlukannya.
  • Setiap alamat yang diselesaikan oleh nama itu mesti awam — A dan AAAA kedua-duanya. Julat peribadi, gelung balik, link-local dan CGNAT ditolak, dan semakan diulang sebelum setiap penghantaran, jadi menghalakan rekod ke 127.0.0.1 kemudian pun tidak berkesan.
  • Pengalihan ialah kegagalan, bukan lompatan. Kami tidak mengikutinya: alamat yang anda berikan kepada kami telah disemak, yang berada dalam pengepala Location tidak.
Pengesahan dilakukan dua kali dengan sengaja — sekali semasa anda menyimpan URL, supaya salah taip dijawab serta-merta dan bukan melalui kegagalan penghantaran yang senyap, dan sekali sebelum setiap penghantaran, kerana pemilik domain boleh menghalakannya semula ke alamat dalaman pada bila-bila masa. Jika titik akhir anda berpindah, kemas kini kunci dahulu: URL yang ditolak tidak menghantar apa-apa dan tidak beratur.

Jawab dengan pantas

Mana-mana 2xx sudah memadai, dalam masa sepuluh saat — itulah keseluruhan tamat masa kami, termasuk sambungan. Jawab dahulu, lakukan kerja yang perlahan selepas itu; titik akhir yang menunggu pangkalan datanya sendiri sebelum membalas akhirnya akan direkodkan sebagai tamat masa dan dicuba semula, dan anda akan memproses peristiwa yang sama dua kali. Apa-apa yang lain — 4xx, 5xx, pengalihan, tergantung — dikira sebagai percubaan yang gagal dan kembali ke jadual di atas.

Penghantaran, secara jujur

Yang dijamin ialah mekanisme penghantarannya: tujuh percubaan dalam lebih kurang 31 jam, penghantaran semula secara manual daripada kawasan akaun anda, dan titik akhir invois yang sentiasa tahu status sebenar. Bina aliran anda supaya webhook yang tidak pernah tiba tidak merugikan anda — baca invois pada halaman terima kasih anda, atau selaraskan invois terbuka sekali sejam. Webhook ialah laluan pantas, bukan satu-satunya laluan.

A complete receiver#

Signature, deduplication and a fast answer, end to end.

The snippets above verify one signature. This is the whole endpoint: raw body, signature check, deduplication by event_id, a fast 2xx, and the one condition that is allowed to mark an order paid.

Node.js with Express. express.raw is the part people get wrong: express.json() hands you a parsed object, and bytes you re-serialise from it are not the bytes we signed.

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.

Rujukan status#

Setiap status invois dan pembayaran, dijelaskan.

Invois

StatusMaksudApa yang perlu dilakukan
pendingMenunggu pembayaran.Kekalkan pesanan terbuka.
paidDibayar sepenuhnya.Lepaskan barangan.
overpaidLebih tiba daripada diminta. Lebihan dikreditkan kepada anda sepenuhnya.Lepaskan barangan; bayar balik perbezaan jika anda mahu.
underpaidKurang tiba daripada diminta. Invois kekal terbuka dan mengekalkan alamatnya: pembeli boleh menambahnya ke tempat yang sama, dan paid_minor memberitahu berapa yang sudah masuk. Ia kekal boleh dibayar sepanjang baki hayatnya ditambah tempoh tangguh 24 jam selepas expires_at.Tunggu tambahan itu, atau selesaikan dengan pelanggan. Jangan lepaskan barangan — invois belum dibayar.
expiredTetingkap sudah ditutup, termasuk tempoh tangguh. Mungkin masih membawa wang: apa sahaja yang sampai kekal pada baki anda, dan paid_minor memberitahu berapa banyak.Tawarkan invois baharu. Jangan terima pembayaran ke alamat lama: sebaik sahaja invois luput, alamat itu kembali ke dalam kolam, dan pemindahan yang sangat lewat menjadi kes sokongan, bukan kredit automatik. Semak paid_minor sebelum memberitahu pelanggan bahawa tiada apa-apa diterima.
cancelledDibatalkan oleh anda. Alamat dilepaskan semula ke kolam.Tiada apa-apa.

Pembayaran

Kelihatan dalam kawasan akaun anda; berguna semasa menyokong pelanggan pertengahan pembayaran.

StatusMaksud
detectedDilihat di rantaian, menunggu pengesahan.
confirmedRangkaian mengesahkannya. Pengkreditan seterusnya.
creditedPada baki anda. Inilah masa webhook berlaku.
reviewDitahan untuk semakan tambahan — contohnya, syiling tiba pada alamat tanpa invois terbuka.
rejectedTidak dikreditkan. Sebabnya direkodkan.

Apabila pembayaran masuk ke `review`

Sesetengah deposit ditahan untuk semakan tambahan dan bukannya dikreditkan serta-merta: jumlah yang luar biasa besar, syiling yang tiba pada alamat tanpa invois terbuka, atau dua sumber blockchain yang kami tanya tidak sepakat tentang apa yang berlaku. Tiada apa yang hilang — wang itu menunggu keputusan, dan webhook dihantar sebaik keputusan dibuat, yang mungkin beberapa minit atau beberapa jam kemudian. Anggap panggilan balik yang tidak sampai pada pembayaran berstatus review sebagai perkara biasa, bukan kegagalan. Jika ia penting bagi sesuatu pesanan, tanya sokongan dan sebutkan tx_hash.

Jumlah#

Keluar dalam unit biasa, balik dalam unit terkecil.

Hantar jumlah dalam unit biasa syiling, sebagai rentetan"1.5" bermaksud satu setengah. Bukan nombor JSON dan bukan unit terkecil.

AsetPerpuluhanAnda hantaramount_minor dalam respons
TON9"1.5""1500000000"
USDT_TON6"1.5""1500000"

Rentetan dan bukan nombor, kerana nombor JSON ialah double IEEE-754 dan jumlah besar dalam nanoton tidak lagi muat di dalamnya dengan tepat. Lebih banyak tempat perpuluhan daripada yang ada pada syiling menghasilkan 422, bukan pembundaran senyap wang anda. Webhook berjalan sebaliknya: di sana amount, fee dan credited ialah integer dalam unit terkecil, kerana pihak itu dibaca oleh kod, bukan oleh manusia.

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

Had#

Minimum, maksimum, dan had kadar.

HadNilaiApabila dilanggar
Invois minimum0.1 TON · 3 USDT422
Invois maksimum7000 TON · 10000 USDT422
Invois sejam, setiap kedai60429
Invois terbuka serentak20, bertambah dengan setiap invois yang dibayar, sehingga 200429
Jangka hayat invois1 minit – 24 jam (lalai 2 jam)422
Permintaan API setiap kunci120 seminit429 + Retry-After

Minimum bukan birokrasi. Yuran kami adalah peratusan, tetapi mengutip pembayaran memerlukan kos tetap: memindahkan USDT keluar dari alamat penerimaan bermakna membiayainya dengan gas terlebih dahulu, daripada poket kami sendiri. Di bawah beberapa dolar yuran tidak menampung pengendalian, dan menerima pembayaran sedemikian bermakna mengkreditkan anda wang yang tidak ekonomik untuk dipindahkan.

Had maksimum bukan tentang peniaga besar — ia perangkap bagi kesilapan unit. Hantar "5000000" sedangkan maksud anda "5", dan jika tidak anda akan mendapat invois lima juta dolar: pembeli nampak angka yang mengarut lalu pergi. Pesanan sebenar tidak pernah mencecah siling ini; yang tersilap sentiasa mencecahnya. Kedua-dua siling ialah tetapan (invoice_max_ton, invoice_max_usdt) dan boleh dinaikkan untuk kedai anda — tanyakan sahaja.

Had sejam dan had invois terbuka kedua-duanya melindungi kolam alamat. Setiap invois terbuka memegang satu alamat penerimaan, dan gelung yang tidak terkawal pada satu laman jika tidak akan menghabiskan kolam untuk semua orang. Kedai baharu boleh memegang 20 invois terbuka serentak; elaun itu bertambah satu bagi setiap invois yang benar-benar telah dikutipnya, sehingga siling 200. underpaid dikira sebagai terbuka — ia masih memegang alamatnya, menunggu bakinya. Membatalkan invois yang ditinggalkan mengembalikan alamatnya serta-merta. Cubaan semula dengan idempotency_key yang sama tidak dikira terhadap had sejam.

Had permintaan ialah 120 seminit bagi setiap kunci API — dua panggilan sesaat, jauh melebihi mana-mana aliran pesanan sebenar. 429 membawa pengepala Retry-After dalam saat: tunggu selama itu dan bukannya mencuba semula dalam gelung ketat, yang hanya menolak tetingkap itu lebih jauh.

Ralat#

Kod status yang akan anda lihat sebenarnya.

Ralat kembali sebagai JSON, dalam dua bentuk. Apa sahaja yang diputuskan oleh kami atau oleh teras pemprosesan meletakkan pasangan {code, message} di bawah detail. Badan permintaan yang gagal pengesahan pula meletakkan senarai ralat medan di situ. Semak yang mana satu anda terima sebelum membaca detail.code — dan bercabang pada `code`, tidak sekali-kali pada `message`: perkataannya boleh berubah bila-bila masa, kodnya tidak.

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
    }
  ]
}
StatusBilaApa yang perlu dilakukan
401Kunci hilang, salah, atau dibatalkan.Semak pengepala. Keluarkan semula kunci jika ia telah dibatalkan.
404Tiada invois sedemikian, atau ia milik kedai lain.Semak id. Kedua-dua kes dijawab sama dengan sengaja, supaya id tidak boleh diselidik.
409Invois berada dalam keadaan yang melarang ini.Baca status semasanya dahulu.
422Permintaan salah bentuk, atau jumlah berada di luar had invois.Mesej menamakan kedua-dua nilai yang dihantar dan hadnya.
429Terlalu banyak invois pada jam ini, terlalu banyak terbuka serentak, atau terlalu banyak permintaan.Tunggu sehingga Retry-After tamat, kemudian cuba semula.
502Kami tidak dapat menghubungi teras pemprosesan.Cuba semula dengan idempotency key yang sama.

Kod

Bentuk bagi keputusan pihak kami ialah {"detail": {"code": …, "message": …}}. Inilah kod yang dikembalikan oleh API peniaga.

KodStatusMaksud
invalid_api_key401Kunci hilang, cacat bentuk, tidak dikenali atau dibatalkan. Keempat-empatnya menjawab serupa, jadi kunci tidak boleh diselidik.
not_found404Tiada objek sedemikian, atau ia milik kedai lain.
invalid_input422Permintaan tidak lulus pengesahan dalam teras — jumlah yang tidak sah, terlalu banyak tempat perpuluhan, jumlah di luar had invois.
conflict409Tindakan itu bercanggah dengan keadaan semasa, seperti membatalkan invois yang tidak lagi terbuka.
too_many_requests429Had kadar: invois sejam, invois terbuka, atau permintaan seminit. Retry-After memberitahu berapa lama perlu menunggu.
cbc_unreachable502Kami tidak dapat menghubungi teras pemprosesan. Cuba semula dengan idempotency_key yang sama.
webhook_url_rejected422Hanya semasa menyimpan kunci: URL webhook gagal semakan di atas. detail.reason menamakan peraturan yang mana — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials, dan seterusnya.

502 tidak bermakna invois tidak dicipta — permintaan mungkin telah berjaya dengan jawapan hilang dalam perjalanan pulang. Cuba semula dengan idempotency_key yang sama dan anda sama ada mendapat invois sedia ada atau yang baharu, tidak pernah dua.

Checkout: what the buyer sees#

The hosted payment page, and when to build your own.

payment_url points at https://paysell.me/pay/{invoice_id}. One page, no account, no login, mobile first, and nothing for you to build.

On the page

  • Your shop's name, the amount and the coin, large, with the invoice's description underneath.
  • A countdown to expires_at — plus the 24-hour grace period when the invoice 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.

Bayaran balik#

Cara memulangkan wang kepada pelanggan.

Bayaran balik dibuat melalui sokongan, bukan melalui panggilan API. Bayaran balik ialah pemindahan baharu ke alamat yang diberikan oleh seseorang, dan pemproses pembayaran yang menghantar wang kembali secara automatik atas panggilan API ialah pemproses pembayaran yang boleh dipaksa menghantar wang ke alamat penyerang. Jadi ia sengaja dibuat secara manual.

Untuk membayar balik pembeli, buka tiket sokongan daripada kawasan akaun anda dengan invoice_id atau tx_hash, jumlahnya, dan alamat untuk dihantar. Operator menyemak pembayaran, mengeluarkan wang daripada baki anda, dan menjawab dalam tiket yang sama. Jangkakan ini mengambil masa satu hari bekerja, bukan seminit.

Dua akibat yang berbaloi diambil kira semasa mereka bentuk. Lebihan bayaran dikreditkan kepada anda sepenuhnya — kami tidak menyimpan sesen pun daripadanya — jadi memulangkan perbezaan kepada pembeli yang menghantar terlalu banyak ialah keputusan anda dan mengikut laluan yang sama. Dan invois yang kurang dibayar bukan kes bayaran balik selagi ia masih terbuka: wang berada pada baki anda, alamat masih dipantau, dan pembeli boleh sekadar menambahnya. Hanya selepas tempoh tangguh, apabila invois menjadi expired dengan wang padanya, barulah ada keputusan untuk dibuat.

Ujian#

Cara menguji integrasi anda sebelum pelancaran.

Kunci di sini adalah kunci langsung: setiap kunci yang dikeluarkan ialah kunci sk_live_ terhadap teras pengeluaran dan mainnet TON. Tiada persekitaran ujian berasingan, dan itu ada kebaikannya: anda melalui laluan yang sama persis dengan laluan pesanan sebenar anda nanti.

Jadi ujilah seperti anda menguji apa-apa yang melibatkan wang sebenar: pada jumlah kecil. Cipta invois untuk jumlah minimum (0.1 TON atau 3 USDT), bayar daripada dompet anda sendiri, dan perhatikan keseluruhan laluan — halaman pembayaran, webhook, semakan tandatangan, pesanan anda bertukar kepada dibayar. Yuran tetap dikenakan, dan syiling benar-benar berpindah.

Bahagian yang boleh anda uji tanpa membelanjakan apa-apa: mencipta dan membaca invois, membatalkannya, 422 pada jumlah yang salah bentuk, 401 pada kunci yang salah, dan pengesahan tandatangan anda sendiri — tandatangani badan contoh dengan rahsia anda dan berikannya kepada pengendali anda sendiri. Yang benar-benar memerlukan pembayaran sebenar hanyalah langkah terakhir: webhook payment.credited yang sebenar.

Rancang integrasi supaya ia tidak bergantung pada sandbox atau pembayaran simulasi: laluan langsung lebih cepat — dan lebih jujur — untuk disahkan.

Anggap pesanan langsung pertama anda sebagai ujian sebenar: pilih jumlah yang kecil, biarkan invois terbuka dalam papan pemuka, dan semak baris pembayaran serta keadaan webhook sebelum anda mengarahkan pelanggan sebenar ke situ.

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.

Senarai semak sebelum pelancaran#

Sepuluh perkara untuk disemak sebelum pelancaran.

  • Kunci hanya di sisi pelayan, tidak sekali-kali dalam JavaScript pelayar.
  • Tandatangan webhook disahkan terhadap "{timestamp}.{raw_body}", dalam masa malar.
  • Penghantaran yang lebih lama daripada lima minit ditolak, dan jam pelayan berada pada NTP.
  • X-Paysell-Event-Id yang berulang tidak melakukan apa-apa pada kali kedua.
  • Webhook menjawab 2xx dalam masa sepuluh saat; kerja perlahan berlaku selepas itu.
  • URL webhook ialah domain https:// pada port 443, tanpa pengalihan di hadapannya.
  • Webhook yang terlepas boleh dihadapi: titik akhir invois dibaca pada halaman terima kasih atau semasa imbasan penyelarasan.
  • idempotency_key dijana sekali bagi setiap pesanan dan digunakan semula pada percubaan semula.
  • Jumlah keluar sebagai rentetan dalam unit biasa; angka webhook dibaca sebagai unit terkecil.
  • Alamat dipaparkan tepat seperti dikembalikan, tanpa diubah suai.
  • overpaid dan underpaid dikendalikan, bukan sekadar paid; expired mungkin masih membawa paid_minor.
  • Barang dilepaskan pada status: paid atau overpaid, tidak sekali-kali hanya kerana panggilan balik itu sampai.
  • 429 dikendalikan dengan menunggu sehingga Retry-After tamat, bukan dengan mencuba semula serta-merta.
  • Baki dibaca daripada kami, bukan dijejaki secara berasingan sebagai kebenaran.

Ada yang tidak jelas?

Jika halaman ini tidak menjawab soalan anda, itu adalah jurang dalam dokumentasi dan patut diberitahu kepada kami. Tulis dari kawasan akaun anda dan kami akan membaiki halaman itu, bukan sekadar jawapannya.