Paysell

Δεχτείτε πληρωμές σε κρυπτονομίσματα

Η Paysell διακανονίζει TON και USDT στο δίκτυο TON. Δημιουργείτε ένα τιμολόγιο, σας δίνουμε έναν σύνδεσμο, και λαμβάνετε ένα υπογεγραμμένο callback μόλις τα χρήματα επιβεβαιωθούν στο blockchain και πιστωθούν στο υπόλοιπό σας.

Επισκόπηση#

Τι κάνει η Paysell, και τι δεν κάνει.

Η Paysell είναι πάροχος πληρωμών, όχι πορτοφόλι. Ποτέ δεν χειρίζεστε ιδιωτικά κλειδιά, δεν παρακολουθείτε το blockchain, ούτε αποφασίζετε πότε μια συναλλαγή είναι οριστική — αυτό το αναλαμβάνουμε εμείς.

Κάθε τιμολόγιο αποκτά τη δική του διεύθυνση παραλαβής. Όταν ένας αγοραστής το πληρώνει, περιμένουμε το δίκτυο να επιβεβαιώσει τη μεταφορά, αφαιρούμε την προμήθειά μας και πιστώνουμε το υπόλοιπο στο υπόλοιπό σας. Κάνετε ανάληψη σε όποια διεύθυνση θέλετε.

Τα υπόλοιπα βρίσκονται σε εμάς και είναι η μοναδική πηγή αλήθειας. Εμφανίστε τα, αλλά μην κρατάτε ποτέ ένα δεύτερο αντίγραφο ως έγκυρο — δύο μετρητές πάντα καταλήγουν να αποκλίνουν, και τότε κανείς δεν ξέρει ποιος είναι σωστός.

Πώς λειτουργεί μια πληρωμή#

Έξι βήματα, τα περισσότερα δικά μας.

Έξι βήματα, τα περισσότερα δικά μας:

  1. 1

    Ο πελάτης σας κάνει κλικ στην πληρωμή

    Ο διακομιστής σας καλεί το API μας με το ποσό και τη δική σας αναφορά παραγγελίας.

  2. 2

    Δίνουμε μια διεύθυνση

    Μια νέα διεύθυνση παραλαβής λαμβάνεται από μια προδημιουργημένη δεξαμενή και συνδέεται με αυτό το τιμολόγιο. Μια διεύθυνση ανήκει ακριβώς σε ένα ανοιχτό τιμολόγιο, κι έτσι αντιστοιχίζεται μια πληρωμή σε αυτό.

  3. 3

    Ο πελάτης στέλνει τα νομίσματα

    Σαρώνει τον κωδικό QR ή αντιγράφει τη διεύθυνση. Στείλτε τον στο payment_url που επιστρέφουμε και η σελίδα αναλαμβάνει τα πάντα για εσάς — ποσό, διεύθυνση, QR, αντίστροφη μέτρηση, ζωντανή κατάσταση.

  4. 4

    Εντοπίζουμε τη μεταφορά

    Ερωτώνται δύο ανεξάρτητες πηγές δεδομένων blockchain και οι απαντήσεις τους συγκρίνονται. Αν διαφωνούν, σταματάμε αντί να επιλέξουμε την πιο βολική απάντηση.

  5. 5

    Περιμένουμε την οριστικοποίηση

    Ένταξη στο masterchain συν τρία μπλοκ από πάνω. Περίπου δεκαπέντε δευτερόλεπτα — μια πληρωμή που φαίνεται διακανονισμένη και μετά εξαφανίζεται θα ήταν δική σας απώλεια, γι' αυτό δεν παίρνουμε αυτό το ρίσκο.

  6. 6

    Πιστώθηκε, και ενημερώνεστε

    Η προμήθεια αφαιρείται, το υπόλοιπο φτάνει στο υπόλοιπό σας, και ένα υπογεγραμμένο webhook πηγαίνει στον διακομιστή σας με το order_id σας.

Από την πληρωμή στο callback: περίπου ένα λεπτό — περίπου δεκαπέντε δευτερόλεπτα επιβεβαιώσεων δικτύου, το υπόλοιπο είναι η σάρωσή μας παρακολουθούμενων διευθύνσεων.

Πού πάνε τα χρήματα#

