4. Integration Guide
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.
The flow
┌─────────────┐ ┌──────────────┐ ┌────────────────────┐
│ 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.
Step 1 — Create an intent when checkout starts
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.
For an SPL token (e.g. USDC)
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,
})
Step 2 — Show the deposit address to the payer
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.
Step 3 — Poll status until terminal
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 seeshortfall_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.
Step 4 — Fulfill the order on swept
When 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
}
Important accounting note
settlement.destination_amount is what actually landed in your wallet, which
may be less than received_lamports because of:
- the network fee (SOL intents; ~5000 lamports), and/or
- your configured platform fee, and/or
- an overpayment's refunded excess.
For fixed-price fulfillment, decide your tolerance:
- If you charge the buyer the fee → require
destination_amount >= expected - platform_fee. - Simplest robust check → require
received_lamports >= expected(they paid enough), then usedestination_amountfor your books.
Backend vs. frontend responsibilities
| 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. |
Handling each outcome (reference)
| 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. |
Multiple / partial payments
- If a payer underpays, the intent stays
underpaidandshortfall_lamportstells you the remaining amount. If they top it up before expiry, it advances topaidand sweeps.received_lamportsaccumulates across payments. - If they overpay, you still get exactly
expected(SOL: excess auto-refunded; token: full balance swept to you).
Wrong-asset payments
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 →