Payment links & invoicing

Crypto payment links and invoices

An invoice is one API call to quote it and one to open it. The payer sends whatever coin they hold, the freelancer receives the asset they asked for, and the money never touches your service or ours.

  • Calls per invoice2 — quote, then order
  • Custodynone — payer → provider → payee
  • Invoice amountquote with amount_to
  • Rate lockrate_type=fixed

Built on/v1/partner/quotes/v1/partner/orders/v1/validate-address

SwapZilla does not host invoices — it gives you the two calls an invoice is made of. Quote by the amount the invoice asks for, open the order, and the payer gets a deposit address that settles straight into the payee's wallet. Nothing is held by us or by you.

  • The payer sends any listed asset; the payee receives the one they billed in.
  • The rate can be locked for the life of the offer with rate_type=fixed.
  • Your invoice id travels with the order as client_order_id.
  • Settlement is non-custodial: payer → exchange provider → payee.

How a crypto payment link works

  1. The payee states what they are owed

    An asset, a network and an address — 500 USDT on TRC20, paid to their own wallet. That is your invoice record; SwapZilla never needs to know about it until the payer shows up.

  2. The payer opens the link and picks a coin

    GET /v1/assets is the honest list of what can be paid with right now — every asset and network with at least one provider behind it. Render it as the picker.

  3. Quote by the amount due, not by the amount sent

    This is the call that makes an invoice an invoice: ask amount_to=500 and the offer answers with the exact FromAmount the payer has to send. Add rate_type=fixed and that number stays true while they pay.

  4. Open the order

    POST /v1/partner/orders with the offer, the payee's address as to_address, the payer's address as refund_address and your invoice number as client_order_id. Pass expected_to_amount and the order is refused rather than created if the payout drifted.

  5. Show the address, then watch the order

    DepositAddress and AmountFrom are the whole payment page — as text, as a QR, with the network named. Poll GET /v1/partner/orders/{id} every 5–10 seconds until DONE, and the receipt is AmountToReceived plus TxID.

The two calls, in full

Quote a €500-sized invoice denominated in USDT, paid in BTC:

curl -sG "https://api.swapzilla.io/v1/partner/quotes" \
  -H "X-API-Key: $API_KEY" \
  --data-urlencode "from_asset=BTC" \
  --data-urlencode "from_network=BITCOIN" \
  --data-urlencode "to_asset=USDT" \
  --data-urlencode "to_network=TRC20" \
  --data-urlencode "amount_to=500" \
  --data-urlencode "rate_type=fixed"

The first offer is the best one. Turn it into a payable address:

curl -s -X POST "https://api.swapzilla.io/v1/partner/orders" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "offer_id": "0b0c8f0e-2c1a-4f3f-8f57-2a1d1c4e77aa",
    "to_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
    "refund_address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
    "client_order_id": "INV-2026-0413",
    "expected_to_amount": 500
  }'

Then a loop that turns statuses into invoice states:

const terminal = ["DONE", "TIME_EXPIRED", "FAILED", "REFUNDED"];

async function watch(orderID) {
  for (;;) {
    const r = await fetch(`https://api.swapzilla.io/v1/partner/orders/${orderID}`, {
      headers: { "X-API-Key": process.env.API_KEY },
    });
    const order = await r.json();
    await onInvoiceState(order);                       // your own bookkeeping
    if (terminal.includes(order.Status)) return order;
    await new Promise((s) => setTimeout(s, 7000));     // 5–10 s is the right cadence
  }
}

Quote when the link is opened, not when it is issued. An offer is valid for five minutes. An invoice emailed on Monday and paid on Friday is quoted on Friday — the link is a page of yours, the offer is created the moment someone lands on it.

Invoice states, mapped from order statuses

Order statusWhat the invoice saysWhat you do
NEW WAIT_DEPOSITAwaiting paymentShow the address and the countdown.
CONFIRMINGPayment seen on-chainStop the countdown; the payer is done.
EXCHANGING SENDINGSettlingNothing — the provider is paying out.
DONEPaidMark it paid on AmountToReceived; file TxID as the receipt.
TIME_EXPIREDLink expired unpaidRe-quote and issue a fresh address.
FAILED REFUNDEDNot paidThe provider returns the coins to refund_address — which is why you asked the payer for one.

What this does not do

Not includedWhat that means for you
A hosted invoice page or PDFThe link, the layout and the emails are yours. The API supplies the amount, the address and the status.
WebhooksThere are none yet — poll the order every 5–10 seconds until a terminal status.
FiatCrypto in, crypto out. An invoice denominated in EUR is your own price feed converting to a stablecoin amount before you quote.
Partial or repeated paymentOne order accepts one deposit of one amount. Split invoices are several orders.
Refund initiationRefunds happen at the provider, to refund_address, when a swap fails — there is no API call that reverses a settled payment.

Who bills this way

  • Freelancers invoicing across borders. The client pays in whatever they hold; the invoice is settled in USDT on the network the freelancer's wallet actually uses.
  • Studios and agencies issuing milestone invoices, each one its own order and its own client_order_id.
  • Platforms that bill on behalf of their users — the payee address is the user's, so the platform never holds client money and never becomes the party that owes it.
  • Anyone billing a payer on another chain: the payer's BTC and the payee's TRC20 USDT never have to meet on the same network.

Questions

Can I create a payment link that never expires?

The link can live forever; the offer behind it cannot. Offers expire five minutes after they are quoted, so a durable link points at your own page, which quotes and opens the order when the payer arrives. The deposit address is created at that moment, not when the invoice is issued.

Does SwapZilla hold the money in between?

No. The swap is non-custodial: the payer sends to the provider's deposit address and the provider pays out to the payee's address. Neither SwapZilla nor your platform is ever in possession of the funds, which is also why there is no payout call to make.

How do I make sure the payee receives exactly the invoiced amount?

Quote with amount_to instead of amount_from, ask for rate_type=fixed, and pass expected_to_amount when you create the order — the order is refused rather than created if the recalculated payout no longer matches what the invoice promised.

What happens if the payer sends the wrong amount?

The provider handles it under its own rules: a shortfall is usually refunded to refund_address, and an overpayment is either exchanged at the current rate or returned. This is why the payer's refund address should always be collected before the order is opened.

Which coins can an invoice be paid in?

Whatever GET /v1/assets lists at that moment — the top-100 assets by market cap, each with the networks and providers currently able to serve them. The payee's settlement asset comes from the same list.

Do I need a key to try it?

Yes for quotes and orders; /v1/assets, /v1/providers and /v1/validate-address answer without one. A key is issued by a SwapZilla admin and used as X-API-Key from your backend, never from a browser.

Getting a key

Register in the partner cabinet and your partner id and the api_key that goes in X-API-Key are issued at once. The key starts working once it is activated: write to @swapzilla_support_bot with a sentence about what you are building. Every endpoint, field and status is in the partner API reference.