Skip to content
Tender

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.