Accept USDT on TON in 5 minutes
A step-by-step guide to creating a store, generating an API key, creating an invoice with curl, and confirming payment through a webhook.
- Published
- 27/08/2026
- Reading time
- 4 min read
- Updated
- 12/09/2026
On this page
This guide takes you from a fresh Paysell account to a working integration: a store, an API key, an invoice created with curl, a buyer paying through the hosted checkout, and a webhook that marks the order paid in your system.
Step 1: Create a store#
Register and create a store from the cabinet. A store is the unit that holds your API keys, invoices, and balance — most merchants only need one, but you can create more if you sell under multiple brands.
Your store can accept payments immediately. A background review runs alongside normal operation, but it only gates withdrawals — not incoming payments — so you can start testing right away.
Note: Your key is live from minute one — there is no separate test environment, and that saves time: you exercise the same path your real orders will take. Test with small real amounts — a fraction of a TON is enough to see the full flow.
Step 2: Create an API key#
From the store settings, generate an API key. It's shown to you exactly once, so copy it into your secrets manager or .env file immediately — Paysell can't show it to you again. Keys look like sk_live_... and go in the Authorization header as a bearer token.
Step 3: Create an invoice with curl#
Every payment starts as an invoice. Here's a complete example for a 25 USDT charge:
curl -X POST https://paysell.me/api/merchant/v1/invoices \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"asset": "USDT_TON",
"amount": "25",
"order_id": "order-1042"
}'
Note that amount is a decimal string, not a number — this avoids floating-point rounding issues. order_id is your own identifier; Paysell doesn't require any particular format, just uniqueness that's meaningful to you.
The response looks like this:
{
"invoice_id": "inv_8f3ka92ldk",
"payment_url": "https://paysell.me/pay/inv_8f3ka92ldk",
"address": "UQCabcdefghijklmnopqrstuvwxyz0123456789ABCD",
"asset": "USDT_TON",
"amount": "25",
"amount_minor": 25000000,
"status": "pending",
"expires_at": "2026-08-27T11:00:00Z"
}
amount_minor is the amount in minimal units — USDT uses 6 decimals, so 25 USDT is 25000000. TON uses 9 decimals. The invoice expires 2 hours after creation by default (configurable from 1 minute to 24 hours); you can shorten or extend that with a ttl_minutes parameter.
Step 4: Open payment_url#
Send the buyer to payment_url — a hosted page at /pay/{invoice_id} — or redirect them there right after checkout. The page shows a QR code and a deep link that opens Tonkeeper, MyTonWallet, or another TON wallet with the amount and address pre-filled, plus a countdown until the invoice expires.
If the buyer sends less than the invoice amount, the invoice moves to underpaid and stays open for another 24 hours so they can top it up. If they send more, the full amount is credited to you and the invoice is marked overpaid. If they send the wrong asset entirely, it's still credited to your balance, but the original invoice stays open.
Step 5: Receive the webhook#
Once the payment is included in the TON masterchain plus three blocks and cross-checked against a second data source — about a minute after the buyer pays — Paysell sends a payment.credited webhook (or payment.rejected if something is wrong) to the endpoint you configured.
Each webhook request carries these headers:
| Header | Purpose |
|---|---|
X-Paysell-Event | Event type, e.g. payment.credited |
X-Paysell-Event-Id | Unique event ID, for deduplication |
X-Paysell-Timestamp | Unix timestamp the request was signed at |
X-Paysell-Signature | sha256=<hex> HMAC signature |
Always verify the signature before trusting a webhook body. Here's a minimal Node.js example:
const crypto = require("crypto");
function verifyPaysellWebhook(rawBody, headers, secret) {
const timestamp = headers["x-paysell-timestamp"];
const signature = (headers["x-paysell-signature"] || "").replace("sha256=", "");
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) {
throw new Error("Timestamp too old — possible replay");
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
if (!isValid) throw new Error("Invalid signature");
return true;
}
Use rawBody — the unparsed request body — not a re-serialized JSON object, or the signature won't match. Reject anything older than 300 seconds, and use X-Paysell-Event-Id to ignore duplicates if a retry arrives after you've already processed the event.
Note: If your endpoint is down or returns an error, Paysell retries with backoff: 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours, then 24 hours, before giving up. You can always re-send a dropped event from the cabinet.
Step 6: Mark the order paid#
In your webhook handler, look up the order by order_id (or invoice_id), confirm the amount matches what you expected — remember, amounts in the webhook body are integers in minimal units — and update the order status. Respond with a 2xx status quickly; do any slow work (sending confirmation emails, updating inventory) after you've acknowledged the webhook.
That's the whole flow: create a store, generate a key, create an invoice, let the buyer pay through the hosted page, and let a verified webhook update your order. See the full API reference for invoice statuses, listing endpoints, and more detail on every field.
Start accepting USDT and TON
A merchant account, an API key and a hosted checkout page — 0.2% per payment, no monthly fee.
Create an account