3. API Reference

The complete HTTP contract. All request/response bodies are JSON.

  • Base URL: https://api.zuuppa.com (use your assigned base URL if different).
  • Authentication: every payments endpoint requires an API key. Send Authorization: Bearer sk_live_... (or sk_test_...). Create the key in the dashboard (API keys). Requests without a valid key get 401. Keep the key server-side. GET /health is open.

Conventions#

  • Amounts are integers in the asset's base units (lamports for SOL, token base units for a mint). See Concepts.
  • index = derivation_index, the integer identifying a payment intent.
  • Errors return a non-2xx status and a body of the form:
    json
    { "error": "human-readable message" }

Error status codes#

CodeMeaning
400 Bad RequestInvalid input (bad mint, non-positive amount, nothing to sweep).
401 UnauthorizedMissing or invalid API key.
404 Not FoundNo intent for the given index/reference.
500 Internal Server ErrorInternal error.
502 Bad GatewayUpstream Solana RPC error.

POST /intents#

Auth: requires Authorization: Bearer sk_....

Create a payment intent. Derives a unique deposit address and returns it.

Pricing modes#

An intent has a mode that determines how its amount is set. Both modes are priced in USD. Every intent carries a USD price and the buyer picks a pay-in token at checkout:

  • custom (default): you set a USD price with amount_usd_cents, plus the accepted_tokens the buyer may pay in.
  • order: you don't pass an amount. Send a cart of catalog items (created in the dashboard, priced in USD); the server prices the order from the stored items. Requires accepted_tokens.

Every intent leaves mint, mint_decimals, and expected_lamports null at creation. The buyer picks one of accepted_tokens via POST /intents/select-token, which converts the USD price to that token's base units at spot and locks the asset. Settlement is always single-asset.

Request body:

