Skip to main content
You need a test secret API key. Create one in the dashboard under Settings → API keys, choosing Test and Secret. It starts with sp_sk_test_. Everything below runs against https://api.southpay.io/api/v2. There is no separate sandbox host. Test and live mode are selected by which key you send.
In test mode your store has exactly one usable asset: Ethereum Sepolia (SETH). Every test payment intent returns one deposit address on Sepolia. Live mode returns one address per asset your store has enabled. See Environments, and Sepolia test wallet to get the test ETH you need to pay an intent.

1. Create a payment intent

You price in fiat. SouthPay locks an exchange rate and works out the crypto amount.
Three fields to store against your order right now:
  • id is the UUID you use on every later API call. All SouthPay ids are bare UUIDs; there is no pi_ prefix scheme.
  • reference is 16 uppercase alphanumeric characters. This is the customer-facing identifier and the checkout token, and it is what arrives in webhooks.
  • expires_at is 60 minutes from creation by default. Send expires_in (seconds, 300 to 86400) on create for a different window. See Payment lifecycle.
crypto_amount_atomic is the exact amount the customer must send, in the asset’s smallest unit. Divide by 10 ** atomic_decimals for a human amount, here 0.022533243243243243 ETH. The Idempotency-Key header is optional on this endpoint but retrying without one creates a second intent. Read Idempotency before you build a retry loop; the behaviour has edges.

2. Send the customer to checkout

Redirect to the hosted_url on the response. Do not build the URL yourself.
The page shows a QR code and the exact amount for each address on the intent, tracks confirmations live, and redirects to your success_url when the payment completes. There is no frontend work to do.

3. Register a webhook endpoint

signing_secret is returned only here and on secret rotation. Store it before you close the terminal. No endpoint will show it to you again.
subscribed_events is required and must list every event you want by name. Wildcards are not supported. "*" and "payment_intent.*" both fail validation. See the full event list.

4. Verify and handle the delivery

When the payment confirms, SouthPay POSTs this to your URL:
Webhook body
data.object.id is the reference, not the payment intent UUID. There is no reference key in the payload, and the amount is settlement_amount_cents (an integer in minor units), not settlement_amount. Look the intent up by reference, or fetch it by UUID from your own order record.
Verify the signature against the raw request body before you trust anything in it.
Return 2xx within 30 seconds. Any non-2xx response is retried, up to 10 attempts over roughly 18 hours, including a 400 or 404 your framework returns by accident. Deduplicate on the event id. Test the whole path without paying anything:

Polling instead

If you cannot receive webhooks, fetch the intent by UUID and read status.
terminal: true means the outcome is settled. It does not mean the record is frozen. A terminal intent can still become refunded later.

Next

Payment lifecycle

The six statuses, the ten state reasons, and what each one means for an order.

Webhooks

Signing, retries, the event catalogue, and the five events that never fire.

Errors

Three response shapes, and why error.message will throw if you assume one.

Payout destinations

Where the money goes, and how to configure it before your first payout.
Last modified on August 31, 2026