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 requirepayments: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.
hosted_url. It is https://pay.southpay.io/{reference} unless your store has a verified custom domain.
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 | 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 return422 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.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.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.
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.
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
Thestatus 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:
GETandPOST /payments/{id}/refunds. - Reconciliation:
GET /payment_intents/{id}/reconciliation. Note the path segment ispayment_intents, notpayments, for that route only.

