A practical, end-to-end walkthrough of wiring Zuuppa Processing into your app. Examples are in TypeScript/Node but the flow is language-agnostic — it's just HTTP.
┌─────────────┐ ┌──────────────┐ ┌────────────────────┐
│ Your │ │ Your │ │ Zuuppa Processing │
│ Frontend │ │ Backend │ │ │
└──────┬──────┘ └──────┬───────┘ └─────────┬──────────┘
│ checkout │ │
│─────────────────────▶│ POST /intents │
│ │─────────────────────────▶│
│ │ {index, address} │
│ address + QR │◀─────────────────────────│
│◀─────────────────────│ │
│ │
[payer sends funds on-chain] ────────────────────────▶ (we detect it)
│ │ (we auto-sweep)
│ poll status │ │
│─────────────────────▶│ GET /status?index=N │
│ │─────────────────────────▶ │
│ "swept" + amount │◀───────────────────────── │
│◀─────────────────────│ fulfill order │
Golden rule: your backend calls POST /intents and reads /status (both
require your API key). Your frontend displays the address and can poll for
live UX via a thin proxy on your backend (so the key stays server-side), but treat
order fulfillment as a backend decision keyed off the swept status.
Get your API key first. Sign in, set your sweep destination in Settings, and create a key under API keys. Keep it server-side — don't ship it to the browser. See Getting started.
Decide the amount in base units. For SOL, lamports = SOL × 1e9. For a token,
base = amount × 10^decimals.
const API = "https://api-processing.zuuppa.com";
const API_KEY = process.env.ZUUPPA_API_KEY!; // sk_live_... from the dashboard
async function createInvoice(orderId: string, sol: number) {
const res = await fetch(`${API}/intents`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify({
expected_lamports: Math.round(sol * 1e9),
reference: orderId, // your order id — you'll get it back
expires_in_secs: 900, // 15-minute payment window
}),
});
if (!res.ok) throw new Error((await res.json()).error);
const intent = await res.json();
// Persist the mapping in YOUR database:
// order_id -> { index: intent.derivation_index, address: intent.address }
return intent;
}
Store derivation_index against your order. It's how you'll look the payment
up later. Also store address to display.
body: JSON.stringify({
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
expected_lamports: 100_000_000, // 100 USDC (6 decimals) in base units
reference: orderId,
expires_in_secs: 900,
})
Display intent.address. Render a QR code of it so wallets can scan. The
Solana Pay URI scheme also works and can pre-fill
the amount:
solana:<address>?amount=<ui_amount>&spl-token=<mint-if-token>&label=Order%201001
Any Solana wallet (Phantom, Solflare, etc.) can pay to a raw address too.
Poll GET /status?index=N. A 3–5 second interval is fine. Stop when you reach a
terminal state. (For production, prefer webhooks and use polling
only for live UI.)
async function pollStatus(index: number, onUpdate: (s: any) => void) {
const terminal = new Set(["swept", "refunded", "expired", "refund_failed"]);
while (true) {
const res = await fetch(`${API}/status?index=${index}`, {
headers: { "Authorization": `Bearer ${API_KEY}` },
});
if (res.ok) {
const s = await res.json();
onUpdate(s); // update UI with s.message
if (terminal.has(s.status)) return s;
}
await new Promise((r) => setTimeout(r, 3000));
}
}
What to show the payer — use message directly; it's written for humans:
pending → "Waiting for payment."underpaid → "Underpaid. Please send X more…" (also see shortfall_lamports).paid / sweeping → "Payment received."swept → "✓ Paid." (fulfill the order — see step 4)overpaid → "Overpaid. The extra is being sent back."expired → "Payment window expired."Frontend polling is a UX convenience. The authoritative "order is paid" decision should be made by your backend (step 4), not trusted from the browser.
sweptWhen an intent reaches swept, the money is in your destination wallet.
Read settlement for the exact amount and reconcile:
const s = await pollStatus(index, updateUi);
if (s.status === "swept" && s.settlement) {
const received = s.settlement.destination_amount; // base units to YOUR wallet
const expected = s.expected_lamports; // what you asked for
// For fixed-price orders, verify you got what you expected (net of fees).
// If you run a platform fee, `received` = expected - fee; account for that.
markOrderPaid(s.reference, {
amount: received,
asset: s.settlement.asset,
txSignatures: s.settlement.signatures,
});
} else if (s.status === "refunded" || s.status === "expired") {
markOrderUnpaid(s.reference, s.status);
} else if (s.status === "refund_failed") {
alertOps(s.reference); // needs manual attention
}
settlement.destination_amount is what actually landed in your wallet, which
may be less than received_lamports because of:
For fixed-price fulfillment, decide your tolerance:
destination_amount >= expected - platform_fee.received_lamports >= expected (they paid
enough), then use destination_amount for your books.| Concern | Where |
|---|---|
POST /intents | Backend (you control amounts, store the index). |
| Show address / QR, poll for live UX | Frontend (proxy /status through your backend — the API key must stay server-side). |
| Decide "order is paid" & fulfill | Backend, on status === "swept" (or the intent.swept webhook). |
POST /sweep (manual) | Backend only — it moves funds. |
| Terminal status | What happened | Your action |
|---|---|---|
swept | Paid & settled to your wallet. | Fulfill. Use settlement for the amount. |
refunded | Funds returned to sender (expired-partial, or wrong asset). | Mark unpaid. Optionally notify. |
expired | No payment within the window. | Mark unpaid / cancel. |
refund_failed | Auto-settlement exhausted retries. | Contact support; funds are safe but need manual handling. |
underpaid and shortfall_lamports
tells you the remaining amount. If they top it up before expiry, it advances to
paid and sweeps. received_lamports accumulates across payments.expected (SOL: excess auto-refunded;
token: full balance swept to you).If a payer sends the wrong token (e.g. USDT to a USDC invoice, or any token to
a SOL invoice), we auto-refund it to the sender on an independent track. This
shows up in /status under token_refunds and does not block the correct
payment:
"token_refunds": [{ "mint": "Es9v...USDT", "status": "refunded" }]
You generally don't need to act on this, but you can surface it ("we returned a token you sent by mistake").
Next: Webhooks →