Skip to content
Zuuppa
Dashboard

API Reference

Payments API

The complete HTTP contract for the core payments endpoints. All request and response bodies are JSON. Base URL https://api.zuuppa.com.

Authentication. The payments endpoints — POST /intents, GET /intents, GET /status, POST /sweep, POST /cancel — require an API key: Authorization: Bearer sk_live_... (or sk_test_...). Requests without a valid key get 401. The client-facing cs_ endpoints are covered in the Checkout SDK; the marketplace surface in the Marketplace API.

CORS is permissive, but treat /intents, /sweep, and /cancel as backend-only calls in production; your sk_ key must stay server-side.

Conventions#

  • Amounts are integers in the asset's base units (lamports for SOL, token base units for a mint). See Assets.
  • index = derivation_index, the integer identifying a payment intent.
  • Prices are integers in USD cents (amount_usd_cents, price_usd_cents). Every intent is USD-priced; the buyer chooses the asset.
  • Errors return a non-2xx status and a body of the form { "error": "human-readable message" }.

Error status codes#

CodeMeaning
400 Bad RequestInvalid input (bad mint, non-positive amount, nothing to sweep, index 0, a legacy fixed-token create).
401 UnauthorizedMissing/invalid API key.
404 Not FoundNo intent for the given index/reference, or an unknown seller in X-Zuuppa-On-Behalf-Of.
409 ConflictWell-formed but conflicts with current state: a payment already advanced past cancellation, no sweep destination configured, or a disabled marketplace.
500 Internal Server ErrorDatabase or internal error.
502 Bad GatewaySolana RPC error (upstream).

Endpoints#

Two public helpers — GET /health and GET /config — are documented in Public endpoints. The authenticated payments surface follows.

POST
sk_ API key

Create a payment intent. Allocates an index, derives a deposit address, and persists the intent. Every intent is priced in USD; the buyer picks the pay-in asset at checkout via POST /intents/select-token.

Request body#

amount_usd_centsintegerRequired
Required. Price in integer USD cents, > 0.
accepted_tokensarrayRequired
Required. The assets the buyer may pay in: [{ "kind": "sol" }, { "kind": "spl", "mint": "<mint>" }]. Every entry must be on the platform allowlist (see GET /allowed-tokens); an unlisted token is a 400. Decimals and symbol come from the allowlist, never from the caller.
referencestringdefault null
Your opaque id (order id, user id). Stored and returned; never interpreted. Also the idempotency key.
customerobjectdefault null
Buyer details to attach at creation: first_name, last_name, email, address — the same shape POST /intents/details accepts. email is required when the payment is on behalf of a marketplace seller; optional otherwise. Ignored on an idempotent reference retry.
application_fee_bpsintegerdefault null
Marketplaces only. The marketplace's cut on this one payment, 0–5000 bps. Valid only alongside X-Zuuppa-On-Behalf-Of; a 400 otherwise. Omit to inherit the seller's override, then the marketplace's default.
expires_in_secsintegerdefault null
Ignored. Every checkout is a fixed 10-minute window.

Optional headers#

X-Zuuppa-On-Behalf-Ofheader
A seller id (UUID) owned by the calling marketplace. The payment is created as that seller. Any bad value answers a uniform 404 seller not found. See the Marketplace API.
Warning

The legacy fixed-token path is gone.

Sending expected_lamports or mint on a create returns 400. Open-ended intents (no amount at all) no longer exist either: every intent carries a USD price.

Examples#

{
  "amount_usd_cents": 5000,
  "accepted_tokens": [
    { "kind": "sol" },
    { "kind": "spl", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }
  ],
  "reference": "order-1001"
}
200 OKthe created PaymentIntent
JSON
{
  "id": "d0714125-193a-4706-91d1-80854d829214",
  "derivation_index": 42,
  "address": "9xQe...pump",
  "client_secret": "cs_7pKf...9dQ2",
  "price_usd_cents": 5000,
  "mint": null,
  "mint_decimals": null,
  "expected_lamports": null,
  "status": "pending",
  "received_lamports": 0,
  "reference": "order-1001",
  "expires_at": "2026-07-26T13:10:00Z",
  "created_at": "2026-07-26T13:00:00Z",
  "updated_at": "2026-07-26T13:00:00Z",
  "refund_sender": null
}

mint, mint_decimals, and expected_lamports are null because the asset is not chosen yet, and payment_uri is absent for the same reason. Both fill in once the buyer calls POST /intents/select-token.

  • A payment for a seller additionally carries marketplace_fee_bps, the cut rate frozen at create time.

Save derivation_index (poll status with it) and show address to the payer. Forward client_secret to your client if it will drive checkout.

The payment_uri#

payment_uri is a Solana Pay URI for address, built server-side. Render it as your QR code rather than assembling one yourself:

Text
solana:<address>?amount=<ui_amount>&spl-token=<mint-if-token>&label=<your account name>

It is omitted while there is no amount to ask for (any USD-priced intent before token selection) and absent once the intent can no longer be paid (paid, swept, cancelled, refunding, refunded). It is present only while pending or underpaid, and on an underpaid intent the amount is the remaining shortfall. It is rebuilt on every status read — re-render from the latest response after a token selection or partial payment.

Errors#

CodeMessage
400payments are priced in USD; pass amount_usd_cents, not expected_lamports/mint
400amount_usd_cents is required / amount_usd_cents must be > 0
400accepted_tokens is required for USD-priced intents / accepted_tokens must not be empty
400token not accepted on this platform / SOL is not accepted on this platform
400customer.email is required for a payment taken on behalf of a seller
400invalid email / country must be an ISO 3166-1 alpha-2 code
400application_fee_bps applies only to a payment for a seller (name one with the X-Zuuppa-On-Behalf-Of header)
400application_fee_bps must be between 0 and 5000
404seller not found (any bad X-Zuuppa-On-Behalf-Of)
409account has no valid sweep destination configured
Tip

