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:
- Derive a unique deposit address for it, never reused across intents.
- Give it a stable
derivation_indexyou use to track it. - Persist it so detection and settlement survive restarts.
An intent records:
| Field | Meaning |
|---|---|
derivation_index | The stable integer id. Use this to poll status and reference the payment. |
address | The Solana deposit address to show the payer. |
mint | The asset accepted: null = native SOL, else an SPL mint address. |
mint_decimals | Decimals of the token (null for SOL). |
expected_lamports | Expected amount in base units (see below). null = accept any amount. |
received_lamports | Amount received so far, in base units. |
status | Lifecycle state (see below). |
reference | Your own id (order id, user id, memo). Optional, opaque to us. |
expires_at | When the payment window closes. Always 10 minutes after creation (see below). |
Fixed 10-minute window. Every intent expires exactly 10 minutes after it's
created. This is a fixed guarantee: the expires_in_secs request field is
accepted for backward compatibility but ignored. An intent that isn't paid in
time is auto-cancelled (partial funds are refunded where possible; see the
lifecycle below).
Naming note: the fields are called expected_lamports / received_lamports
for historical reasons, but they hold base units of the intent's asset:
lamports for SOL, token base units for an SPL token. Treat them as generic
integer amounts.
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). mintset → 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:
base_units = ui_amount × 10^decimals
ui_amount = base_units ÷ 10^decimalsExamples:
- 0.5 SOL (9 decimals) →
500000000lamports. - 100 USDC (6 decimals) →
100000000base units. - 100 units of a 9-decimal token →
100000000000base 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):
| status | action | Meaning |
|---|---|---|
pending | waiting | Created, no payment detected yet. |
underpaid | underpaid | Received less than expected. Waiting for the rest. |
paid | paid | Received the expected amount (or any, for open-ended intents). About to sweep. |
overpaid | overpaid | Received more than expected. Will sweep expected + refund excess. |
sweeping / settling | paid | Settlement transaction in progress. |
swept | swept | Terminal success. Funds are in your destination wallet. |
refunding | refunding | Being refunded (cancelled with a partial balance). |
refunded | refunded | Terminal. Funds returned to the sender. |
cancelled | cancelled | Terminal. 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_failed | refund_failed | Terminal-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#
pending → paid → sweeping → sweptOverpayment#
pending → overpaid → settling → swept
(expected amount → your wallet; excess → back to sender)Underpayment then completion#
pending → underpaid → (sender sends the rest) → paid → sweptUnderpayment then expiry (SOL)#
pending → underpaid → (10-min window closes) → refunding → refunded
(the partial amount is returned to the sender)Cancellation (timeout or explicit)#
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:
- Automatic timeout: any intent past its 10-minute
expires_atis cancelled by a background sweep (effective window ~10:00–10:30, allowing for sweep latency). - Explicit cancel by your backend:
POST /cancel { index }with yoursk_key (see API reference). - 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 received →
cancelled(no funds to move). - Partial funds + a known sender (SOL) →
refunding→refunded; the partial balance is returned automatically. - Partial funds with no known sender, or an SPL token →
cancelledwith the funds left on the deposit address for manual return. - A payment that lands after cancellation is not lost. It's routed to
refundingand 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.
No webhook fires on cancellation or timeout. React to it by polling /status
or watching the dashboard. See Webhooks.
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:
"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.
destination_amount can be less than received_lamports. That's expected.
received_lamports is the gross amount the payer sent; destination_amount is
what you actually net after fees/refunds. Use settlement for accounting.
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_amountis 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
*_failedstatus for attention, never silently lost.
Next: API reference →