Η προμήθεια, και πάνω σε τι υπολογίζεται.

Η προμήθεια είναι 0,2%, σταθερή για το κατάστημά σας τη στιγμή της εγγραφής του. Αν η τυπική τιμή αλλάξει αργότερα, η δική σας δεν αλλάζει — είναι γραμμένη σε κάθε τιμολόγιο ως αριθμός, όχι ως αναφορά σε μια ρύθμιση.

Η προμήθεια λαμβάνεται από αυτό που πραγματικά φτάνει, όχι από αυτό που ζητούσε το τιμολόγιο. Τιμολογήστε 5 USDT και λάβετε 20, η προμήθεια υπολογίζεται στα 20. Αν πληρωθεί λιγότερο, υπολογίζεται σε αυτό που ήρθε.

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

Η υπερπληρωμή πιστώνεται εξ ολοκλήρου — δεν κρατάμε τη διαφορά. Η ελλιπής πληρωμή αφήνει το τιμολόγιο ανοιχτό ώστε ο αγοραστής να μπορεί να συμπληρώσει στην ίδια διεύθυνση.

Η απομάκρυνση των νομισμάτων από μια διεύθυνση παραλαβής κοστίζει gas δικτύου, και το πληρώνουμε εμείς — αυτό το μέρος δεν αγγίζει ποτέ το υπόλοιπό σας. Η ανάληψη στη δική σας διεύθυνση είναι άλλο πράγμα: έχει τη δική της προμήθεια, η οποία αφαιρείται από το ποσό που ζητάτε, και οι ακριβείς αριθμοί βρίσκονται στον τιμοκατάλογο.

Γρήγορο ξεκίνημα#

Πέντε λεπτά μέχρι το πρώτο σας τιμολόγιο.

Πέντε βήματα. Τα δύο είναι κλικ στην περιοχή λογαριασμού σας, το ένα είναι ένα μόνο αίτημα από τον διακομιστή σας, και τα δύο τελευταία γίνονται από μόνα τους.

  1. 1

    Δημιουργήστε ένα κατάστημα

    Στην περιοχή λογαριασμού σας. Αρχίζει να δέχεται πληρωμές αμέσως — χωρίς αναμονή για έλεγχο. Η επαλήθευση γίνεται ήσυχα στο παρασκήνιο και περιορίζει μόνο τις αναλήψεις, όχι τις εισερχόμενες πληρωμές.

  2. 2

    Εκδώστε ένα κλειδί API

    Το κατάστημά σας → Κλειδιά API → Νέο κλειδί. Το κλειδί και το μυστικό του webhook εμφανίζονται μία φορά και ποτέ ξανά. Φυλάξτε τα όπως θα φυλάγατε έναν κωδικό βάσης δεδομένων, και μην τα στείλετε ποτέ σε πρόγραμμα περιήγησης.

  3. 3

    Δημιουργήστε ένα τιμολόγιο

    Ένα αίτημα από τον διακομιστή σας, ένας σύνδεσμος πίσω. Και τα τέσσερα αποσπάσματα κώδικα παρακάτω στέλνουν ακριβώς το ίδιο πράγμα.

  4. 4

    Στείλτε τον αγοραστή στο payment_url

    Αυτή είναι ολόκληρη η ολοκλήρωση αγοράς — ποσό, διεύθυνση, κωδικός QR, αντίστροφη μέτρηση, κατάσταση σε πραγματικό χρόνο — και δεν υπάρχει τίποτα να φτιάξετε. Δείτε Ολοκλήρωση αγοράς για το τι βλέπει πραγματικά ο αγοραστής.

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

Πιστοποίηση#

Το κλειδί API σας, και πώς χρησιμοποιείται.

Κάθε αίτημα μεταφέρει το κλειδί σας στην κεφαλίδα Authorization:

http
Authorization: Bearer sk_live_IH4SNdnYsv-yabTuPCVln4vOvvGoEyxA

Κάθε κλειδί που εκδίδεται εδώ ξεκινά με sk_live_. Το πρόθεμα sk_test_ υπάρχει μόνο σε μια εγκατάσταση που δείχνει στο δίκτυο δοκιμών, και τέτοια εγκατάσταση δεν προσφέρεται — δείτε Δοκιμές. Αποθηκεύουμε ένα μονόδρομο hash, όχι το ίδιο το κλειδί, οπότε κανείς, ούτε καν εμείς, δεν μπορεί να σας το δείξει ξανά. Το χάσατε; Εκδώστε ένα νέο και ανακαλέστε το παλιό.

