Skip to main content
Card payments let a customer pay a crypto payment intent without holding crypto. The customer picks a card or local payment method, pays in your settlement currency, and an onramp provider delivers crypto to the deposit address on the intent. Nothing about the payment intent changes. The same addresses, the same amounts, the same statuses, the same webhooks. Card payments only change where the deposit comes from. SouthPay never sees, stores or transmits card details. The customer enters them on the provider’s own checkout, which is why the flow ends in a redirect rather than a form you host.

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.
Do not release goods, mark an order paid, or trust a customer returning to your success URL when the card charge succeeds. Wait for payment_intent.completed, exactly as you would for a customer paying from their own wallet.
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.
When you need to know why an intent has no card option, read 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’s customer_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.
The provider’s own cut is never a number SouthPay adds. It is discovered inside each quote and is already reflected in the charge amounts below.

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’s reference, 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.
Provider names and icons come from the providers themselves and are meant for display. The … 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 an onramp 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.
The card flow does not return the customer to your page, and you cannot change that. The return target is always SouthPay’s hosted checkout for the intent. A customer who pays by card on your own checkout leaves your domain, waits for settlement on pay.southpay.io, and is then forwarded to your success_url.Wallet payments stay on your page throughout; card payments do not. If keeping the customer on your own domain end to end matters more than offering cards, offer wallet payments only.

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 the url you get back.
  • Once any session for the intent is paid, no further session can be created. A repeat request returns 409.
Treat the response as authoritative rather than assuming your request created something new. Compare the id.

Session statuses

paid, expired and failed are terminal.
failed also covers refunded and cancelled at the provider. A session that moves from paid to failed usually means the customer’s card payment was reversed after the fact, not that they never paid. If crypto had already arrived and the intent completed, that is a chargeback-shaped event and needs a human. SouthPay alerts internally when this happens.

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:
Use it for support and reconciliation. Do not use it as a completion signal.

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. Check onramp_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.
A refund you request on a completed card-funded intent is not blocked. With no refund.destination_address, the destination is resolved from the on-chain sender — the onramp provider’s wallet, not your customer. Always send an explicit refund.destination_address when refunding an intent that has an onramp_session.
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 to paid. 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.
Last modified on August 31, 2026