A server-side payment flow with retry and duplicate protection.
1. Prepare IDs and access
Start with an enabled store and tested payment methods. In Settings → API access, create a read/write credential limited to the needed project. Keep the token on your backend, never in browser code or a public repository.
Copy the Project API ID and Store API ID from the store’s Basic → API IDs box. These are UUIDs, not the readable project identifier or your order number. Use your own API hostname.
In Store → IPN, create a signing secret before supplying an ipn_url. Your HTTPS receiver must be reachable from the merchant VPS.
2. Create an invoice
Replace the placeholders and send this request from your backend. Amounts are decimal strings, not floating-point calculations.
curl --fail-with-body --request POST \
'https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices' \
--header 'Authorization: Bearer YOUR_MERCHANT_API_TOKEN' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-1042-attempt-1' \
--data '{
"amount": "10.00",
"currency": "EUR",
"order_id": "order-1042",
"description": "Example order",
"ipn_url": "https://shop.example.com/payments/wholly-ipn",
"metadata": {
"cart_id": "cart-1042"
}
}'Save data.invoice_id with your order, then redirect the customer to links.checkout. Use a unique idempotency key for a new payment attempt. For a timeout, retry with the same credential, key and exact body bytes.
Omitting payment_methods uses the store’s accepted methods. You can narrow them per invoice with chain slugs and tickers; this never enables an unaccepted asset. All request fields and response examples →
3. Choose IPN, webhooks or both
IPN follows the invoice lifecycle. Set the store’s default IPN URL, or override it with ipn_url for an invoice. Webhooks subscribe an endpoint to selected events, for example invoice.settled.
They use the same signature format, but different secrets: IPN uses the store IPN secret; every webhook endpoint has its own secret. Neither uses the API bearer token for signing.
If both deliver to your app, expect overlapping notifications. Do not credit an order twice.
4. Verify and save the notification
- Read the exact raw request body before JSON parsing. Verify
Wholly-Signaturewith the matching secret and timestamp/replay checks. The official SDKs provide verifiers. - Validate the signed body’s project, store, invoice and event identity. Unsigned delivery headers are not an authentication source.
- Store the event durably with a unique
event_id, then return HTTP 2xx promptly. Process orders in a background worker. - Retrieve the current invoice from your configured API host, not an arbitrary host supplied in a request. Compare your saved project, store, amount, currency and order reference.
PHP · Python · JavaScript / TypeScript · Signature specification and receiver examples
5. Fulfil once, on settlement
For an event-based receiver, handle event_type = invoice.settled, then verify the current status = settled and your exception policy. Fulfil once under a database transaction/unique order constraint.
A fast-finalizing chain can send both payment.received and invoice.settled with status = settled. On another chain, payment.received may still say processing. Neither flow is an error.
Deduplicate events by event_id, not just sequence: different event types can share a sequence. State-based SDK handlers instead collapse invoice revisions and check state regardless of event type. Do not mix that collapse with an event-type filter. Both approaches still need order-level duplicate protection.
Inspect requires_review and manual resolutions before fulfilling. amount_status = paid alone does not prove confirmation. The top-level paid asset fields summarize settlement; payment_info holds the detailed receipts and quote data. All statuses, events and exception rules →
6. Test retries and recovery
Test a small payment, duplicate delivery, an expired invoice and a temporarily unavailable receiver. Replaying an event must not create a second order credit. Handle out-of-order events without overwriting newer state.
Inspect Store → IPN / Webhooks → History → Details, or the delivery sections in Invoice Details. Resend reuses the recorded event, not a new settlement.
Low processing credit pauses IPN/webhooks while payments continue. Reconcile outstanding orders through the API and handle retained deliveries after recovery. Never fulfil from a browser redirect or a customer’s screenshot.