Το κατάστημα προκύπτει από το κλειδί, γι' αυτό κανένα αίτημα δεν παίρνει ποτέ id καταστήματος. Ένα κλειδί μπορεί να ενεργήσει μόνο στο δικό του κατάστημα.

Η διαδρομή φέρει έκδοση: /api/merchant/v1/…. Μέσα σε μια έκδοση μόνο προσθέτουμε πεδία — τίποτα δεν μετονομάζεται και τίποτα δεν αλλάζει σιωπηλά νόημα. Μια αλλαγή που θα έσπαγε τον κώδικά σας παίρνει νέο πρόθεμα, /v2, και το /v1 συνεχίζει να λειτουργεί για ανακοινωμένο διάστημα.

Αυτό το κλειδί δημιουργεί τιμολόγια στο όνομά σας. Κρατήστε το στην πλευρά του διακομιστή. Οτιδήποτε σε JavaScript προγράμματος περιήγησης είναι δημόσιο, όσο καλά κι αν φαίνεται κρυμμένο.

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.

Δημιουργία τιμολογίου#

POST /api/merchant/v1/invoices

POST/api/merchant/v1/invoices

Σώμα αιτήματος

ΠεδίοΤύποςΥποχρεωτικόΠεριγραφή
assetstringναιΕίτε TON είτε USDT_TON.
amountstringναιΚανονικές μονάδες του νομίσματος, ως συμβολοσειρά: "5" είναι 5 USDT. Όχι περισσότερα δεκαδικά από όσα έχει το νόμισμα. Δείτε Ποσά.
order_idstringόχιΗ δική σας αναφορά, έως 200 χαρακτήρες. Επιστρέφει σε κάθε webhook — έτσι αντιστοιχίζετε μια πληρωμή σε παραγγελία.
descriptionstringόχιΈως 1000 χαρακτήρες. Εμφανίζεται στον αγοραστή στη σελίδα πληρωμής.
ttl_minutesnumberόχιΠόσο καιρό παραμένει πληρωτέο το τιμολόγιο, σε λεπτά. 1–1440· παραλείψτε το και ισχύει η προεπιλογή — 2 ώρες σήμερα.
idempotency_keystringόχιΈως 200 χαρακτήρες. Στείλτε την ίδια τιμή σε επανάληψη και θα λάβετε πίσω το ίδιο τιμολόγιο αντί για δεύτερο. Είναι πεδίο του σώματος, όχι η κεφαλίδα Idempotency-Key — αυτή η κεφαλίδα δεν διαβάζεται εδώ.

Απάντηση · 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"
}

Πώς να το χρησιμοποιήσετε στην παραγγελία σας

ΠεδίοΤι να κάνετε με αυτό
invoice_idΑποθηκεύστε το με την παραγγελία σας. Είναι αυτό που ταυτοποιεί την πληρωμή παντού αλλού.
payment_urlΑνακατευθύνετε τον αγοραστή εδώ. Δεν χρειάζεται να χτίσετε τίποτα άλλο.
addressΜόνο αν χτίζετε τη δική σας ολοκλήρωση αγοράς. Εμφανίστε την ακριβώς όπως δόθηκε — δείτε την προειδοποίηση παρακάτω.
amountΤο ποσό σε κανονικές μονάδες, ακριβώς όπως το στείλατε. Αυτό εμφανίζετε.
amount_minorΤο ίδιο ποσό ως ακέραιος στην ελάχιστη μονάδα. Με αυτό υπολογίζετε.
expires_atΕμφανίστε αντίστροφη μέτρηση. Μετά την παρέλευσή του η διεύθυνση παύει να παρακολουθείται για αυτό το τιμολόγιο.
statusΕδώ είναι πάντα pending. Οι πραγματικές αλλαγές φτάνουν μέσω webhook.
Αν χτίζετε τη δική σας σελίδα, εκτυπώστε τη διεύθυνση ακριβώς όπως επιστράφηκε. Είναι σε μη επιστρεπτή μορφή (UQ… στο mainnet, 0Q… στο testnet). Η μετατροπή, η ομορφοποίηση ή η αντικατάστασή της με άλλη κωδικοποίηση της ίδιας διεύθυνσης θα κάνει τα νομίσματα που στέλνονται σε ένα πορτοφόλι που δεν έχει ακόμα αναπτυχθεί να επιστρέψουν στον αποστολέα.