Idempotency (recommended)

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
sk_ API key

The pay-in tokens this platform currently accepts — the exact set you may put in an intent's accepted_tokens. Use it to build your checkout's token picker instead of letting a buyer name any mint; a token not listed here is rejected at POST /intents.

200 OK
JSON
{
  "tokens": [
    {
      "mint": "So11111111111111111111111111111111111111112",
      "is_native": true,
      "name": "Solana",
      "ticker": "SOL",
      "decimals": 9,
      "image_url": "https://assets.zuuppa.com/tokens/.../img-1a2b3c4d.png"
    },
    {
      "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "is_native": false,
      "name": "USD Coin",
      "ticker": "USDC",
      "decimals": 6,
      "image_url": null
    }
  ]
}

is_native marks native SOL — build its accepted_tokens entry as { "kind": "sol" }; every other row is an SPL token you reference as { "kind": "spl", "mint": "<mint>" }. image_url is null until an image has been uploaded (or if image storage isn't configured).

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 (this one does not include payment_uri; it returns the stored intent alone). Errors: 404 no intent for that reference.

cURL
curl -H 'Authorization: Bearer sk_live_...' \
  "https://api.zuuppa.com/intents?reference=order-1001"
GET
sk_ API key

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 param index (required) — the derivation_index from POST /intents.

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:15:00Z",
  "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 plus:

actionstring
Machine-friendly state: waiting, underpaid, paid, overpaid, swept, refunding, refunded, cancelled, refund_failed. Always present.
messagestring
Human-readable status message, safe to show a payer. Always present.
payment_uristring
Solana Pay URI for the QR code. Present only while payable; omitted once past payment; asks for the shortfall while underpaid.
shortfall_lamportsinteger
How many more base units are needed. Present only when underpaid.
token_refundsarray
Wrong-token refunds: [{ "mint": "...", "status": "pending|settling|refunded|failed" }]. Independent of status. Present only if non-empty.
customer_detailsobject
Buyer name, email, and address. Present only if collected.
settlementobject
Exact settled amounts (see below). Present only after the first sweep.

The settlement object#

The source of truth for accounting.

assetstring
"SOL" or the SPL mint address.
decimalsinteger
9 for SOL, else mint decimals.
destination_amountinteger
Base units delivered to your destination wallet (net of platform fee / refunded excess).
destination_uinumber
destination_amount ÷ 10^decimals, for display.
platform_fee_amountinteger
Base units sent to the platform wallet (0 if fees off).
platform_fee_uinumber
Decimal-adjusted platform fee.
marketplace_fee_amountinteger
Base units sent to the marketplace's cut wallet. 0 unless taken for a seller.
marketplace_fee_uinumber
Decimal-adjusted marketplace cut.
signaturesstring[]
On-chain sweep transaction signature(s).
Note
The _ui fields are floats — fine for display, wrong for a ledger. For accounting, use the integer base-unit fields, or the exact decimal strings that webhooks carry.

Errors: 404 no intent for that index.

POST
sk_ API key

Manually trigger a sweep of an intent's balance to the destination. Usually unnecessary — auto-settle does this for you. Use it to force a retry.

Important

This moves funds.

Treat it as a privileged backend-only operation. Do not expose it to untrusted callers.

Request body#

JSON
{ "index": 42 }
200 OK
JSON
{
  "signature": "4bd...",
  "lamports_swept": 499995000,
  "platform_fee_lamports": 0,
  "from": "9xQe...pump",
  "to": "BLTpUS6b..."
}
  • lamports_swept: base units delivered to the destination (net of platform fee).
  • platform_fee_lamports: base units to the platform wallet.

Errors#

CodeMessage
400index 0 is reserved ...
400nothing to sweep (balance ...)
404no intent for that index
502on RPC failure.
POST
sk_ API key

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.
  • A webhook fires either way: intent.cancelled, or intent.refunding then intent.refunded. An idempotent second cancel does not duplicate the event.

Errors#

CodeMessage
404no intent for that index
409payment already received; cannot cancel
Note
There is also a client-facing POST /intents/cancel that takes a client_secret instead of your sk_ key, for cancelling from an untrusted client. See the Checkout SDK.

The PaymentIntent object#

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

idstring (UUID)
Internal unique id.
derivation_indexinteger
The index / stable id. Poll with this.
addressstring
Deposit address to show the payer.
client_secretstring | null
One-time single-intent token for client-facing endpoints. Returned only on the first POST /intents; null/omitted elsewhere.
price_usd_centsinteger | null
The USD price in cents.
mintstring | null
Pay-in asset: null = SOL, else SPL mint. null until token selection.
mint_decimalsinteger | null
Token decimals (null for SOL, and before selection).
expected_lamportsinteger | null
Expected amount, base units. null until token selection.
received_lamportsinteger
Amount received so far, base units (gross).
statusstring
Lifecycle state (see status values).
referencestring | null
Your opaque id.
expires_atstring (ISO 8601) | null
Payment window close time.
created_at / updated_atstring (ISO 8601)
Timestamps.
refund_senderstring | null
Address the server will/did refund to (the payer's address, once known).
marketplace_fee_bpsinteger | null
Marketplace payments only. The cut rate frozen at create time.
marketplace_fee_centsinteger | null
Marketplace payments only. The cut in USD cents, written at settlement.
customer_detailsobject | null
Buyer details (first_name, last_name, email, address), all optional. Omitted when none collected.

© 2026 Zuuppa. All rights reserved.