Skip to main content
A payment intent is how you charge a customer. You post a fiat amount, SouthPay locks an exchange rate and allocates one on-chain deposit address per asset your store accepts, and you send the customer to the hosted checkout page. Which assets appear is decided by your store settings, not by anything in the request. For the walkthrough, see the payment intents guide. This page is the field-level reference. Every path below is relative to https://api.southpay.io/api/v2. All amounts you send are fiat major units; all crypto amounts you read back are atomic-unit strings.

Authentication and scope

Reads require payments:read, writes require payments:write. A secret API key (sp_sk_…), an OAuth token, or a dashboard session all work. A publishable key (sp_pk_…) can call POST /payments and nothing else. Any other path returns 403 publishable_key_forbidden.

Create a payment intent

POST /payments. Scope payments:write. Publishable keys allowed. Idempotency-Key optional. All fields go inside a required payment_intent object.
string
required
Fiat amount in major units, as a decimal string ("49.99"). Must be greater than zero. Multiplied by 100 and stored as settlement_amount_cents.
string
required
Uppercase ISO 4217 code. One of USD, EUR, GBP, HKD, SGD, JPY, DKK, CHF, SEK, NOK. This prices the payment; it does not restrict which crypto assets the customer may pay with.
string
Where checkout sends the customer after a successful payment.
string
Where checkout sends the customer after a failure or expiry.
string
Your own order reference. Maximum 255 characters. Filterable on the list endpoint.
string
Short label shown on the checkout page. Maximum 150 characters.
string
Longer description shown on the checkout page. Maximum 500 characters.
string
Product or order image shown on checkout. Must be https and at most 2048 characters. Surfaces as display_image_url on the detail view.
integer
default:"3600"
How long the payment stays payable, in seconds. Minimum 300 (5 minutes), maximum 86400 (24 hours). Omit it for the 60 minute default. The resolved deadline comes back as expires_at.
object
Key-value pairs echoed back on the intent and in every payment webhook. At most 50 keys, and every value must be a string. Numbers and booleans are rejected.
object[]
Order breakdown shown on checkout. At most 50 items.
Send the customer to hosted_url. It is https://pay.southpay.io/{reference} unless your store has a verified custom domain.
An intent expires 60 minutes after creation unless you send expires_in. Do not create the intent until the customer is ready to pay.The crypto amounts are quoted when the intent is created and stay fixed for the whole window, so a longer expires_in leaves you exposed to more exchange-rate movement. A customer who funds the payment by card can push expires_at out to 6 hours regardless of what you set, because the card leg needs that time to settle.

Response fields

string
required
Bare UUID. There is no prefixed-id scheme anywhere in this API.
string
required
16 uppercase alphanumeric characters. This is the checkout token and the value that appears as data.object.id in webhooks.
string
The customer-facing checkout page.
string
required
One of pending, processing, completed, expired, failed, refunded. See lifecycle.
string | null
Why the intent reached its current status. See payment lifecycle.
string
required
Fiat amount as a string with exactly two decimals, for example "49.99". Note that the webhook envelope carries settlement_amount_cents as an integer instead.
string
required
The fiat currency the intent is priced in.
boolean
required
True when status is completed, expired, refunded or failed. The intent will not change again.
string
required
ISO 8601. 60 minutes after created_at, or expires_in seconds after it when you sent that on create.
string | null
The underlying settlement record: pending, processing, completed, failed, or null. It is never "settled".
string | null
completed, pending, failed, skipped, or null when no auto-swap applies.
string | null
Status of the most recent reconciliation snapshot. See Reconciliation.
object[]
One deposit address per asset your store accepts. Returned by create and retrieve.

Create errors

Validation failures on this endpoint return 422 with the flat envelope {"error": "<message>"}. An unsupported currency also returns 422, but through the global handler, so it uses the structured envelope: {"error": {"message": "Unsupported settlement currency: XYZ", "code": null}}. Write your client to handle both shapes.
Omitting currency entirely, or omitting the payment_intent wrapper, currently returns 500 rather than a validation error. Always send both.
POST /payments is rate limited to 1,200 requests per minute in live mode, with a burst of 200, counted against your store rather than the individual key. Over the limit you get 429 {"error": {"message": "Too many requests", "code": "rate_limited"}} with a Retry-After header in seconds. See Rate limits for every throttled path.

Retrieve a payment intent

GET /payments/{id}. Scope payments:read. {id} is the intent UUID, not the reference. Returns the detail view: everything from create, plus ledger-derived financials, the on-chain transaction, and the full event history.

Detail-only fields