Ανάγνωση τιμολογίου#

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

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

POST/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 από προγενέστερη μεταφορά, το συμβάν αφορά την επιπλέον κατάθεση και όχι εκείνη την πληρωμή.

Τι φτάνει

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

Αντιστοίχιση πεδίων

ΠεδίοΣημασία
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, για τα αρχεία σας και την υποστήριξη.

Κεφαλίδες σε κάθε παράδοση

http
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-Signaturesha256= ακολουθούμενο από το HMAC σε δεκαεξαδική μορφή. Δείτε παρακάτω.

Επαλήθευση της υπογραφής

Κάθε αίτημα υπογράφεται με το μυστικό webhook που εμφανίζεται μία μόνο φορά, όταν δημιουργήσατε το κλειδί. Η υπογραφή είναι HMAC-SHA256(secret, "{timestamp}.{raw_body}") — η χρονοσήμανση από το X-Paysell-Timestamp, μια κυριολεκτική τελεία, και μετά τα bytes του σώματος. Ελέγξτε την πριν ενεργήσετε: χωρίς αυτό, όποιος μάθει το URL σας μπορεί να σας παραδώσει μια πληρωμένη παραγγελία.

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

Υπογράψτε τα ακατέργαστα 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 όχι.
Η επαλήθευση γίνεται δύο φορές σκόπιμα — μία όταν αποθηκεύετε το URL, ώστε ένα τυπογραφικό λάθος να απαντηθεί αμέσως αντί με σιωπηλή μη παράδοση, και μία πριν από κάθε αποστολή, επειδή ο κάτοχος ενός τομέα μπορεί να τον στρέψει σε εσωτερική διεύθυνση οποιαδήποτε στιγμή. Αν το endpoint σας μετακομίσει, ενημερώστε πρώτα το κλειδί: ένα απορριφθέν URL δεν παραδίδει τίποτα και δεν μπαίνει σε ουρά.

Απαντήστε γρήγορα

Οποιοδήποτε 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.

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.

Αναφορά κατάστασης#

Όλες οι καταστάσεις τιμολογίων και πληρωμών, εξηγημένες.

Τιμολόγιο

ΚατάστασηΣημασίαΤι να κάνετε
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 στην απάντηση
TON9"1.5""1500000000"
USDT_TON6"1.5""1500000"

Συμβολοσειρά και όχι αριθμός, επειδή οι αριθμοί JSON είναι IEEE-754 doubles και ένα μεγάλο ποσό σε nanoton παύει να αναπαρίσταται ακριβώς μέσα τους. Περισσότερα δεκαδικά από όσα έχει το νόμισμα δίνουν 422, ποτέ σιωπηλή στρογγυλοποίηση των χρημάτων σας. Στα webhooks ισχύει το αντίστροφο: εκεί τα amount, fee και credited είναι ακέραιοι στην ελάχιστη μονάδα, γιατί εκείνη την πλευρά τη διαβάζει κώδικας, όχι άνθρωπος.

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

Όρια#

Ελάχιστα, μέγιστα, και όρια ρυθμού.