FieldTypeDefaultDescription
modestring"custom""custom" or "order".
amount_usd_centsintegercustom mode: required. USD price in integer cents (e.g. 1250 = $12.50). Must be > 0. Rejected in order mode.
accepted_tokensarrayRequired (both modes). Pay-in tokens the buyer may choose among. Each element is { "kind": "sol" } or { "kind": "spl", "mint": "<mint>" }. Decimals/symbol are resolved server-side (the buyer can't inject them).
cartarrayorder mode: required. Line items: [{ "item_id": "<uuid>", "quantity": <n> }]. Priced from your account's catalog items.
referencestringnullYour opaque id (order id, user id). Stored and returned; never interpreted.
expires_in_secsintegernullIgnored. Accepted for backward compatibility, but every checkout is a fixed 10-minute window (see Concepts).

Examples

USD-priced $12.50 custom payment, buyer pays in SOL or USDC (expires 10 minutes after creation):

json
{
  "amount_usd_cents": 1250,
  "accepted_tokens": [
    { "kind": "sol" },
    { "kind": "spl", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }
  ],
  "reference": "order-1003"
}

Custom payment priced at $5.00, SOL only:

json
{
  "amount_usd_cents": 500,
  "accepted_tokens": [ { "kind": "sol" } ],
  "reference": "order-1001"
}

Order priced from catalog items, buyer pays in SOL:

json
{
  "mode": "order",
  "cart": [
    { "item_id": "7b9c...e1", "quantity": 2 },
    { "item_id": "a4f0...92", "quantity": 1 }
  ],
  "accepted_tokens": [ { "kind": "sol" } ],
  "reference": "order-1004"
}

Response 200 OK: the created intent (see the object reference):

json
{
  "id": "d0714125-193a-4706-91d1-80854d829214",
  "derivation_index": 42,
  "address": "9xQe...pump",
  "client_secret": "cs_7pKf...9dQ2",
  "mint": null,
  "mint_decimals": null,
  "expected_lamports": null,
  "status": "pending",
  "received_lamports": 0,
  "reference": "order-1003",
  "mode": "custom",
  "price_usd_cents": 1250,
  "accepted_tokens": [
    { "kind": "sol" },
    { "kind": "spl", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "decimals": 6, "symbol": "USDC" }
  ],
  "expires_at": "2026-07-26T13:10:00Z",
  "created_at": "2026-07-26T13:00:00Z",
  "updated_at": "2026-07-26T13:00:00Z",
  "refund_sender": null
}

For a USD-priced intent the asset fields stay null until the buyer selects a token. The create response instead carries the price and options:

json
{
  "mode": "custom",
  "mint": null,
  "mint_decimals": null,
  "expected_lamports": null,
  "price_usd_cents": 1250,
  "accepted_tokens": [
    { "kind": "sol" },
    { "kind": "spl", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "decimals": 6, "symbol": "USDC" }
  ],
  "status": "pending"
}

Save derivation_index. You'll poll status with it. Show address to the payer.

client_secret (cs_...) is a one-time, single-intent token returned only on the first create (only its hash is stored, so it is never shown again; an idempotent-reference retry returns client_secret: null). It lets an untrusted client (a mobile app / browser) read only this one intent's status and attach buyer details without your secret sk_ key. See Client-facing endpoints. Forward it to your client if you use those; otherwise ignore it. Treat it like a password for that one intent: a leak exposes only that single payment, never your account or other intents.

Errors

  • 400 mode must be 'custom' or 'order'
  • 400 amount_usd_cents is required for custom payments
  • 400 amount_usd_cents must be > 0
  • 400 custom payments are priced in USD; pass amount_usd_cents, not expected_lamports/mint
  • 400 accepted_tokens is required for USD-priced intents
  • 400 accepted_tokens must not be empty
  • 400 invalid mint address / invalid mint: ... (an accepted_tokens SPL mint not found on-chain)
  • 400 cart is only valid for order mode
  • 400 cart is required for order mode
  • 400 order mode prices from the cart; do not pass an amount or mint
  • 400 quantity must be > 0
  • 400 unknown or inactive item (a cart item_id isn't one of your active catalog items)
  • 400 order total exceeds maximum

If you pass a reference, POST /intents is idempotent on it: retrying the same reference (network retry, crash, double-tap) returns the same intent (same derivation_index and address) instead of creating a second deposit address. Always pass a stable unique reference (e.g. your order id).


GET /intents?reference={reference}#

Auth: requires Authorization: Bearer sk_....

Recover an intent by its reference. For example, if the POST /intents response was lost, look it up instead of creating a new one.

Response 200 OK: the same PaymentIntent object as POST /intents. Errors: 404 no intent for that reference.

bash
curl -H 'Authorization: Bearer sk_live_...' \
  "https://api.zuuppa.com/intents?reference=order-1001"

GET /status?index={index}#

Auth: requires Authorization: Bearer sk_....

The endpoint your app polls. Returns the intent's full state plus a human-readable message, the exact settled amounts (once swept), and any wrong-token refunds.

Query params

  • index (required): the derivation_index from POST /intents.

Response 200 OK:

json
{
  "id": "d0714125-...",
  "derivation_index": 42,
  "address": "9xQe...pump",
  "mint": null,
  "mint_decimals": null,
  "expected_lamports": 500000000,
  "status": "swept",
  "received_lamports": 500000000,
  "reference": "order-1001",
  "expires_at": "2026-07-26T13:10:00Z",
  "created_at": "2026-07-26T13:00:00Z",
  "updated_at": "2026-07-26T13:02:11Z",
  "refund_sender": "Fx3X...gsy",

  "action": "swept",
  "message": "Payment received and settled.",
  "settlement": {
    "asset": "SOL", "decimals": 9,
    "destination_amount": 499995000, "destination_ui": 0.499995,
    "platform_fee_amount": 0, "platform_fee_ui": 0.0,
    "signatures": ["4bd..."]
  }
}

Response fields: the flattened PaymentIntent (see below) plus:

FieldTypePresentDescription
actionstringalwaysMachine-friendly state: waiting, underpaid, paid, overpaid, swept, refunding, refunded, cancelled, refund_failed.
messagestringalwaysHuman-readable status message, safe to show a payer.
shortfall_lamportsintegeronly when underpaidHow many more base units are needed to complete the payment.
token_refundsarrayonly if non-emptyWrong-token refunds for this address: [{ "mint": "...", "status": "pending|settling|refunded|failed" }]. Independent of the SOL status.
settlementobjectonly after first sweepExact settled amounts. See below.
customer_detailsobjectonly if collectedBuyer details submitted via POST /intents/details. See the object reference.

settlement object (source of truth for accounting):

FieldTypeDescription
assetstring"SOL" or the SPL mint address.
decimalsinteger9 for SOL, else mint decimals.
destination_amountintegerBase units delivered to your destination wallet (net of platform fee / refunded excess).
destination_uinumberdestination_amount ÷ 10^decimals, for display.
platform_fee_amountintegerBase units sent to the platform wallet (0 if fees off).
platform_fee_uinumberDecimal-adjusted platform fee.
signaturesstring[]On-chain sweep transaction signature(s).

Errors

  • 404 no intent for that index

POST /sweep#

Auth: requires Authorization: Bearer sk_....

Manually trigger a sweep of an intent's balance to your destination. Usually unnecessary, since settlement is automatic. Use it only to force a retry.

Request body

json
{ "index": 42 }

Response 200 OK:

json
{
  "signature": "4bd...",
  "lamports_swept": 499995000,
  "platform_fee_lamports": 0,
  "from": "9xQe...pump",
  "to": "BLTpUS6b..."
}

Errors

  • 400 nothing to sweep (balance ...)
  • 404 no intent for that index
  • 502 on RPC failure.

POST /cancel#

Auth: requires Authorization: Bearer sk_....

Cancel one of your intents before it's paid, for example when the buyer abandoned checkout. See the cancellation lifecycle for what happens to any partial funds.

Request body

json
{ "index": 42 }

Response 200 OK: the intent's status (same shape as GET /status), now cancelled (or refunding/refunded if a partial SOL balance is being returned).

Behavior

  • Only acts while the intent is pending/underpaid. If a payment already advanced it, the cancel is refused with 409.
  • Idempotent: cancelling an already-cancelled/refunding/refunded intent returns 200 with its current status.
  • Race-safe: if a payment confirms at the same instant, the payment wins and the cancel returns 409.

Errors

  • 404 no intent for that index
  • 409 payment already received; cannot cancel

Client-facing endpoints#

These two endpoints are authenticated by a per-intent client_secret (cs_...) instead of your secret sk_ key, so they are safe to call from an untrusted client such as a mobile app or browser. Each is scoped to the single intent whose client_secret is presented: it can never read or modify your account or any other intent. Get the client_secret from the POST /intents response and forward it to your client.

GET /intents/status?client_secret={cs_...}#

Auth: none. The client_secret is the credential.

Read a single intent's status. Returns the same body as GET /status (the flattened PaymentIntent plus action, message, shortfall_lamports, token_refunds, settlement).

Query params

  • client_secret (required): the intent's cs_... token.
bash
curl "https://api.zuuppa.com/intents/status?client_secret=cs_7pKf...9dQ2"

Errors

  • 400 invalid client_secret (missing/malformed).
  • 404 no intent for that client_secret.

POST /intents/cancel#

Auth: none. The client_secret is the credential.

Cancel this one intent from the client (used by the checkout SDK when the buyer closes the sheet). Same lifecycle, idempotency, and race-safety as the sk_ POST /cancel.

Request body

json
{ "client_secret": "cs_7pKf...9dQ2" }

Response 200 OK: the intent's status, now cancelled (or refunding/refunded).

Errors

  • 400 invalid client_secret.
  • 404 no intent for that client_secret.
  • 409 payment already received; cannot cancel.

POST /intents/details#

Auth: none. The client_secret is the credential.

Attach optional buyer details (name, email, international address) to the intent, e.g. for a receipt or shipping. All fields are optional; send only the ones you collect. The details are stored on the intent and surfaced back on GET /status, GET /intents/status, the dashboard, and webhook payloads as customer_details.

Only accepted while the intent is still open (pending/underpaid). A later submission replaces the previously stored details.

Request body

FieldTypeDescription
client_secretstringRequired. The intent's cs_... token.
first_namestringOptional.
last_namestringOptional.
emailstringOptional. Validated as an email address when present.
addressobjectOptional. International address (all parts optional; see below).

address object

FieldTypeDescription
countrystringISO 3166-1 alpha-2 code (e.g. US, GB, JP). Stored upper-cased.
line1stringStreet address.
line2stringApartment, suite, etc.
citystringCity / locality.
statestringState / province / region.
postal_codestringPostal / ZIP code.
json
{
  "client_secret": "cs_7pKf...9dQ2",
  "first_name": "Ada",
  "last_name": "Lovelace",
  "email": "ada@example.com",
  "address": {
    "country": "GB",
    "line1": "12 Baker St",
    "city": "London",
    "postal_code": "NW1 6XE"
  }
}

Response 200 OK: the updated status (same shape as GET /intents/status), now including customer_details.

Errors

  • 400 invalid client_secret / invalid email / country must be an ISO 3166-1 alpha-2 code / no details provided.
  • 404 no open intent for that client_secret (unknown, or already past payment).

POST /intents/select-token#

Auth: none. The client_secret is the credential.

For a USD-priced intent (created with amount_usd_cents or in order mode), the buyer picks one of the merchant's accepted_tokens. The server converts the USD price to that token's base units at the current spot rate (ceil-rounded, never under-collecting) and locks the asset, setting mint, mint_decimals, and expected_lamports on the intent. The buyer can only choose among the merchant's tokens; they can never set the amount.

Re-selectable while the intent is still pending (each pick re-converts at a fresh rate); refused once a payment lands. Selecting does not extend expires_at.

Request body

FieldTypeDescription
client_secretstringRequired. The intent's cs_... token.
mintstring | nullThe chosen pay-in mint. Omit or send null for native SOL. Must match one of the intent's accepted_tokens.
json
{ "client_secret": "cs_7pKf...9dQ2", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }

Response 200 OK: the updated status (same shape as GET /intents/status), now with mint/mint_decimals/expected_lamports populated for the locked asset.

Errors

  • 400 invalid client_secret.
  • 400 this intent's amount is already fixed (not a USD-priced intent).
  • 400 token not accepted for this intent (mint isn't one of accepted_tokens).
  • 404 no intent for that client_secret.
  • 409 payment already in progress (intent is past pending, so it can't re-lock).

GET /intents/quote?client_secret={cs_...}#

Auth: none. The client_secret is the credential.

Preview what each accepted token would cost right now, without locking anything (pure read). Use it to show the buyer per-token amounts before they pick.

Query params

  • client_secret (required): the intent's cs_... token.

Response 200 OK:

json
{
  "price_usd_cents": 1250,
  "expires_in_seconds": 600,
  "quotes": [
    { "symbol": "SOL", "decimals": 9, "expected_lamports": 83333334 },
    { "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "symbol": "USDC", "decimals": 6, "expected_lamports": 12500000 }
  ]
}
FieldTypeDescription
price_usd_centsintegerThe intent's USD price.
expires_in_secondsintegerHow long a quote is considered fresh (indicative).
quotesarrayOne entry per accepted token: mint (omitted for SOL), symbol, decimals, expected_lamports (amount in that token's base units).

Amounts are indicative. The authoritative amount is set when the buyer calls POST /intents/select-token.

Errors

  • 400 invalid client_secret.
  • 400 this intent's amount is already fixed (not a USD-priced intent).
  • 404 no intent for that client_secret.

GET /health#

Liveness probe. Returns 200 OK with body ok. No auth, no JSON.


PaymentIntent object#

The core object returned by /intents and flattened into /status. Fields:

FieldTypeDescription
idstring (UUID)Internal unique id.
derivation_indexintegerThe index / stable id. Poll with this.
addressstringDeposit address to show the payer.
client_secretstring | nullOne-time single-intent token for client-facing endpoints. Returned only on the first POST /intents; omitted elsewhere.
mintstring | nullAccepted asset: null = SOL, else SPL mint.
mint_decimalsinteger | nullToken decimals (null for SOL).
expected_lamportsinteger | nullExpected amount, base units. null = any, or a USD-priced intent whose token isn't locked yet.
received_lamportsintegerAmount received so far, base units (gross).
statusstringLifecycle state (see Concepts).
referencestring | nullYour opaque id.
modestringPricing mode: "custom" or "order".
price_usd_centsinteger | nullUSD price in cents for a USD-denominated intent; null for a fixed-token amount.
accepted_tokensarray | nullPay-in tokens the buyer may choose among (USD-priced intents). Each: { "kind": "sol" } or { "kind": "spl", "mint", "decimals", "symbol" }. null once the asset is fixed/locked.
expires_atstring (ISO 8601) | nullPayment window close time.
created_atstring (ISO 8601)Creation time.
updated_atstring (ISO 8601)Last state change.
refund_senderstring | nullAddress we will/did refund to (the payer's address, once known).
customer_detailsobject | nullBuyer details submitted via POST /intents/details (name, email, address). Omitted when none were collected.

customer_details object (present only when details were submitted; each sub-field appears only if provided):

FieldTypeDescription
first_namestringBuyer's first name.
last_namestringBuyer's last name.
emailstringBuyer's email.
addressobjectInternational address: country (ISO 3166-1 alpha-2), line1, line2, city, state, postal_code.

Next: Integration guide →