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:

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:

For fixed-price fulfillment, decide your tolerance:


Backend vs. frontend responsibilities

ConcernWhere
POST /intentsBackend (you control amounts, store the index).
Show address / QR, poll for live UXFrontend (proxy /status through your backend — the API key must stay server-side).
Decide "order is paid" & fulfillBackend, on status === "swept" (or the intent.swept webhook).
POST /sweep (manual)Backend only — it moves funds.

Handling each outcome (reference)

Terminal statusWhat happenedYour action
sweptPaid & settled to your wallet.Fulfill. Use settlement for the amount.
refundedFunds returned to sender (expired-partial, or wrong asset).Mark unpaid. Optionally notify.
expiredNo payment within the window.Mark unpaid / cancel.
refund_failedAuto-settlement exhausted retries.Contact support; funds are safe but need manual handling.

Multiple / partial payments


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 →