The card charge is not the payment
This is the single most important thing on this page. A successful card charge means the provider took the customer’s money. It does not mean the payment intent is paid. The provider still has to send crypto on-chain to the deposit address, and the intent settles the way every other payment settles: when that deposit is detected and confirmed. Because card and bank rails can settle long after the customer commits, an intent with a funding session in progress is held open past its normal expiry window.Availability
Card payments are off by default. There is no API field and no dashboard switch that turns them on — only SouthPay staff can enable them for a store. Contact [email protected] to have them enabled. Even on an enabled store, availability is decided per payment intent. A method is offered only when all of the following hold:- The settlement currency is one the provider network supports.
- The intent has a deposit address in an asset the provider network can deliver. This is a subset of the assets your store accepts, so an intent that accepts only assets outside that subset gets no card option.
- The amount is inside the provider’s floor and ceiling.
- The economics work for your store. See Who pays the fee.
Checking availability
GET /api/v2/checkout/{reference} carries an onramp object:
boolean
Whether to render a card option for this checkout.
string[]
Opaque provider identifiers enabled for this intent. Pass one back as
provider to pin a session to it. Treat the values as opaque strings; do not branch on them or display them.onramp_unavailable on the authenticated intent, GET /api/v2/payments/{id}:
onramp_unavailable is null once a funding session exists for the intent, whatever that session’s outcome.
below_min_amount and above_max_amount additionally carry charge_amount_cents, min_amount_cents, max_amount_cents and currency, so you can tell the customer the actual bound rather than a generic refusal. max_amount_cents is null when there is no ceiling.
Who pays the fee
Your store’scustomer_pays_fee setting decides who absorbs the cost of the card rail, and it changes what the customer is charged.
customer_pays_fee: true. The customer covers the platform fee, a card surcharge and the provider’s own cut. The card is charged more than the order total, and you receive the full order amount.
customer_pays_fee: false. The customer’s card is charged the order total exactly, with nothing added. You receive the order amount minus a flat card rate. Because the provider’s cut comes out of your side here, some methods on some amounts are not worth offering, and those are dropped from the pricing response. If none survive, payment_methods comes back as an empty array — not an error, and not an onramp_unavailable reason. If the economics only fail at the last moment, creating the session returns 422 with Card payments are not available for this amount.
The pricing response tells you which model is in effect and what has been added, so you can show the customer a breakdown rather than an unexplained difference:
string
payer or merchant.integer
What SouthPay added on top of the order for the customer to cover.
0 when the store absorbs the fee.Building your own checkout
If you use hosted checkout or the checkout widget, the card option renders itself and there is nothing to build. Everything below is for a checkout UI you host — start from Custom checkout, which covers the endpoint your page renders from. The endpoints are public and unauthenticated, keyed by the intent’sreference, and cross-origin requests are allowed from any origin. Call them straight from the browser — no API key, no proxy, and no Idempotency-Key header. Paths are relative to https://api.southpay.io/api/v2.
Read the warning about the return redirect before you design the flow. It is the one constraint that can change how you build.
1. Price the available methods
GET /checkout/{reference}/onramp/payment_methods
Every method is priced by running a real quote for it, so the response contains only methods a provider actually quoted at this amount, in this currency, for this customer’s region. A method that cannot be priced is left out rather than shown and failed at the provider. Results are cached briefly per intent, so reopening the picker is fast.
Expect this call to be slow the first time — it is quoting several methods against live providers.
string
ISO 3166-1 alpha-2 country code for the customer. Ignored when the request arrives with a
CF-IPCountry header, which takes precedence.string
Sub-national region code, for example a US state. Ignored when the request arrives with a
CF-Region-Code or CF-Region header.string
Pin pricing to one provider identifier from
onramp.providers. Omit to use the default.Region matters more than it looks. In countries with sub-national regions, availability differs between them — a customer in one US state can be offered providers that cannot serve a customer in another. Send
region when you know it, or the customer may pick a provider that rejects them at its own checkout.… values above are placeholders for opaque identifiers.
object[]
Methods that priced successfully, best crypto delivered first. An empty array means no method could be offered; it is not an error.
2. Open a funding session
POST /checkout/{reference}/onramp_sessions
Returns 201 with a url. Send the customer there.
string
The
id of the method the customer chose. Omit and the provider picks.string
Send this only when the customer picked a provider other than the recommended one — the
id from the providers entry they chose. If that provider no longer quotes by the time the session is created, the ranking is used instead.string
Prefills the customer’s email at the provider.
string
Provider identifier from
onramp.providers. Omit to use the default.string
ISO 3166-1 alpha-2 country code. Overridden by a
CF-IPCountry header.string
Sub-national region code. Overridden by a
CF-Region-Code or CF-Region header.string
The provider’s checkout link. Redirect the customer here.
integer
What the customer’s card is charged, in minor units of
currency.string
pending, paid, expired or failed. See Session statuses.string | null
The provider’s own transaction identifier, for support requests.
3. Handle the return
The provider returns the customer to the hosted checkout for the intent with anonramp query parameter:
onramp=success means the card charge went through. It does not mean the payment is complete — crypto still has to arrive on-chain. Keep showing a pending state until the intent reaches completed.
One session at a time
A funding session cannot be cancelled once it is created, so SouthPay will not create a second one while an earlier one may still be payable. Two live links against one deposit address means the customer can be charged twice.- Requesting a session while one is pending returns the existing session with
201, not a new one. Redirect to theurlyou get back. - Once any session for the intent is
paid, no further session can be created. A repeat request returns409.
id.
Session statuses
paid, expired and failed are terminal.
Reading state from your backend
There is no card-specific webhook event. A card-funded payment emits the ordinary payment intent events:payment_intent.processing when the deposit is seen, payment_intent.completed when it confirms. Integrate against those.
To inspect the funding session, read onramp_session on GET /api/v2/payments/{id}. It holds the most recent session for the intent:
Refunds
A refund normally returns crypto to the address the funds came from. For a card-funded deposit that address belongs to the onramp provider, not your customer. Sending there hands the money to a third party with no way to attribute it, and your customer gets nothing back. Checkonramp_session on the intent before refunding. If it is non-null, the deposit was card-funded and the on-chain sender is not your customer.
Automatic refunds are blocked. A card-funded payment that SouthPay would normally refund itself — overpaid, or underpaid past expiry — is not auto-refunded the way a wallet-funded one is. No refund is created; the payment is flagged for manual resolution instead.
Refunds you request on a failed or expired intent are blocked as well. They take the same path. POST /payments/{id}/refunds returns 201 with no new refund in the array rather than an error, so read the response instead of assuming the request worked.
An explicit destination is never blocked, on any refundable status. See Refunds for the request format, and email [email protected] with the payment intent id if you need help working out where funds should go.
Test mode
Card payments work in test mode with an important limit. What test mode exercises: availability, method discovery and pricing, provider selection, session creation, the redirect, the return, and every status transition on the session up topaid.
What it does not do: deliver crypto. Test-mode card payments are simulated end to end, so no deposit is ever sent on-chain. The session will reach paid and the payment intent will sit unpaid until it expires.
To carry a test payment all the way through settlement, send testnet funds to the intent’s deposit address yourself once the session is paid. See Sepolia test wallet.
Test and live credentials are never mixed. If a store is configured with the wrong pair, card payments are refused for that mode rather than falling back to the other — that is what live_credentials_in_test and test_credentials_in_live report. A store with no test credentials has no card option in test mode; it never quietly charges a real card.
Errors
Both endpoints return{"error": "<message>"} as a plain string, not the structured envelope used elsewhere. See Errors.
502 is worth retrying once; the underlying quote search has a deadline and a slow provider can trip it. Do not retry 422 — the answer will not change without the intent changing.
These endpoints have their own rate limits, tighter than the rest of the checkout surface, because each call runs live quotes. Price methods once per checkout and cache the result in your UI rather than re-querying on every render.