ΌριοΤιμήΣε παραβίαση
Ελάχιστο τιμολόγιο0.1 TON · 3 USDT422
Μέγιστο τιμολόγιο7000 TON · 10000 USDT422
Τιμολόγια ανά ώρα, ανά κατάστημα60429
Ανοιχτά τιμολόγια ταυτόχρονα20, αυξανόμενα με κάθε πληρωμένο τιμολόγιο, έως 200429
Διάρκεια ζωής τιμολογίου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`: η διατύπωση μπορεί να αλλάξει οποιαδήποτε στιγμή, ο κωδικός όχι.

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
    }
  ]
}
ΚατάστασηΠότεΤι να κάνετε
401Το κλειδί λείπει, είναι λάθος, ή έχει ανακληθεί.Ελέγξτε την κεφαλίδα. Επανεκδώστε το κλειδί αν ανακλήθηκε.
404Δεν υπάρχει τέτοιο τιμολόγιο, ή ανήκει σε άλλο κατάστημα.Ελέγξτε το id. Οι δύο περιπτώσεις απαντούν όμοια σκόπιμα, ώστε να μην μπορεί να ανιχνευθεί ένα id.
409Το τιμολόγιο βρίσκεται σε κατάσταση που το απαγορεύει.Διαβάστε πρώτα την τρέχουσα κατάστασή του.
422Το αίτημα είναι λανθασμένο ή το ποσό βρίσκεται εκτός των ορίων του τιμολογίου.Το μήνυμα ονομάζει τόσο την τιμή που στάλθηκε όσο και το όριο.
429Πάρα πολλά τιμολόγια αυτή την ώρα, πάρα πολλά ανοιχτά ταυτόχρονα, ή πάρα πολλά αιτήματα.Περιμένετε να περάσει το Retry-After, και μετά δοκιμάστε ξανά.
502Δεν μπορέσαμε να φτάσουμε τον πυρήνα επεξεργασίας.Δοκιμάστε ξανά με το ίδιο κλειδί ιδεμποτεντίας.

Κωδικοί

Η μορφή που αποφασίζουμε εμείς είναι {"detail": {"code": …, "message": …}}. Αυτοί είναι οι κωδικοί που επιστρέφει το API εμπόρου.

ΚωδικόςΚατάστασηΣημασία
invalid_api_key401Το κλειδί λείπει, είναι λανθασμένα διαμορφωμένο, άγνωστο ή ανακλημένο. Και οι τέσσερις περιπτώσεις απαντούν όμοια, ώστε να μην μπορεί να ανιχνευθεί ένα κλειδί.
not_found404Δεν υπάρχει τέτοιο αντικείμενο, ή ανήκει σε άλλο κατάστημα.
invalid_input422Το αίτημα δεν πέρασε την επικύρωση στον πυρήνα — λανθασμένο ποσό, πάρα πολλά δεκαδικά ψηφία, ποσό εκτός των ορίων του τιμολογίου.
conflict409Η ενέργεια αντιβαίνει στην τρέχουσα κατάσταση, όπως η ακύρωση ενός τιμολογίου που δεν είναι πλέον ανοιχτό.
too_many_requests429Ένα όριο ρυθμού: τιμολόγια ανά ώρα, ανοιχτά τιμολόγια, ή αιτήματα ανά λεπτό. Το Retry-After λέει πόσο να περιμένετε.
cbc_unreachable502Δεν μπορέσαμε να φτάσουμε τον πυρήνα επεξεργασίας. Δοκιμάστε ξανά με το ίδιο idempotency_key.
webhook_url_rejected422Μόνο κατά την αποθήκευση ενός κλειδιού: το 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 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.

Επιστροφές χρημάτων#

Πώς να επιστρέψετε χρήματα σε πελάτη.

Οι επιστροφές γίνονται μέσω της υποστήριξης, όχι με κλήση 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 διαδρομή επαληθεύεται πιο γρήγορα — και πιο πιστά.

Αντιμετωπίστε την πρώτη σας πραγματική παραγγελία ως τη δοκιμή: διαλέξτε μικρό ποσό, κρατήστε το τιμολόγιο ανοιχτό στον πίνακα ελέγχου, και ελέγξτε τη γραμμή της πληρωμής και την κατάσταση του webhook πριν στρέψετε εκεί πραγματικούς πελάτες.

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.

Λίστα ελέγχου πριν την κυκλοφορία#

Δέκα πράγματα για έλεγχο πριν τη λειτουργία.

  • Το κλειδί είναι μόνο στην πλευρά του διακομιστή, ποτέ σε 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, όχι με άμεση επανάληψη.
  • Τα υπόλοιπα διαβάζονται από εμάς, δεν παρακολουθούνται ξεχωριστά ως αλήθεια.

Κάτι δεν είναι ξεκάθαρο;

Αν αυτή η σελίδα δεν απάντησε στην ερώτησή σας, πρόκειται για κενό στην τεκμηρίωση που αξίζει να μας το πείτε. Γράψτε μας από την περιοχή λογαριασμού σας και θα διορθώσουμε τη σελίδα, όχι μόνο την απάντηση.