string
The API-hosted checkout endpoint, https://api.southpay.io/api/v2/checkout/{reference}. Distinct from hosted_url, which is the customer-facing page.
string | null
The image_url you supplied on create.
object[] | null
The line items you supplied on create.
string
Fiat value of confirmed funds, two decimals. "0.00" when nothing has confirmed.
string | null
Timestamp of the transition into completed, falling back to the last transition.
boolean
When true the customer was charged the SouthPay fee on top of the order amount.
object | null
Null until fee revenue is booked.
object | null
Ledger-derived fiat view. Null until crypto is received.
object | null
The confirmed deposit transaction, or the most recent one if none has confirmed. Null when nothing has arrived.
object | null
Present once an address is confirmed. excess_atomic is clamped to "0" for exact and under payments, so a non-zero value here is the only reliable signal of a genuine overpayment.
object
Your own store details, for rendering receipts. legal_name, country, registration_number, tax_id and website are null until a verification is approved.
object | null
The most recent card-to-crypto funding session, when card payments are enabled for your store. Null when the customer paid from their own wallet.status here is the funding session, not the payment. A paid session means the card charge went through; the intent still settles from the on-chain deposit that follows.
object | null
Why no card option could be offered for this intent. Always null once a funding session exists, whatever its outcome.Carries reason and a human-readable message. below_min_amount and above_max_amount also carry charge_amount_cents, min_amount_cents, max_amount_cents and currency, so you can name the actual bound. See Card payments for the full list of reasons.
object | null
Populated only when an auto-swap has moved past pending and has not failed. Keys: status, sell_amount, sell_coin_symbol, sell_chain_symbol, buy_amount, buy_coin_symbol, buy_chain_symbol, estimated, expected_buy_amount, actual_buy_amount, slippage_bps.
object[]
Same rows as the events endpoint: id, event_type, from_status, to_status, payload, created_at.
object[]
The same events under different key names: type, from, to, at, payload. No id.
An unknown or out-of-mode id returns 404 with {"error": {"message": "…", "code": null}}.

List payment intents

GET /payments. Scope payments:read. Returns the list view: the create fields plus settlement_status and auto_swap_status, without payment_addresses. Ordered newest first.
string
Filter by status. Unrecognized values are ignored rather than rejected.
string
Case-insensitive substring match on reference.
string
Exact match on the order_id you set at create time.
string
ISO date. Includes intents created on or after this day.
string
ISO date. Includes intents created up to and including this day.
integer
default:"1"
integer
default:"10"
Between 1 and 100.
status=confirming is accepted by the filter but confirming is not a payment intent status. It always matches zero rows. confirming exists only as a transaction status inside the transaction object. Do not use it as a filter.
Pagination is not uniform across this API. Payments, billing, agentic payments and platform endpoints wrap the page in meta. Payouts, agentic payouts, balance swaps and x402 settlements use pagination instead. Balances, payout schedules, webhook endpoints, billing products, prices and subscriptions return bare arrays with no pagination at all.

Summarize payments

GET /payments/summary. Scope payments:read. Aggregates order counts, volume and success rate over a date range, bucketed by day. Useful for dashboards; it is not a substitute for reports, which are ledger-accurate PDFs.
string
ISO date. Defaults to 29 days before to.
string
ISO date. Defaults to today.
A range longer than 366 days is silently clamped to 366, and from later than to is clamped to to. Unparseable dates fall back to the defaults instead of erroring.
currencies is sorted by descending volume. timeseries has one zero-filled entry per day in the range. success_rate is a fraction rounded to four places, not a percentage.

List payment events

GET /payments/{payment_intent_id}/events. Scope payments:read. The immutable transition log behind your webhook deliveries. Use it to backfill history or to reconcile after a webhook outage. Returns a bare array, oldest first, with no pagination.
event_type is the public event name, matching the webhook event catalogue. Some events appear here that no webhook endpoint can subscribe to, notably payment_intent.canceled from the agentic cancel endpoint.

Download the invoice PDF

GET /payments/{id}/invoice. Scope payments:read. Returns application/pdf as an attachment named invoice-{reference}.pdf. There is no JSON variant, and no status check. An unpaid intent still renders. The only failure is 404 for an unknown id.
Request

Lifecycle

The status field has exactly six values. There is no created, no confirming and no overpaid. An overpayment is not a separate status: it is status: "completed" with state_reason: "completed_overpaid", and the excess is in the overpayment object. For the allowed transitions, all ten state_reason values and how statuses map to webhook events, see payment lifecycle.

Sub-resources

  • Refunds: GET and POST /payments/{id}/refunds.
  • Reconciliation: GET /payment_intents/{id}/reconciliation. Note the path segment is payment_intents, not payments, for that route only.
Last modified on August 31, 2026