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#
| Code | Meaning |
|---|---|
400 Bad Request | Invalid input (bad mint, non-positive amount, nothing to sweep, index 0, a legacy fixed-token create). |
401 Unauthorized | Missing/invalid API key. |
404 Not Found | No intent for the given index/reference, or an unknown seller in X-Zuuppa-On-Behalf-Of. |
409 Conflict | Well-formed but conflicts with current state: a payment already advanced past cancellation, no sweep destination configured, or a disabled marketplace. |
500 Internal Server Error | Database or internal error. |
502 Bad Gateway | Solana RPC error (upstream). |
Endpoints#
Two public helpers — GET /health and GET /config — are documented in Public endpoints. The authenticated payments surface follows.
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#
> 0.[{ "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.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.X-Zuuppa-On-Behalf-Of; a 400 otherwise. Omit to inherit the seller's override, then the marketplace's default.Optional headers#
404 seller not found. See the Marketplace API.The legacy fixed-token path is gone.
Sendingexpected_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"
}{
"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:
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#
| Code | Message |
|---|---|
400 | payments are priced in USD; pass amount_usd_cents, not expected_lamports/mint |
400 | amount_usd_cents is required / amount_usd_cents must be > 0 |
400 | accepted_tokens is required for USD-priced intents / accepted_tokens must not be empty |
400 | token not accepted on this platform / SOL is not accepted on this platform |
400 | customer.email is required for a payment taken on behalf of a seller |
400 | invalid email / country must be an ISO 3166-1 alpha-2 code |
400 | application_fee_bps applies only to a payment for a seller (name one with the X-Zuuppa-On-Behalf-Of header) |
400 | application_fee_bps must be between 0 and 5000 |
404 | seller not found (any bad X-Zuuppa-On-Behalf-Of) |
409 | account has no valid sweep destination configured |
Idempotency (recommended)
If you pass areference, 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).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.
{
"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 -H 'Authorization: Bearer sk_live_...' \
"https://api.zuuppa.com/intents?reference=order-1001"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.
{
"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:
waiting, underpaid, paid, overpaid, swept, refunding, refunded, cancelled, refund_failed. Always present.underpaid.underpaid.[{ "mint": "...", "status": "pending|settling|refunded|failed" }]. Independent of status. Present only if non-empty.The settlement object#
The source of truth for accounting.
destination_amount ÷ 10^decimals, for display.0 unless taken for a seller._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.
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.
This moves funds.
Treat it as a privileged backend-only operation. Do not expose it to untrusted callers.Request body#
{ "index": 42 }{
"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#
| Code | Message |
|---|---|
400 | index 0 is reserved ... |
400 | nothing to sweep (balance ...) |
404 | no intent for that index |
502 | on RPC failure. |
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#
{ "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 with409. - Idempotent: cancelling an already-
cancelled/refunding/refundedintent returns200with 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, orintent.refundingthenintent.refunded. An idempotent second cancel does not duplicate the event.
Errors#
| Code | Message |
|---|---|
404 | no intent for that index |
409 | payment already received; cannot cancel |
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.
POST /intents; null/omitted elsewhere.null = SOL, else SPL mint. null until token selection.first_name, last_name, email, address), all optional. Omitted when none collected.