Recommended integration
One integration for every payment method.
Let PerionPayments tell you which methods to offer, show them on your own page, and start the one the payer picks from your server. The same five steps work for EcoCash, Visa & Mastercard and ZimSwitch, and for every system you connect. Your API client needs payment_methods:read, checkout_sessions:write, payment_intents:write and payment_intents:read.
Cards today
Visa & Mastercard and ZimSwitch take the card on the card acquirer's secure page, which PerionPayments opens for you. They are offered to internal pilot merchants only and have no sandbox scenarios yet, so build and test this guide with the Sandbox test method.
List the payment methods for this payment
curl "$PERION_API_BASE_URL/v1/payment_methods?amount=2500¤cy=USD" \
-H "Authorization: Bearer $PERION_ACCESS_TOKEN" \
-H "Perion-Version: 2026-07-15"{
"object": "list",
"data": [
{
"object": "payment_method",
"type": "mobile_money",
"rail": "sandbox",
"display_name": "PerionPayments Sandbox",
"interaction_mode": "approve_on_device",
"health": "available",
"currencies": ["USD"],
"amount_min": 100,
"amount_max": 10000000
}
],
"has_more": false,
"next_cursor": null
}Show one button per method, with its display_name, in the order returned. Keep each method's type and rail: they limit the checkout in step 2 and name the method in step 3. Never hard-code the list. It changes with the account, amount, currency and method health; a live account lists its own methods, such as EcoCash, Visa & Mastercard and ZimSwitch.
Create a checkout limited to the chosen method
curl "$PERION_API_BASE_URL/v1/checkout_sessions" \
-H "Authorization: Bearer $PERION_ACCESS_TOKEN" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: order-10428-checkout" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500,
"currency": "USD",
"merchant_reference": "order_10428",
"allowed_rails": ["sandbox"],
"return_url": "https://merchant.example/orders/10428/return",
"cancel_url": "https://merchant.example/orders/10428"
}'{
"id": "cs_01K0A9P2…",
"object": "checkout_session",
"payment_intent": "pi_01K0A9N3ZQ2X7V4B8C6D5E1F0G",
"url": "https://checkout.perionpayments.com/s/cs_01K0A9P2…",
"client_secret": "cs_01K0A9P2…_secret_…",
"status": "open",
"allowed_rails": ["sandbox"],
"expires_at": "2026-10-14T10:00:00Z",
…
}allowed_rails holds the rail the payer chose, so checkout never asks them to choose again. Store the session id and its payment_intent with the order. Add embed_origin, your page's exact HTTPS origin, only if you will mount the drop-in. A session expires after 30 minutes unless you set expires_at, up to 24 hours ahead.
Start the payment
Either confirm it from your server
For a wallet such as EcoCash, confirm with the payer's mobile number. The answer is requires_action with approve_on_device: ask the payer to approve the prompt on their phone, and don't confirm again while it waits.
curl "$PERION_API_BASE_URL/v1/payment_intents/$PERION_PAYMENT_ID/confirm" \
-H "Authorization: Bearer $PERION_ACCESS_TOKEN" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: order-10428-confirm" \
-H "Content-Type: application/json" \
-d '{
"payment_method": {
"type": "mobile_money",
"rail": "sandbox",
"mobile_number": "+263770000001"
}
}'{
"id": "pi_01K0A9N3ZQ2X7V4B8C6D5E1F0G",
"status": "requires_action",
"next_action": {
"type": "approve_on_device",
"display_message": "Approve the payment prompt on your phone.",
"mobile_number_masked": "********0001"
},
…
}For a card method, confirm with its rail. The answer is requires_action with next_action.type redirect_to_url: send the payer's browser to next_action.url, for example with a 303 redirect from your server. The payer enters their card on the card acquirer's secure page, which PerionPayments opens for you, never on yours.
curl "$PERION_API_BASE_URL/v1/payment_intents/$PERION_PAYMENT_ID/confirm" \
-H "Authorization: Bearer $PERION_ACCESS_TOKEN" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: order-10428-confirm" \
-H "Content-Type: application/json" \
-d '{
"payment_method": { "type": "card", "rail": "<rail from step 1>" }
}'{
"id": "pi_01K0A9N3ZQ2X7V4B8C6D5E1F0G",
"status": "requires_action",
"next_action": {
"type": "redirect_to_url",
"url": "https://checkout.perionpayments.com/s/cs_01K0A9P2…/pay/pa_…",
"return_method": "get",
"expires_at": "2026-10-14T09:45:00Z"
},
…
}Or mount the drop-in
Create the session with embed_origin and pass its client_secret to PerionPayments.js. The payer chooses and approves inside the frame. A checkout limited to one card method opens that card page at once: the drop-in moves your whole page there.
Handle a cancel on your page
If the payer cancels on your page, tell PerionPayments with the same idempotency rules as any other POST. What happens depends on how far the payment got.
curl "$PERION_API_BASE_URL/v1/payment_intents/$PERION_PAYMENT_ID/cancel" \
-H "Authorization: Bearer $PERION_ACCESS_TOKEN" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: order-10428-cancel" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer left the order" }'| Response | When | What to do |
|---|---|---|
200cancelled | Nothing had been sent to a payment method yet (requires_payment_method or requires_confirmation). | The payment is closed and payment_intent.cancelled follows. Release the order. |
202requires_action | The payer is on the card acquirer's secure page. The body is the payment as it stands. | PerionPayments asks the card acquirer at once. If nothing was submitted, the payment ends failed with failure_code: customer_cancelled within seconds (payment_intent.failed). If the payer paid after all, it succeeds as usual, so read it again before you release the order. |
409attempt_in_flight | A wallet prompt is out, or the result is still being confirmed (processing). | The payer may still approve. Wait for the result and never start another payment for this order. |
409intent_not_cancellable | The payment has already ended. | Read it and act on its status. |
Fulfill once
Fulfill the order when a verified payment_intent.succeeded event arrives, or when GET /v1/payment_intents/{id} shows succeeded for the expected amount and currency. Deduplicate by event ID so a repeat never fulfills twice. Treat failed, cancelled and expired as not paid, and offer the payer another try only then.