Documentation
Start accepting any coin
Three calls take you from nothing to a settled payment. Create an invoice, show your buyer the checkout, and listen for the webhook that tells you the money landed.
Quickstart
Create an invoice with the amount you want and the order reference from your own system. Tender mints a deposit address on every chain you accept and hands back a public token for the checkout page.
Request
curl -X POST https://api.tender.sh/v1/invoices \
-H "Authorization: Bearer $TENDER_API_KEY" \
-H "Idempotency-Key: order_8842" \
-H "Content-Type: application/json" \
-d '{
"amount_expected": "49.00",
"currency": "USD",
"reference": "order_8842",
"redirect_url": "https://yourstore.com/thanks"
}'Response
{
"id": "inv_2p9xQ4",
"token": "chk_7Fk2mD9sLq",
"status": "PENDING",
"amount_expected": "49.00",
"currency": "USD",
"expires_at": "2026-09-24T14:32:00Z",
"addresses": [
{ "chain": "bitcoin", "address": "bc1q9x…4de03" },
{ "chain": "solana", "address": "7Fk2…Lq9s" },
{ "chain": "base", "address": "0x7c2f91…a4de03" }
]
}Send the buyer to https://pay.tender.sh/{token}. The token is not the invoice id. It carries nothing private, so it is safe in a URL.
API reference
Merchant routes authenticate with your API key. Public routes are what the checkout page calls and need no auth.
Merchant
Authorization: Bearer <api_key>
POST/v1/invoices
Create an invoice. Honours Idempotency-Key, so a retry returns the original.
GET/v1/invoices/:id
Fetch one invoice with its addresses and payments.
GET/v1/invoices
List invoices, filtered by status or date. Paginated.
POST/v1/invoices/:id/cancel
Cancel an invoice that has not yet been paid.
GET/v1/merchant
Your settlement address, settlement asset and webhook URL.
PATCH/v1/merchant
Update settlement address, webhook URL or fee.
Public
No authentication, safe to call from the browser
GET/public/invoices/:token
The invoice, its addresses and its status. No merchant data.
GET/public/invoices/:token/events
Server-sent events. Pushes every status change as it happens.
GET/public/chains
Supported chains, assets and estimated settlement times.
POST/public/invoices/:token/submit-tx
Optional. The buyer pastes a tx hash and detection speeds up.
Invoice states
An invoice is the thing your system cares about. These are every state it can reach, and the six terminal ones are marked. Once an invoice is terminal it will never move again.
PENDING
Created, addresses minted, nothing received yet.
DETECTED
A deposit is visible on the source chain but has not settled.
SETTLEDTerminal
The full amount landed at your address in your chosen asset.
OVERPAIDTerminal
Settled, and more came in than was owed. The excess is recorded.
UNDERPAIDTerminal
Less than the amount arrived before the invoice closed. A deposit below the chain minimum is refunded automatically; anything above it reached your address — see the invoice's payments.
EXPIREDTerminal
The deadline passed with nothing received.
CANCELLEDTerminal
You cancelled it before it was paid.
NEEDS_RECOVERYTerminal
A deposit succeeded but the onward settlement failed. This one is not auto-refunded. See below.
Why NEEDS_RECOVERY exists
Failures before the deposit lands are refunded for you. A failure after it lands is not, and recovery is explicit. Most processors hide this behind a generic error; Tender gives it a state so your support team can see it and act on it.
Webhooks
Every state change posts to your endpoint, signed and timestamped. Delivery is at-least-once, so dedupe on the event id.
- invoice.detected
- invoice.settled
- invoice.underpaid
- invoice.overpaid
- invoice.expired
- invoice.needs_recovery
Payload
{
"id": "evt_4mK8xQ",
"event": "invoice.settled",
"created_at": "2026-09-24T14:12:41Z",
"data": {
"invoice_id": "inv_2p9xQ4",
"reference": "order_8842",
"status": "SETTLED",
"amount_expected": "49.00",
"amount_settled": "49.00",
"from_chain": "bitcoin",
"settled_at": "2026-09-24T14:12:39Z"
}
}Verifying the signature
import { createHmac, timingSafeEqual } from "node:crypto";
// headers: X-Tender-Signature-V2 and X-Tender-Timestamp.
// Verify against the RAW body: parsing first changes the bytes.
export function verify(rawBody: string, signature: string, timestamp: string, secret: string) {
// Reject replays: the timestamp is signed, so it cannot be faked.
if (!/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "sha256=" + createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Use X-Tender-Signature-V2: it signs the timestamp together with the body, so a delivery older than five minutes can be rejected safely. Each retry is re-signed with a fresh timestamp. The older X-Tender-Signature (body only) is still sent for existing integrations.
Before you integrate
- Do I need MON to receive payments?
- No. Gas is abstracted end to end, so neither you nor your buyer ever acquires the destination chain's native token.
- How long are deposit addresses valid?
- Addresses are permanent. The expiry belongs to the invoice, not the address, which is why an invoice can expire while its address stays live.
- What happens if a buyer pays twice?
- Each deposit is processed independently. The first settles the invoice; the second is recorded and reported as an overpayment.
Ready to start?
Create your first invoice in under five minutes. No sales call, no contract.