2. Core Concepts

Read this before integrating. These concepts appear throughout the API.


Payment intent#

A payment intent is the central object. It represents "I expect a payment to this address." When you create one (POST /intents), we:

  1. Derive a unique deposit address for it, never reused across intents.
  2. Give it a stable derivation_index you use to track it.
  3. Persist it so detection and settlement survive restarts.

An intent records:

FieldMeaning
derivation_indexThe stable integer id. Use this to poll status and reference the payment.
addressThe Solana deposit address to show the payer.
mintThe asset accepted: null = native SOL, else an SPL mint address.
mint_decimalsDecimals of the token (null for SOL).
expected_lamportsExpected amount in base units (see below). null = accept any amount.
received_lamportsAmount received so far, in base units.
statusLifecycle state (see below).
referenceYour own id (order id, user id, memo). Optional, opaque to us.
expires_atWhen the payment window closes. Always 10 minutes after creation (see below).

Assets: SOL or any SPL token#

Each intent accepts exactly one asset, chosen at creation:

  • No mint → native SOL. Amounts are in lamports (1 SOL = 1,000,000,000 lamports).
  • mint set → that SPL token. We fetch the mint's decimals on-chain (which also validates it exists), and amounts are in the token's base units.

Base units = the smallest indivisible unit. To convert:

text
base_units = ui_amount × 10^decimals
ui_amount  = base_units ÷ 10^decimals

Examples:

  • 0.5 SOL (9 decimals) → 500000000 lamports.
  • 100 USDC (6 decimals) → 100000000 base units.
  • 100 units of a 9-decimal token → 100000000000 base units.

Both classic SPL Token and Token-2022 mints are supported automatically.


The settlement lifecycle#

Once a payment arrives, we settle it automatically: sweep the funds to your sweep destination, refund any excess, and so on. You don't trigger this. You just observe the status.

Status values#

status (and the derived action in /status, which is a friendlier alias):

statusactionMeaning
pendingwaitingCreated, no payment detected yet.
underpaidunderpaidReceived less than expected. Waiting for the rest.
paidpaidReceived the expected amount (or any, for open-ended intents). About to sweep.
overpaidoverpaidReceived more than expected. Will sweep expected + refund excess.
sweeping / settlingpaidSettlement transaction in progress.
sweptsweptTerminal success. Funds are in your destination wallet.
refundingrefundingBeing refunded (cancelled with a partial balance).
refundedrefundedTerminal. Funds returned to the sender.
cancelledcancelledTerminal. The checkout was cancelled: either the 10-minute window closed with nothing received, or it was cancelled explicitly (see cancellation). No funds were kept.
refund_failed / settle_failedrefund_failedTerminal-ish. Automatic settlement/refund exhausted retries; contact support.

The status you care about most is swept. That means the money is yours and settlement tells you exactly how much.

Normal happy path#

text
pending → paid → sweeping → swept

Overpayment#

text
pending → overpaid → settling → swept
(expected amount → your wallet; excess → back to sender)

Underpayment then completion#

text
pending → underpaid → (sender sends the rest) → paid → swept

Underpayment then expiry (SOL)#

text
pending → underpaid → (10-min window closes) → refunding → refunded
(the partial amount is returned to the sender)

Cancellation (timeout or explicit)#

text
pending → (window closes, or you cancel) → cancelled        (nothing received)
pending/underpaid → (cancel with partial funds) → refunding → refunded   (SOL, sender known)

Cancelling a checkout#

A checkout leaves pending/underpaid and becomes terminal in one of three ways:

  1. Automatic timeout: any intent past its 10-minute expires_at is cancelled by a background sweep (effective window ~10:00–10:30, allowing for sweep latency).
  2. Explicit cancel by your backend: POST /cancel { index } with your sk_ key (see API reference).
  3. Explicit cancel by the client: POST /intents/cancel { client_secret }, scoped to that one intent (used by the checkout SDK when the buyer closes the sheet).

In all three cases:

  • Nothing receivedcancelled (no funds to move).
  • Partial funds + a known sender (SOL)refundingrefunded; the partial balance is returned automatically.
  • Partial funds with no known sender, or an SPL tokencancelled with the funds left on the deposit address for manual return.
  • A payment that lands after cancellation is not lost. It's routed to refunding and returned.

Cancel is race-safe and idempotent: it only acts while the intent is pending/underpaid (a payment that already advanced the row wins, and the cancel is refused with 409), and cancelling an already-cancelled intent returns 200.


What "settled" means and the settlement object#

When an intent reaches swept, /status includes a settlement object: the exact on-chain amounts that were delivered. This is your source of truth for accounting:

json
"settlement": {
  "asset": "SOL",
  "decimals": 9,
  "destination_amount": 499995000,
  "destination_ui": 0.499995,
  "platform_fee_amount": 0,
  "platform_fee_ui": 0.0,
  "signatures": ["4bd..."]
}
  • destination_amount: base units that reached your destination wallet (net of platform fee, and for overpayments net of the refunded excess).
  • platform_fee_amount: base units sent to your platform-fee wallet (0 if off).
  • asset / decimals: so you can format correctly ("SOL" or the mint).
  • signatures: the on-chain sweep transaction signature(s) for verification.

Fees, refunds, and edge cases (summary)#

We handle these automatically:

  • Network fee. Every sweep pays the tiny Solana network fee (~5000 lamports). For SOL it comes out of the swept amount; for tokens we front it, so you receive the full token amount.
  • Platform fee (optional). Zuuppa (the platform) deducts a small fee from each sweep in the same transaction (no extra network fee). It's set globally by the platform, not by you. You receive your revenue net of it. destination_amount is always your actual net.
  • Overpayment. SOL: the expected amount is swept, the excess is refunded to the sender. Token: the full balance is swept to you (no token excess-refund).
  • Underpayment + expiry. SOL: the partial amount is refunded to the sender. Token: held for manual handling.
  • Wrong asset. A token sent to an intent expecting a different asset is automatically refunded to the sender on an independent track (reported in token_refunds). SOL sent to a token intent is held for manual review.

Idempotency & safety guarantees#

  • Detection is idempotent. The same on-chain transaction is never counted twice.
  • Settlement is race-safe and crash-safe. A payment is never swept twice, and a settlement interrupted mid-flight is retried automatically.
  • Retries with backoff. Transient failures retry, then surface as a *_failed status for attention, never silently lost.

Next: API reference →