Δεχτείτε πληρωμές σε κρυπτονομίσματα
Η Paysell διακανονίζει TON και USDT στο δίκτυο TON. Δημιουργείτε ένα τιμολόγιο, σας δίνουμε έναν σύνδεσμο, και λαμβάνετε ένα υπογεγραμμένο callback μόλις τα χρήματα επιβεβαιωθούν στο blockchain και πιστωθούν στο υπόλοιπό σας.
Επισκόπηση#
Τι κάνει η Paysell, και τι δεν κάνει.
Η Paysell είναι πάροχος πληρωμών, όχι πορτοφόλι. Ποτέ δεν χειρίζεστε ιδιωτικά κλειδιά, δεν παρακολουθείτε το blockchain, ούτε αποφασίζετε πότε μια συναλλαγή είναι οριστική — αυτό το αναλαμβάνουμε εμείς.
Κάθε τιμολόγιο αποκτά τη δική του διεύθυνση παραλαβής. Όταν ένας αγοραστής το πληρώνει, περιμένουμε το δίκτυο να επιβεβαιώσει τη μεταφορά, αφαιρούμε την προμήθειά μας και πιστώνουμε το υπόλοιπο στο υπόλοιπό σας. Κάνετε ανάληψη σε όποια διεύθυνση θέλετε.
Πώς λειτουργεί μια πληρωμή#
Έξι βήματα, τα περισσότερα δικά μας.
Έξι βήματα, τα περισσότερα δικά μας:
- 1
Ο πελάτης σας κάνει κλικ στην πληρωμή
Ο διακομιστής σας καλεί το API μας με το ποσό και τη δική σας αναφορά παραγγελίας.
- 2
Δίνουμε μια διεύθυνση
Μια νέα διεύθυνση παραλαβής λαμβάνεται από μια προδημιουργημένη δεξαμενή και συνδέεται με αυτό το τιμολόγιο. Μια διεύθυνση ανήκει ακριβώς σε ένα ανοιχτό τιμολόγιο, κι έτσι αντιστοιχίζεται μια πληρωμή σε αυτό.
- 3
Ο πελάτης στέλνει τα νομίσματα
Σαρώνει τον κωδικό QR ή αντιγράφει τη διεύθυνση. Στείλτε τον στο
payment_urlπου επιστρέφουμε και η σελίδα αναλαμβάνει τα πάντα για εσάς — ποσό, διεύθυνση, QR, αντίστροφη μέτρηση, ζωντανή κατάσταση. - 4
Εντοπίζουμε τη μεταφορά
Ερωτώνται δύο ανεξάρτητες πηγές δεδομένων blockchain και οι απαντήσεις τους συγκρίνονται. Αν διαφωνούν, σταματάμε αντί να επιλέξουμε την πιο βολική απάντηση.
- 5
Περιμένουμε την οριστικοποίηση
Ένταξη στο masterchain συν τρία μπλοκ από πάνω. Περίπου δεκαπέντε δευτερόλεπτα — μια πληρωμή που φαίνεται διακανονισμένη και μετά εξαφανίζεται θα ήταν δική σας απώλεια, γι' αυτό δεν παίρνουμε αυτό το ρίσκο.
- 6
Πιστώθηκε, και ενημερώνεστε
Η προμήθεια αφαιρείται, το υπόλοιπο φτάνει στο υπόλοιπό σας, και ένα υπογεγραμμένο webhook πηγαίνει στον διακομιστή σας με το
order_idσας.
Από την πληρωμή στο callback: περίπου ένα λεπτό — περίπου δεκαπέντε δευτερόλεπτα επιβεβαιώσεων δικτύου, το υπόλοιπο είναι η σάρωσή μας παρακολουθούμενων διευθύνσεων.
Πού πάνε τα χρήματα#
Η προμήθεια, και πάνω σε τι υπολογίζεται.
Η προμήθεια είναι 0,2%, σταθερή για το κατάστημά σας τη στιγμή της εγγραφής του. Αν η τυπική τιμή αλλάξει αργότερα, η δική σας δεν αλλάζει — είναι γραμμένη σε κάθε τιμολόγιο ως αριθμός, όχι ως αναφορά σε μια ρύθμιση.
Η προμήθεια λαμβάνεται από αυτό που πραγματικά φτάνει, όχι από αυτό που ζητούσε το τιμολόγιο. Τιμολογήστε 5 USDT και λάβετε 20, η προμήθεια υπολογίζεται στα 20. Αν πληρωθεί λιγότερο, υπολογίζεται σε αυτό που ήρθε.
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Η υπερπληρωμή πιστώνεται εξ ολοκλήρου — δεν κρατάμε τη διαφορά. Η ελλιπής πληρωμή αφήνει το τιμολόγιο ανοιχτό ώστε ο αγοραστής να μπορεί να συμπληρώσει στην ίδια διεύθυνση.
Γρήγορο ξεκίνημα#
Πέντε λεπτά μέχρι το πρώτο σας τιμολόγιο.
Πέντε βήματα. Τα δύο είναι κλικ στην περιοχή λογαριασμού σας, το ένα είναι ένα μόνο αίτημα από τον διακομιστή σας, και τα δύο τελευταία γίνονται από μόνα τους.
- 1
Δημιουργήστε ένα κατάστημα
Στην περιοχή λογαριασμού σας. Αρχίζει να δέχεται πληρωμές αμέσως — χωρίς αναμονή για έλεγχο. Η επαλήθευση γίνεται ήσυχα στο παρασκήνιο και περιορίζει μόνο τις αναλήψεις, όχι τις εισερχόμενες πληρωμές.
- 2
Εκδώστε ένα κλειδί API
Το κατάστημά σας → Κλειδιά API → Νέο κλειδί. Το κλειδί και το μυστικό του webhook εμφανίζονται μία φορά και ποτέ ξανά. Φυλάξτε τα όπως θα φυλάγατε έναν κωδικό βάσης δεδομένων, και μην τα στείλετε ποτέ σε πρόγραμμα περιήγησης.
- 3
Δημιουργήστε ένα τιμολόγιο
Ένα αίτημα από τον διακομιστή σας, ένας σύνδεσμος πίσω. Και τα τέσσερα αποσπάσματα κώδικα παρακάτω στέλνουν ακριβώς το ίδιο πράγμα.
- 4
Στείλτε τον αγοραστή στο
payment_urlΑυτή είναι ολόκληρη η ολοκλήρωση αγοράς — ποσό, διεύθυνση, κωδικός QR, αντίστροφη μέτρηση, κατάσταση σε πραγματικό χρόνο — και δεν υπάρχει τίποτα να φτιάξετε. Δείτε Ολοκλήρωση αγοράς για το τι βλέπει πραγματικά ο αγοραστής.
- 5
Περιμένετε το webhook
Μόλις τα χρήματα επιβεβαιωθούν στην αλυσίδα και πιστωθούν, κάνουμε POST ένα υπογεγραμμένο συμβάν
payment.creditedστον διακομιστή σας. Επαληθεύστε την υπογραφή και μετά σημειώστε την παραγγελία ως πληρωμένη — αλλά μόνο όταν τοdata.statusείναιpaidήoverpaid. Δείτε 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"
}'Ανακατευθύνετε τον αγοραστή στο payment_url της απάντησης. Αυτό είναι όλο — το υπόλοιπο φτάνει ως 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.
Πιστοποίηση#
Το κλειδί API σας, και πώς χρησιμοποιείται.
Κάθε αίτημα μεταφέρει το κλειδί σας στην κεφαλίδα Authorization:
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxAΚάθε κλειδί που εκδίδεται εδώ ξεκινά με sk_live_. Το πρόθεμα sk_test_ υπάρχει μόνο σε μια εγκατάσταση που δείχνει στο δίκτυο δοκιμών, και τέτοια εγκατάσταση δεν προσφέρεται — δείτε Δοκιμές. Αποθηκεύουμε ένα μονόδρομο hash, όχι το ίδιο το κλειδί, οπότε κανείς, ούτε καν εμείς, δεν μπορεί να σας το δείξει ξανά. Το χάσατε; Εκδώστε ένα νέο και ανακαλέστε το παλιό.
Το κατάστημα προκύπτει από το κλειδί, γι' αυτό κανένα αίτημα δεν παίρνει ποτέ id καταστήματος. Ένα κλειδί μπορεί να ενεργήσει μόνο στο δικό του κατάστημα.
Η διαδρομή φέρει έκδοση: /api/merchant/v1/…. Μέσα σε μια έκδοση μόνο προσθέτουμε πεδία — τίποτα δεν μετονομάζεται και τίποτα δεν αλλάζει σιωπηλά νόημα. Μια αλλαγή που θα έσπαγε τον κώδικά σας παίρνει νέο πρόθεμα, /v2, και το /v1 συνεχίζει να λειτουργεί για ανακοινωμένο διάστημα.
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.
Δημιουργία τιμολογίου#
POST /api/merchant/v1/invoices
/api/merchant/v1/invoicesΣώμα αιτήματος
| Πεδίο | Τύπος | Υποχρεωτικό | Περιγραφή |
|---|---|---|---|
| asset | string | ναι | Είτε TON είτε USDT_TON. |
| amount | string | ναι | Κανονικές μονάδες του νομίσματος, ως συμβολοσειρά: "5" είναι 5 USDT. Όχι περισσότερα δεκαδικά από όσα έχει το νόμισμα. Δείτε Ποσά. |
| order_id | string | όχι | Η δική σας αναφορά, έως 200 χαρακτήρες. Επιστρέφει σε κάθε webhook — έτσι αντιστοιχίζετε μια πληρωμή σε παραγγελία. |
| description | string | όχι | Έως 1000 χαρακτήρες. Εμφανίζεται στον αγοραστή στη σελίδα πληρωμής. |
| ttl_minutes | number | όχι | Πόσο καιρό παραμένει πληρωτέο το τιμολόγιο, σε λεπτά. 1–1440· παραλείψτε το και ισχύει η προεπιλογή — 2 ώρες σήμερα. |
| idempotency_key | string | όχι | Έως 200 χαρακτήρες. Στείλτε την ίδια τιμή σε επανάληψη και θα λάβετε πίσω το ίδιο τιμολόγιο αντί για δεύτερο. Είναι πεδίο του σώματος, όχι η κεφαλίδα Idempotency-Key — αυτή η κεφαλίδα δεν διαβάζεται εδώ. |
Απάντηση · 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"
}Πώς να το χρησιμοποιήσετε στην παραγγελία σας
| Πεδίο | Τι να κάνετε με αυτό |
|---|---|
| invoice_id | Αποθηκεύστε το με την παραγγελία σας. Είναι αυτό που ταυτοποιεί την πληρωμή παντού αλλού. |
| payment_url | Ανακατευθύνετε τον αγοραστή εδώ. Δεν χρειάζεται να χτίσετε τίποτα άλλο. |
| address | Μόνο αν χτίζετε τη δική σας ολοκλήρωση αγοράς. Εμφανίστε την ακριβώς όπως δόθηκε — δείτε την προειδοποίηση παρακάτω. |
| amount | Το ποσό σε κανονικές μονάδες, ακριβώς όπως το στείλατε. Αυτό εμφανίζετε. |
| amount_minor | Το ίδιο ποσό ως ακέραιος στην ελάχιστη μονάδα. Με αυτό υπολογίζετε. |
| expires_at | Εμφανίστε αντίστροφη μέτρηση. Μετά την παρέλευσή του η διεύθυνση παύει να παρακολουθείται για αυτό το τιμολόγιο. |
| status | Εδώ είναι πάντα pending. Οι πραγματικές αλλαγές φτάνουν μέσω webhook. |
UQ… στο mainnet, 0Q… στο testnet). Η μετατροπή, η ομορφοποίηση ή η αντικατάστασή της με άλλη κωδικοποίηση της ίδιας διεύθυνσης θα κάνει τα νομίσματα που στέλνονται σε ένα πορτοφόλι που δεν έχει ακόμα αναπτυχθεί να επιστρέψουν στον αποστολέα.Ανάγνωση τιμολογίου#
GET /api/merchant/v1/invoices/{invoice_id}
/api/merchant/v1/invoices/{invoice_id}Ίδια μορφή με παραπάνω, με τα status, paid και paid_minor να αντικατοπτρίζουν το παρόν: το paid είναι πόσα έχουν φτάσει σε κανονικές μονάδες, το paid_minor το ίδιο ως ακέραιος στην ελάχιστη μονάδα. Χρήσιμο ως εναλλακτική όταν χάθηκε ένα webhook, ή σε σελίδα ευχαριστίας.
Ερωτήστε το το πολύ κάθε λίγα δευτερόλεπτα, και αντιμετωπίστε τα webhooks ως το κύριο κανάλι. Τιμολόγια που ανήκουν σε άλλο κατάστημα απαντούν με 404 — όχι 403, ώστε ένα id να μην μπορεί να ελεγχθεί για ύπαρξη.
Ακύρωση τιμολογίου#
POST /api/merchant/v1/invoices/{invoice_id}/cancel
/api/merchant/v1/invoices/{invoice_id}/cancelΚλείνει ένα τιμολόγιο που είναι ακόμη ανοιχτό — pending ή underpaid — και απελευθερώνει τη διεύθυνσή του. Χρησιμοποιήστε το όταν ο πελάτης εγκαταλείπει την ολοκλήρωση αγοράς: οι διευθύνσεις είναι πεπερασμένος πόρος, και η επιστροφή τους διατηρεί τη δεξαμενή υγιή.
Ένα τιμολόγιο που δεν είναι πλέον ανοιχτό απαντά με 409. Η ακύρωση ενός τιμολογίου σε underpaid δεν επιστρέφει νομίσματα σε κανέναν: τα χρήματα που έχουν ήδη πιστωθεί παραμένουν στο υπόλοιπό σας, και το μόνο που κλείνει είναι η αποδοχή συμπλήρωσης.
Webhooks#
Τι φτάνει, και πώς να το επαληθεύσετε.
Ορίστε ένα URL webhook όταν δημιουργείτε το κλειδί. Κάνουμε POST εκεί όταν μια πληρωμή πιστώνεται — και όταν απορρίπτεται μια κατάθεση που κρατήθηκε για πρόσθετο έλεγχο. Κάθε παράδοση είναι υπογεγραμμένη και συνεχίζουμε τις επαναλήψεις για περίπου μιάμιση ημέρα μέχρι να απαντήσετε 2xx. Παραδίδετε το εμπόρευμα με status: paid ή overpaid — όχι με την απλή άφιξη της κλήσης.
Συμβάντα
| Συμβάν | Πότε | Τι περιέχει το σώμα |
|---|---|---|
| payment.credited | Η μεταφορά επιβεβαιώθηκε στην αλυσίδα, κρατήθηκε η προμήθειά μας και τα υπόλοιπα βρίσκονται στο υπόλοιπό σας. | Τα πεδία που παρατίθενται παρακάτω. |
| payment.rejected | Μια κατάθεση που κρατήθηκε για πρόσθετο έλεγχο (δείτε Αναφορά κατάστασης) απορρίφθηκε. Τα χρήματα δεν θα φτάσουν στο υπόλοιπό σας. | invoice_id, order_id, asset, amount, tx_hash και reason. Μην παραδώσετε το εμπόρευμα· αν το τιμολόγιο ήταν ήδη paid από προγενέστερη μεταφορά, το συμβάν αφορά την επιπλέον κατάθεση και όχι εκείνη την πληρωμή. |
Τι φτάνει
{
"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"
}
}Αντιστοίχιση πεδίων
| Πεδίο | Σημασία |
|---|---|
| event_id | Μοναδικό ανά συμβάν· επίσης στην κεφαλίδα X-Paysell-Event-Id. Αποθηκεύστε το και αγνοήστε τις επαναλήψεις — δείτε παρακάτω. |
| data.order_id | Η αναφορά σας. Αναζητήστε την παραγγελία σας με αυτό. |
| data.amount | Τι έστειλε ο αγοραστής σε αυτή τη μεταφορά, στην ελάχιστη μονάδα — σε αντίθεση με το API, που δέχεται κανονικές μονάδες. |
| data.fee | Τι κρατήσαμε, στην ελάχιστη μονάδα. |
| data.credited | Τι έφτασε στο υπόλοιπό σας: amount − fee, στην ελάχιστη μονάδα. |
| data.paid_minor | Σύνολο όσων έχουν ληφθεί σε αυτό το τιμολόγιο μέχρι τώρα, στην ελάχιστη μονάδα. Το πεδίο που μετράει στο underpaid: η κατάσταση λέει ότι έφτασαν λιγότερα, αυτό λέει πόσο λιγότερα. |
| data.asset | Το νόμισμα που πραγματικά έφτασε. Όχι κατ' ανάγκη εκείνο που ζητούσε το τιμολόγιο. |
| data.asset_mismatch | Υπάρχει, και είναι true, μόνο όταν το νόμισμα που έφτασε δεν είναι εκείνο του τιμολογίου. Τα χρήματα πιστώνονται σε εσάς, αλλά το τιμολόγιο μένει απλήρωτο και το status δεν γίνεται ποτέ paid. |
| data.invoice_asset | Έρχεται μαζί με το asset_mismatch: το νόμισμα που ζητά πραγματικά το τιμολόγιο. |
| data.status | Η τωρινή κατάσταση του τιμολογίου: pending, underpaid, paid, overpaid ή expired. Συγκρίνετε με αυτό που περιμένατε. |
| data.tx_hash | Η συναλλαγή on-chain, για τα αρχεία σας και την υποστήριξη. |
Κεφαλίδες σε κάθε παράδοση
X-Paysell-Event: payment.credited
X-Paysell-Event-Id: 99f74f58-efbb-4af1-b0a3-76b0073f9e6b
X-Paysell-Timestamp: 1789000000
X-Paysell-Signature: sha256=6f1c0e6a…| Κεφαλίδα | Σημασία |
|---|---|
| X-Paysell-Event | Ο τύπος του συμβάντος: payment.credited ή payment.rejected. |
| X-Paysell-Event-Id | Μοναδικό ανά συμβάν. Αυτή είναι η τιμή για την αποδιπλοποίηση. |
| X-Paysell-Timestamp | Πότε υπογράψαμε, σε δευτερόλεπτα unix. Αποτελεί μέρος της υπογεγραμμένης συμβολοσειράς. |
| X-Paysell-Signature | sha256= ακολουθούμενο από το HMAC σε δεκαεξαδική μορφή. Δείτε παρακάτω. |
Επαλήθευση της υπογραφής
Κάθε αίτημα υπογράφεται με το μυστικό webhook που εμφανίζεται μία μόνο φορά, όταν δημιουργήσατε το κλειδί. Η υπογραφή είναι HMAC-SHA256(secret, "{timestamp}.{raw_body}") — η χρονοσήμανση από το X-Paysell-Timestamp, μια κυριολεκτική τελεία, και μετά τα bytes του σώματος. Ελέγξτε την πριν ενεργήσετε: χωρίς αυτό, όποιος μάθει το URL σας μπορεί να σας παραδώσει μια πληρωμένη παραγγελία.
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)
}Υπογράψτε τα ακατέργαστα bytes του σώματος, ακριβώς όπως λήφθηκαν. Αν αναλύσετε το JSON και το επανασειριοποιήσετε, τα bytes αλλάζουν — σειρά κλειδιών, κενά — και η υπογραφή δεν θα ταιριάζει. Συγκρίνετε σε σταθερό χρόνο (hmac.compare_digest, crypto.timingSafeEqual): ένα απλό == επιστρέφει ταχύτερα όταν το πρώτο byte είναι λάθος, και αυτή η διαφορά αρκεί για να μαντέψει κανείς μια υπογραφή byte προς byte.
Το παράθυρο της χρονοσήμανσης
Απορρίψτε οτιδήποτε έχει χρονοσήμανση που απέχει περισσότερο από πέντε λεπτά από το δικό σας ρολόι, προς οποιαδήποτε κατεύθυνση. Η χρονοσήμανση βρίσκεται μέσα στην υπογεγραμμένη συμβολοσειρά ακριβώς για να μην μπορεί να αλλαχτεί χωρίς να σπάσει η υπογραφή· το παράθυρο είναι αυτό που το μετατρέπει σε προστασία. Χωρίς αυτό, ένα αίτημα που καταγράφηκε μία φορά παραμένει έγκυρο για πάντα και μπορεί να αναπαραχθεί οποιαδήποτε στιγμή — η υπογραφή από μόνη της δεν λήγει ποτέ. Κρατήστε το ρολόι του διακομιστή σας συγχρονισμένο με NTP, αλλιώς αυτός ο έλεγχος θα αρχίσει να απορρίπτει σωστές παραδόσεις.
Διπλότυπα
Το ίδιο συμβάν μπορεί να φτάσει περισσότερες από μία φορές. Αυτό δεν είναι σφάλμα: επαναλαμβάνουμε μέχρι να απαντήσετε 2xx, και μια παράδοση που πέτυχε αλλά η απάντησή της δεν μας έφτασε ποτέ στέλνεται ξανά. Καταγράψτε το X-Paysell-Event-Id (έρχεται επίσης ως event_id μέσα στο σώμα) και κάντε τη δεύτερη άφιξη να μην κάνει τίποτα.
Επαναλήψεις
Η πρώτη απόπειρα φεύγει μόλις πιστωθεί η πληρωμή. Αν αποτύχει — λήξη χρόνου, άρνηση σύνδεσης, σφάλμα TLS, ανακατεύθυνση, ή οποιαδήποτε κατάσταση εκτός 2xx — επαναλαμβάνουμε με σταθερό πρόγραμμα:
1 λεπτό → 5 λεπτά → 15 λεπτά → 1 ώρα → 6 ώρες → 24 ώρεςΕπτά απόπειρες συνολικά, απλωμένες σε περίπου 31 ώρες. Οι πρώτες είναι κοντά η μία στην άλλη επειδή η συνήθης αιτία είναι ένας παραλήπτης που επανεκκινούσε και έχει ήδη επανέλθει· οι τελευταίες είναι αραιές επειδή το σφυροκόπημα ενός διακομιστή που είναι εκτός λειτουργίας μία ημέρα δεν βοηθά κανέναν.
Μετά την τελευταία απόπειρα η παράδοση σημειώνεται ως dropped και σταματάμε από μόνοι μας. Δεν χάνεται: η γραμμή της πληρωμής στην περιοχή λογαριασμού σας δείχνει την κατάσταση, το πλήθος των αποπειρών και την κατηγορία του σφάλματος, με ένα κουμπί Αποστολή ξανά που ξεκινά μια νέα σειρά και των επτά αποπειρών. Η άλλη σας διέξοδος είναι το GET /api/merchant/v1/invoices/{invoice_id} — το τιμολόγιο γνωρίζει πάντα τη δική του κατάσταση.
Πώς πρέπει να μοιάζει ένα URL webhook
Το URL ελέγχεται όταν το αποθηκεύετε, και ξανά πριν από κάθε μεμονωμένη παράδοση. Ένα URL που αποτυγχάνει στον έλεγχο λαμβάνει 422 με code: "webhook_url_rejected" κατά την αποθήκευση, και σημειώνει την παράδοση ως failed — χωρίς επαναλήψεις — αν αρχίσει να αποτυγχάνει αργότερα. Οι κανόνες:
- Μόνο `https://`, και θύρα 443. Ένα webhook μεταφέρει στοιχεία πληρωμής· σε απλό http είναι αναγνώσιμα από οποιονδήποτε βρίσκεται στη διαδρομή.
- Όνομα τομέα, όχι διεύθυνση IP. Χρειάζεστε ούτως ή άλλως πιστοποιητικό, και δεν εκδίδονται πιστοποιητικά για σκέτες IP.
- Κανένα `localhost`, και κανένα όνομα
.local,.internal,.corp,.lanή.test— οι διακομιστές μας δεν μπορούν να φτάσουν το δίκτυό σας, και ένα όνομα που επιλύεται μέσα στο δικό μας είναι ακριβώς αυτό που δεν πρέπει να καλέσουμε. - Κανένα διαπιστευτήριο μέσα στο URL (
https://user:pass@…). Βάλτε το δικό σας token στη διαδρομή ή σε παράμετρο ερωτήματος αν χρειάζεστε ένα. - Κάθε διεύθυνση στην οποία επιλύεται το όνομα πρέπει να είναι δημόσια — τόσο A όσο και AAAA. Ιδιωτικά, loopback, link-local και CGNAT εύρη απορρίπτονται, και ο έλεγχος επαναλαμβάνεται πριν από κάθε παράδοση, οπότε το να στρέψετε αργότερα την εγγραφή στο
127.0.0.1επίσης δεν λειτουργεί. - Μια ανακατεύθυνση είναι αποτυχία, όχι ενδιάμεσο βήμα. Δεν τις ακολουθούμε: η διεύθυνση που μας δώσατε ελέγχθηκε, εκείνη σε μια κεφαλίδα
Locationόχι.
Απαντήστε γρήγορα
Οποιοδήποτε 2xx αρκεί, εντός δέκα δευτερολέπτων — αυτό είναι όλο το χρονικό μας περιθώριο, μαζί με τη σύνδεση. Απαντήστε πρώτα, κάντε την αργή δουλειά μετά· ένα endpoint που περιμένει τη δική του βάση δεδομένων πριν απαντήσει θα καταγραφεί τελικά ως λήξη χρόνου και θα γίνει επανάληψη, και θα επεξεργαστείτε το ίδιο συμβάν δύο φορές. Οτιδήποτε άλλο — ένα 4xx, ένα 5xx, μια ανακατεύθυνση, ένα κόλλημα — μετράει ως αποτυχημένη απόπειρα και επιστρέφει στο παραπάνω πρόγραμμα.
Η παράδοση, με ειλικρίνεια
Αυτό που εγγυάται είναι ο μηχανισμός παράδοσης: επτά απόπειρες σε περίπου 31 ώρες, χειροκίνητη επαναποστολή από την περιοχή λογαριασμού σας, και ένα endpoint τιμολογίου που γνωρίζει πάντα την πραγματική κατάσταση. Σχεδιάστε τη ροή έτσι ώστε ένα webhook που δεν φτάνει ποτέ να μη σας κοστίζει τίποτα — διαβάστε το τιμολόγιο στη σελίδα ευχαριστιών σας, ή συμφωνήστε τα ανοιχτά τιμολόγια μία φορά την ώρα. Τα webhooks είναι η γρήγορη διαδρομή, όχι η μόνη διαδρομή.
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.
Αναφορά κατάστασης#
Όλες οι καταστάσεις τιμολογίων και πληρωμών, εξηγημένες.
Τιμολόγιο
| Κατάσταση | Σημασία | Τι να κάνετε |
|---|---|---|
| pending | Αναμονή πληρωμής. | Κρατήστε την παραγγελία ανοιχτή. |
| paid | Πληρώθηκε πλήρως. | Παραδώστε τα αγαθά. |
| overpaid | Ήρθε περισσότερο από όσο ζητήθηκε. Το πλεόνασμα πιστώνεται σε εσάς εξ ολοκλήρου. | Παραδώστε τα αγαθά· επιστρέψτε τη διαφορά αν θέλετε. |
| underpaid | Ήρθε λιγότερο από όσο ζητήθηκε. Το τιμολόγιο παραμένει ανοιχτό και κρατά τη διεύθυνσή του: ο αγοραστής μπορεί να το συμπληρώσει στο ίδιο σημείο, και το paid_minor λέει πόσα έχουν ήδη μπει. Παραμένει πληρωτέο για το υπόλοιπο της ζωής του συν μια περίοδο χάριτος 24 ωρών μετά το expires_at. | Περιμένετε τη συμπλήρωση, ή διευθετήστε με τον πελάτη. Μην παραδώσετε τα αγαθά — το τιμολόγιο δεν είναι πληρωμένο. |
| expired | Το παράθυρο έκλεισε, μαζί με την περίοδο χάριτος. Μπορεί να κρατά ακόμη χρήματα: ό,τι έφτασε έμεινε στο υπόλοιπό σας, και το paid_minor λέει πόσα. | Προσφέρετε νέο τιμολόγιο. Μην αποδέχεστε πληρωμή στην παλιά διεύθυνση: μόλις λήξει ένα τιμολόγιο, η διεύθυνση επιστρέφει στη δεξαμενή, και μια πολύ καθυστερημένη μεταφορά είναι υπόθεση υποστήριξης και όχι αυτόματη πίστωση. Ελέγξτε το paid_minor πριν πείτε στον πελάτη ότι δεν λήφθηκε τίποτα. |
| cancelled | Ακυρώθηκε από εσάς. Η διεύθυνση επιστρέφει στη δεξαμενή. | Τίποτα. |
Πληρωμή
Ορατό στην περιοχή λογαριασμού σας· χρήσιμο κατά την υποστήριξη πελάτη εν μέσω πληρωμής.
| Κατάσταση | Σημασία |
|---|---|
| detected | Εντοπίστηκε on-chain, αναμονή επιβεβαιώσεων. |
| confirmed | Το δίκτυο το επιβεβαίωσε. Ακολουθεί πίστωση. |
| credited | Στο υπόλοιπό σας. Αυτή είναι η στιγμή που ενεργοποιείται το webhook. |
| review | Κρατήθηκε για πρόσθετο έλεγχο — για παράδειγμα, νομίσματα που φτάνουν σε διεύθυνση χωρίς ανοιχτό τιμολόγιο. |
| rejected | Δεν πιστώθηκε. Ο λόγος καταγράφεται. |
Όταν μια πληρωμή πηγαίνει σε `review`
Ορισμένες καταθέσεις κρατούνται για πρόσθετο έλεγχο αντί να πιστωθούν αμέσως: ένα ασυνήθιστα μεγάλο ποσό, νομίσματα που φτάνουν σε διεύθυνση χωρίς ανοιχτό τιμολόγιο, ή οι δύο πηγές blockchain που ρωτάμε να διαφωνούν για το τι συνέβη. Τίποτα δεν χάνεται — τα χρήματα περιμένουν μια απόφαση και το webhook ενεργοποιείται μόλις υπάρξει, κάτι που μπορεί να συμβεί λεπτά ή ώρες αργότερα. Αντιμετωπίστε την απουσία κλήσης σε μια πληρωμή που εμφανίζεται ως review ως κάτι φυσιολογικό και όχι ως αποτυχία. Αν έχει σημασία για μια παραγγελία, ρωτήστε την υποστήριξη αναφέροντας το tx_hash.
Ποσά#
Προς τα έξω κανονικές μονάδες, πίσω οι ελάχιστες.
Στέλνετε τα ποσά στις κανονικές μονάδες του νομίσματος, ως συμβολοσειρά — "1.5" σημαίνει ενάμισι. Όχι αριθμός JSON και όχι η ελάχιστη μονάδα.
| Περιουσιακό στοιχείο | Δεκαδικά | Εσείς στέλνετε | amount_minor στην απάντηση |
|---|---|---|---|
| TON | 9 | "1.5" | "1500000000" |
| USDT_TON | 6 | "1.5" | "1500000" |
Συμβολοσειρά και όχι αριθμός, επειδή οι αριθμοί JSON είναι IEEE-754 doubles και ένα μεγάλο ποσό σε nanoton παύει να αναπαρίσταται ακριβώς μέσα τους. Περισσότερα δεκαδικά από όσα έχει το νόμισμα δίνουν 422, ποτέ σιωπηλή στρογγυλοποίηση των χρημάτων σας. Στα webhooks ισχύει το αντίστροφο: εκεί τα amount, fee και credited είναι ακέραιοι στην ελάχιστη μονάδα, γιατί εκείνη την πλευρά τη διαβάζει κώδικας, όχι άνθρωπος.
// 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Όρια#
Ελάχιστα, μέγιστα, και όρια ρυθμού.
| Όριο | Τιμή | Σε παραβίαση |
|---|---|---|
| Ελάχιστο τιμολόγιο | 0.1 TON · 3 USDT | 422 |
| Μέγιστο τιμολόγιο | 7000 TON · 10000 USDT | 422 |
| Τιμολόγια ανά ώρα, ανά κατάστημα | 60 | 429 |
| Ανοιχτά τιμολόγια ταυτόχρονα | 20, αυξανόμενα με κάθε πληρωμένο τιμολόγιο, έως 200 | 429 |
| Διάρκεια ζωής τιμολογίου | 1 λεπτό – 24 ώρες (προεπιλογή 2 ώρες) | 422 |
| Αιτήματα API ανά κλειδί | 120 ανά λεπτό | 429 + Retry-After |
Το ελάχιστο δεν είναι γραφειοκρατία. Η προμήθειά μας είναι ποσοστό, αλλά η είσπραξη μιας πληρωμής κοστίζει ένα σταθερό ποσό: η μετακίνηση USDT από μια διεύθυνση παραλαβής σημαίνει χρηματοδότησή της με gas πρώτα, από την τσέπη μας. Κάτω από μερικά δολάρια η προμήθεια δεν καλύπτει τον χειρισμό, και η αποδοχή μιας τέτοιας πληρωμής θα σήμαινε να σας πιστώσουμε χρήματα που δεν συμφέρει να μετακινηθούν.
Το μέγιστο δεν αφορά τα μεγάλα καταστήματα — είναι παγίδα για το λάθος στις μονάδες. Στείλτε "5000000" εκεί που εννοούσατε "5" και διαφορετικά θα προέκυπτε τιμολόγιο πέντε εκατομμυρίων δολαρίων: ο αγοραστής βλέπει παράλογο ποσό και φεύγει. Μια πραγματική παραγγελία δεν φτάνει ποτέ σε αυτό το ταβάνι· μια λανθασμένη πάντα. Και τα δύο ταβάνια είναι ρυθμίσεις (invoice_max_ton, invoice_max_usdt) και μπορούν να ανέβουν για το κατάστημά σας — ζητήστε το.
Το ωριαίο όριο και το όριο ανοιχτών τιμολογίων προστατεύουν και τα δύο τη δεξαμενή διευθύνσεων. Κάθε ανοιχτό τιμολόγιο κατέχει μια διεύθυνση παραλαβής, και ένας ανεξέλεγκτος βρόχος σε έναν ιστότοπο θα εξαντλούσε αλλιώς τη δεξαμενή για όλους τους άλλους. Ένα νέο κατάστημα μπορεί να κρατά 20 τιμολόγια ανοιχτά ταυτόχρονα· το περιθώριο αυξάνεται κατά ένα για κάθε τιμολόγιο που έχει πραγματικά εισπράξει, έως ένα ταβάνι 200. Το underpaid μετράει ως ανοιχτό — κρατά ακόμη τη διεύθυνσή του, περιμένοντας τα υπόλοιπα. Η ακύρωση ενός εγκαταλελειμμένου τιμολογίου επιστρέφει τη διεύθυνσή του αμέσως. Επαναλήψεις με το ίδιο idempotency_key δεν προσμετρώνται στο ωριαίο όριο.
Το όριο αιτημάτων είναι 120 ανά λεπτό ανά κλειδί API — δύο κλήσεις το δευτερόλεπτο, πολύ πάνω από οποιαδήποτε πραγματική ροή παραγγελιών. Ένα 429 φέρει κεφαλίδα Retry-After σε δευτερόλεπτα: περιμένετε τόσο αντί να επαναλαμβάνετε σε στενό βρόχο, κάτι που απλώς σπρώχνει το παράθυρο πιο μακριά.
Σφάλματα#
Οι κωδικοί κατάστασης που θα δείτε πραγματικά.
Τα σφάλματα επιστρέφουν ως JSON, σε δύο μορφές. Οτιδήποτε αποφασίζουμε εμείς ή ο πυρήνας επεξεργασίας βάζει ένα ζεύγος {code, message} κάτω από το detail. Ένα σώμα αιτήματος που δεν περνά την επικύρωση βάζει εκεί αντ' αυτού μια λίστα σφαλμάτων πεδίων. Ελέγξτε ποια από τις δύο λάβατε πριν διαβάσετε το detail.code — και διακλαδίζετε με βάση το `code`, ποτέ με βάση το `message`: η διατύπωση μπορεί να αλλάξει οποιαδήποτε στιγμή, ο κωδικός όχι.
{
"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
}
]
}| Κατάσταση | Πότε | Τι να κάνετε |
|---|---|---|
| 401 | Το κλειδί λείπει, είναι λάθος, ή έχει ανακληθεί. | Ελέγξτε την κεφαλίδα. Επανεκδώστε το κλειδί αν ανακλήθηκε. |
| 404 | Δεν υπάρχει τέτοιο τιμολόγιο, ή ανήκει σε άλλο κατάστημα. | Ελέγξτε το id. Οι δύο περιπτώσεις απαντούν όμοια σκόπιμα, ώστε να μην μπορεί να ανιχνευθεί ένα id. |
| 409 | Το τιμολόγιο βρίσκεται σε κατάσταση που το απαγορεύει. | Διαβάστε πρώτα την τρέχουσα κατάστασή του. |
| 422 | Το αίτημα είναι λανθασμένο ή το ποσό βρίσκεται εκτός των ορίων του τιμολογίου. | Το μήνυμα ονομάζει τόσο την τιμή που στάλθηκε όσο και το όριο. |
| 429 | Πάρα πολλά τιμολόγια αυτή την ώρα, πάρα πολλά ανοιχτά ταυτόχρονα, ή πάρα πολλά αιτήματα. | Περιμένετε να περάσει το Retry-After, και μετά δοκιμάστε ξανά. |
| 502 | Δεν μπορέσαμε να φτάσουμε τον πυρήνα επεξεργασίας. | Δοκιμάστε ξανά με το ίδιο κλειδί ιδεμποτεντίας. |
Κωδικοί
Η μορφή που αποφασίζουμε εμείς είναι {"detail": {"code": …, "message": …}}. Αυτοί είναι οι κωδικοί που επιστρέφει το API εμπόρου.
| Κωδικός | Κατάσταση | Σημασία |
|---|---|---|
| invalid_api_key | 401 | Το κλειδί λείπει, είναι λανθασμένα διαμορφωμένο, άγνωστο ή ανακλημένο. Και οι τέσσερις περιπτώσεις απαντούν όμοια, ώστε να μην μπορεί να ανιχνευθεί ένα κλειδί. |
| not_found | 404 | Δεν υπάρχει τέτοιο αντικείμενο, ή ανήκει σε άλλο κατάστημα. |
| invalid_input | 422 | Το αίτημα δεν πέρασε την επικύρωση στον πυρήνα — λανθασμένο ποσό, πάρα πολλά δεκαδικά ψηφία, ποσό εκτός των ορίων του τιμολογίου. |
| conflict | 409 | Η ενέργεια αντιβαίνει στην τρέχουσα κατάσταση, όπως η ακύρωση ενός τιμολογίου που δεν είναι πλέον ανοιχτό. |
| too_many_requests | 429 | Ένα όριο ρυθμού: τιμολόγια ανά ώρα, ανοιχτά τιμολόγια, ή αιτήματα ανά λεπτό. Το Retry-After λέει πόσο να περιμένετε. |
| cbc_unreachable | 502 | Δεν μπορέσαμε να φτάσουμε τον πυρήνα επεξεργασίας. Δοκιμάστε ξανά με το ίδιο idempotency_key. |
| webhook_url_rejected | 422 | Μόνο κατά την αποθήκευση ενός κλειδιού: το URL webhook απέτυχε στους παραπάνω ελέγχους. Το detail.reason ονομάζει ποιος κανόνας — scheme, port, ip_literal, local_hostname, private_address, dns_error, credentials, και ούτω καθεξής. |
Ένα 502 δεν σημαίνει ότι το τιμολόγιο δεν δημιουργήθηκε — το αίτημα μπορεί να πέρασε με την απάντηση χαμένη στον δρόμο επιστροφής. Δοκιμάστε ξανά με το ίδιο idempotency_key και θα λάβετε είτε το υπάρχον τιμολόγιο είτε ένα νέο, ποτέ δύο.
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.
Επιστροφές χρημάτων#
Πώς να επιστρέψετε χρήματα σε πελάτη.
Οι επιστροφές γίνονται μέσω της υποστήριξης, όχι με κλήση API. Μια επιστροφή είναι μια νέα μεταφορά σε διεύθυνση που έδωσε κάποιος άνθρωπος, και ένας επεξεργαστής πληρωμών που στέλνει χρήματα πίσω αυτόματα με μια κλήση API είναι ένας επεξεργαστής πληρωμών που μπορεί να αναγκαστεί να στείλει χρήματα στη διεύθυνση ενός επιτιθέμενου. Γι' αυτό είναι σκόπιμα χειροκίνητη.
Για να επιστρέψετε χρήματα σε έναν αγοραστή, ανοίξτε αίτημα υποστήριξης από την περιοχή λογαριασμού σας με το invoice_id ή το tx_hash, το ποσό, και τη διεύθυνση αποστολής. Ένας χειριστής ελέγχει την πληρωμή, βγάζει τα χρήματα από το υπόλοιπό σας, και απαντά στο ίδιο αίτημα. Περιμένετε ότι αυτό θα πάρει μια εργάσιμη ημέρα, όχι ένα λεπτό.
Δύο συνέπειες που αξίζει να λάβετε υπόψη στον σχεδιασμό. Η υπερπληρωμή πιστώνεται σε εσάς εξ ολοκλήρου — δεν κρατάμε τίποτα από αυτήν — οπότε η επιστροφή της διαφοράς σε έναν αγοραστή που έστειλε παραπάνω είναι δική σας απόφαση και ακολουθεί την ίδια διαδρομή. Και ένα υποπληρωμένο τιμολόγιο δεν είναι περίπτωση επιστροφής όσο παραμένει ανοιχτό: τα χρήματα είναι στο υπόλοιπό σας, η διεύθυνση παρακολουθείται ακόμη, και ο αγοραστής μπορεί απλώς να το συμπληρώσει. Μόνο μετά την περίοδο χάριτος, όταν το τιμολόγιο γίνεται expired με χρήματα πάνω του, υπάρχει απόφαση να παρθεί.
Δοκιμές#
Πώς να δοκιμάσετε την ενσωμάτωσή σας πριν τη δημοσίευση.
Τα κλειδιά εδώ είναι live: κάθε κλειδί που εκδίδεται είναι κλειδί sk_live_ προς τον πυρήνα παραγωγής και το mainnet του TON. Δεν υπάρχει ξεχωριστό περιβάλλον δοκιμών, κάτι που έχει και ένα πλεονέκτημα: δοκιμάζετε ακριβώς τη διαδρομή που θα ακολουθήσουν οι πραγματικές παραγγελίες σας.
Οπότε δοκιμάστε όπως θα δοκιμάζατε οτιδήποτε αγγίζει πραγματικά χρήματα: με μικρά ποσά. Δημιουργήστε ένα τιμολόγιο για το ελάχιστο (0.1 TON ή 3 USDT), πληρώστε το από το δικό σας πορτοφόλι, και παρακολουθήστε όλη τη διαδρομή — τη σελίδα πληρωμής, το webhook, τον έλεγχο υπογραφής, την παραγγελία σας να γίνεται πληρωμένη. Η προμήθεια ισχύει, και τα νομίσματα κινούνται πραγματικά.
Τα μέρη που μπορείτε να δοκιμάσετε χωρίς να ξοδέψετε τίποτα: δημιουργία και ανάγνωση τιμολογίου, ακύρωσή του, το 422 σε λανθασμένο ποσό, το 401 σε λάθος κλειδί, και η δική σας επαλήθευση υπογραφής — υπογράψτε ένα δείγμα σώματος με το μυστικό σας και δώστε το στον δικό σας χειριστή. Αυτό που πραγματικά απαιτεί αληθινή πληρωμή είναι μόνο το τελευταίο βήμα: ένα πραγματικό webhook payment.credited.
Σχεδιάστε την ενσωμάτωση ώστε να μην εξαρτάται από sandbox ή από προσομοίωση πληρωμής: η live διαδρομή επαληθεύεται πιο γρήγορα — και πιο πιστά.
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.
Λίστα ελέγχου πριν την κυκλοφορία#
Δέκα πράγματα για έλεγχο πριν τη λειτουργία.
- Το κλειδί είναι μόνο στην πλευρά του διακομιστή, ποτέ σε JavaScript προγράμματος περιήγησης.
- Η υπογραφή webhook επαληθεύεται έναντι του
"{timestamp}.{raw_body}", σε σταθερό χρόνο. - Παραδόσεις παλαιότερες των πέντε λεπτών απορρίπτονται, και το ρολόι του διακομιστή είναι συγχρονισμένο με NTP.
- Ένα επαναλαμβανόμενο
X-Paysell-Event-Idδεν κάνει τίποτα τη δεύτερη φορά. - Το webhook απαντά 2xx εντός δέκα δευτερολέπτων· η αργή εργασία γίνεται μετά.
- Το URL webhook είναι τομέας https:// στη θύρα 443, χωρίς ανακατεύθυνση μπροστά του.
- Ένα χαμένο webhook είναι διαχειρίσιμο: το endpoint τιμολογίου διαβάζεται στη σελίδα ευχαριστιών ή σε μια σάρωση συμφωνίας.
- Το
idempotency_keyδημιουργείται μία φορά ανά παραγγελία και επαναχρησιμοποιείται σε επαναλήψεις. - Τα ποσά φεύγουν ως συμβολοσειρές σε κανονικές μονάδες· οι αριθμοί του webhook διαβάζονται ως ελάχιστες μονάδες.
- Η διεύθυνση εμφανίζεται ακριβώς όπως επιστράφηκε, χωρίς τροποποίηση.
- Τα
overpaidκαιunderpaidαντιμετωπίζονται, όχι μόνο τοpaid· τοexpiredμπορεί να φέρει ακόμηpaid_minor. - Το εμπόρευμα παραδίδεται με
status: paidήoverpaid, ποτέ με την απλή άφιξη της κλήσης. - Το
429αντιμετωπίζεται περιμένοντας να περάσει τοRetry-After, όχι με άμεση επανάληψη. - Τα υπόλοιπα διαβάζονται από εμάς, δεν παρακολουθούνται ξεχωριστά ως αλήθεια.
Κάτι δεν είναι ξεκάθαρο;
Αν αυτή η σελίδα δεν απάντησε στην ερώτησή σας, πρόκειται για κενό στην τεκμηρίωση που αξίζει να μας το πείτε. Γράψτε μας από την περιοχή λογαριασμού σας και θα διορθώσουμε τη σελίδα, όχι μόνο την απάντηση.