Pay for someone
You start it. The person approves it themselves.
A personal assistant can pay a merchant's payment link for the person it works for, through the public checkout API and with no PerionPayments credentials. It can start the payment; only the person can finish it, with their PIN on their own phone or their own card on the card acquirer's secure page.
Try it in Sandbox first.
With Sandbox access, create a payment link from your API client and follow these steps with the test number +263 77 000 0001. It approves a few seconds after you confirm, and no real money moves.
Find the request
A payment request reaches the person as a link: a /c/ share page, a /p/ page of up to twelve links, or a /l/ checkout address. The share and page HTML name each link's public JSON in a rel="alternate" link and describe it as a schema.org Invoice in JSON-LD, so you read the merchant and amount from PerionPayments rather than from the page's text. From a /l/ address, take the pl_ id from the path.
<link rel="alternate" type="application/json"
href="https://api.perionpayments.com/public/v1/payment_links/pl_…"
title="Payment link pl_… as JSON">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Invoice","identifier":"pl_…",
"url":"https://…/c/pl_…","description":"Market basket",
"provider":{"@type":"Organization","name":"Acacia Market","url":"https://acacia.example"},
"totalPaymentDue":{"@type":"PriceSpecification","price":"15.00","priceCurrency":"USD"},
"paymentStatus":"https://schema.org/PaymentDue"}
</script># The pl_ id from the JSON alternate, or from a /c/ or /l/ address
curl "https://api.perionpayments.com/public/v1/payment_links/pl_…" \
-H "Perion-Version: 2026-07-15"{
"id": "pl_…",
"object": "payment_link_landing",
"livemode": false,
"status": "active",
"merchant": {
"id": "mer_…",
"display_name": "Acacia Market",
"verified": true,
"verified_name": "Acacia Market (Private) Limited",
"website": "https://acacia.example",
"support_contact": { "email": "help@acacia.example", "phone": null }
},
"amount": 1500,
"currency": "USD",
"description": "Market basket",
"single_use": true,
"expires_at": "2026-10-20T08:00:00Z",
…
}Check the merchant before you name it
When merchant.verified is true, PerionPayments vouches for verified_name, website and support_contact: name the merchant by verified_name. When it is false, display_name is only the merchant's own choice; say so, and let the person decide. Then check that status is active, that the amount, currency and description are what the person expects, and that expires_at hasn't passed. livemode: false is a Sandbox link: tell the person no real money will move.
See what the person must do
Open one checkout for the link, then list the methods it offers. The claim returns a client_secret: keep it in memory for this payment only, and send it as Authorization: PerionCheckout on every checkout call. Each method's payer_presence says what the person must do themselves.
# A key you keep for this payment, reused on every retry (16-255 characters)
export CLAIM_KEY="…"
curl -X POST "https://api.perionpayments.com/public/v1/payment_links/pl_…/checkout_sessions" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: $CLAIM_KEY"
# Keep the returned client_secret in memory only, then list the methods
export PERION_CHECKOUT_SECRET="cs_…_secret_…"
curl "https://api.perionpayments.com/public/v1/checkout_sessions/cs_…/payment_methods" \
-H "Authorization: PerionCheckout $PERION_CHECKOUT_SECRET" \
-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",
"payer_presence": "approve_on_own_phone",
"health": "available",
…
}
],
…
}| payer_presence | The person | You |
|---|---|---|
EcoCashapprove_on_own_phone | Approves a prompt with their PIN on their own phone. | Confirm with their mobile number, once they say yes. |
Visa & Mastercard, ZimSwitchenter_card_on_hosted_page | Types their own card on the card acquirer's secure page, which the checkout opens for them. | Don't confirm. Give them the checkout's url to pay there. |
Other methodscomplete_on_hosted_page | Finishes the payment on the hosted payment page. | Don't confirm. Give them the checkout's url. |
Ask the person, in one line
Say who, how much, what for and how, then wait for a yes: “Acacia Market (verified) · USD 15.00 · Market basket. Pay with EcoCash? You'll approve it on your phone.” Confirm only with that yes, for that merchant, that amount and that method. Anything different needs a new yes.
Start it, and say who you are
Confirm the checkout with the method and the number the person chose. Send Perion-Payer-Agent once, as <name> or <name>; url=<https URL>, with a name of 1 to 80 characters. The merchant's timeline then shows “Started by the customer's assistant (Tendai's assistant)”. The header grants nothing, and a malformed one is refused with 400 invalid_request, so you know your name wasn't recorded.
# Only after the person said yes to this merchant, amount and method
export CONFIRM_KEY="…"
curl "https://api.perionpayments.com/public/v1/checkout_sessions/cs_…/confirm" \
-H "Authorization: PerionCheckout $PERION_CHECKOUT_SECRET" \
-H "Perion-Version: 2026-07-15" \
-H "Idempotency-Key: $CONFIRM_KEY" \
-H "Perion-Payer-Agent: Tendai's assistant; url=https://assistant.example" \
-H "Content-Type: application/json" \
-d '{
"payment_method": {
"type": "mobile_money",
"rail": "sandbox",
"mobile_number": "+263770000001"
}
}'{
"id": "cs_…",
"object": "checkout_session",
"status": "open",
"amount": 1500,
"currency": "USD",
"payment_intent": {
"id": "pi_…",
"status": "requires_action",
"next_action": {
"type": "approve_on_device",
"display_message": "Approve the payment prompt on your phone.",
"mobile_number_masked": "********0001"
},
"next_poll_after": "2026-10-14T09:30:02Z",
"payer_reference": "PER-K7QM-2TXA",
…
},
"receipt_url": null,
…
}Add receipt_contact only as the person chose it: "sms": true texts the result to the number paying when the checkout offers SMS receipts, and an email must be one they gave you for this.
The person approves on their own phone
Tell the person the prompt is on its way, and that only they can approve it, with their PIN, on their phone. Never ask for their PIN, a password or a one-time code, and never pass one on: PerionPayments never asks for them through the API. If the person doesn't approve, the payment ends unpaid; nothing moves without them.
Read until it is final
Read the checkout no sooner than payment_intent.next_poll_after, and tell the person what payment_intent.status means, in the words answer.sentence uses, said to them.
# No sooner than payment_intent.next_poll_after
curl "https://api.perionpayments.com/public/v1/checkout_sessions/cs_…" \
-H "Authorization: PerionCheckout $PERION_CHECKOUT_SECRET" \
-H "Perion-Version: 2026-07-15"| payment_intent.status | Tell the person | Then |
|---|---|---|
Waitingrequires_action | “Waiting for you to approve on your phone” (a card: “Waiting for you to enter your card on the payment page”) | Keep reading. Don't confirm again. |
Confirmingprocessing | “Still confirming · please don't pay again” | Keep reading. While retry_advice is do_not_create_another_payment, never start another payment. |
Paidsucceeded | “Paid · confirmed by EcoCash at 18:02 CAT”, with the method and time from the receipt | Keep the receipt. |
Not paidfailedcancelledexpired | “Not paid · it didn't go through” | A new try is a new payment, and it needs a new yes. |
Public routes are rate limited for every caller. A 429 carries Retry-After: wait that long, then repeat the same request with the same Idempotency-Key.
Keep the receipt
Once the payment succeeds, receipt_url is set, and its last path segment is the receipt id. Read the receipt as JSON and keep its payer_reference: it is the reference the merchant searches when the person asks about this payment.
# receipt_url ends in the receipt id: https://…/r/rcpt_…
curl "https://api.perionpayments.com/public/v1/receipts/rcpt_…" \
-H "Perion-Version: 2026-07-15"Leave recurring terms to the person.
A checkout that carries enrollment starts a recurring plan, and confirming it asks for agreement_acceptance. Whatever you send is recorded as the person's own consent, so don't confirm it: send them to the checkout page to read and accept the terms themselves.