Skip to Content
Pesa VoucherDeveloper Documentation

PayPal

Accept international PayPal deposits and send PayPal payouts, backed by your Pesa Voucher merchant wallet.


PayPal Payments API

Live URL: https://payments.pesavoucher.com/api/v1/ Sandbox URL: https://sandbox.payments.pesavoucher.com/api/v1/

Live Testing Note To perform real live transactions or trigger a live test webhook, please contact us: support@pesavoucher.com Telegram: Join our channel → t.me/Dev_PesaVoucher

Overview

PesaVoucher's PayPal integration lets you accept international deposits (a customer "purchasing" credit via PayPal) and send PayPal payouts, using the same prepaid float model as M-Pesa. The flow is a standard PayPal Checkout redirect: you create an order, the customer approves it on PayPal, and you capture it to credit your merchant wallet.

If the paying user's preferred currency is KES, TZS, or UGX, their PayPal deposit (always in USD) is automatically converted into their preferred currency before their wallet is credited — see Currencies for how that conversion works and what rate is used.

Authentication

All requests require these headers:

X-API-KEY: your_api_key
X-API-SECRET: your_api_secret
Content-Type: application/json

Get your credentials from your merchant dashboard.


1. Create Order (Initiate Deposit)

Endpoint: POST /paypal/create_order

Creates a PayPal order and returns an approval_url. Redirect the customer there to log in to PayPal and approve the payment.

curl -X POST https://payments.pesavoucher.com/api/v1/paypal/create_order \
-H "Content-Type: application/json" \
-H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxx" \
-H "X-API-SECRET: sk_live_xxxxxxxxxxxxxxxxxxxx" \
-d '{
"amount": 10.00,
"currency": "USD",
"order_id": "ORDER-2026-0417",
"metadata": {
"customer_id": "CUST-8871",
"description": "Wallet top-up"
}
}'

KES example:

{
"amount": 500,
"currency": "KES",
"order_id": "ORDER-KES-2026-0417"
}

Request Parameters

ParameterTypeRequiredDescription
amountfloatYesDeposit amount, 2 decimal places. Minimum $1.00 (or currency equivalent)
currencystringYesISO currency code. USD and KES are confirmed supported — other codes (e.g. JPY) are rejected
order_idstringYesYour internal order/reference ID
metadataobjectNoAny custom data you want attached to the transaction

Success Response

{
"success": true,
"data": {
"paypal_order_id": "8AC13285XA1234567",
"transaction_id": "TXN-20260417-0001",
"order_id": "ORDER-2026-0417",
"approval_url": "https://www.sandbox.paypal.com/checkoutnow?token=8AC13285XA1234567",
"status": "Pending",
"amount": 10.00,
"currency": "USD"
}
}

Next step: Redirect the customer's browser to data.approval_url. After they approve, PayPal redirects them to your configured return_url with the order token attached — see Capture Order below.


2. Capture Order (Return URL)

Endpoint: GET /paypal/capture

Finalizes the payment and credits your merchant wallet. PayPal automatically appends token (the PayPal order ID) to your return_url after the buyer approves — call this endpoint (or point your return_url directly at it) to complete the deposit.

GET /paypal/capture?token={paypal_order_id}&order_id={order_id}

Success Response

{
"success": true,
"data": {
"capture_id": "3JT12345AB6789012",
"status": "Completed",
"amount": 10.00,
"currency": "USD",
"order_id": "ORDER-2026-0417",
"paypal_order_id": "8AC13285XA1234567"
}
}

Idempotent by design. Calling /paypal/capture again with the same order_id/token after it has already completed is safe — it returns the existing captured result rather than capturing (and crediting) a second time.

Cross-currency users: the amount/currency in this response reflect the captured PayPal transaction (USD). If the paying user's preferred currency isn't USD, the amount actually credited to their wallet is the converted value in their preferred currency, not the USD figure shown here — check the user's wallet balance or Order Status for the credited amount, and see Currencies for how the conversion rate is applied.


Create Order & Capture — Validation Errors

ScenarioHTTP StatusNotes
Missing amount400{ "success": false, "error": "..." }
Amount below minimum ($1.00)400Minimum deposit is $1.00 (or currency equivalent)
Unsupported currency (e.g. JPY)400Only USD and KES are currently supported
Missing API credentials400No X-API-KEY / X-API-SECRET headers sent
Duplicate capture attempt200Safe to retry — returns the already-captured result, does not double-credit

Error Response

{
"success": false,
"error": "Human-readable error message"
}

3. Create Payout (Merchant → PayPal Recipient)

Endpoint: POST /paypal/create_payout

Sends funds from your float to a PayPal recipient. Admin-only — your merchant account needs is_admin or can_payout permission enabled.

curl -X POST https://payments.pesavoucher.com/api/v1/paypal/create_payout \
-H "Content-Type: application/json" \
-H "X-API-KEY: pk_live_xxxxxxxxxxxxxxxxxxxx" \
-H "X-API-SECRET: sk_live_xxxxxxxxxxxxxxxxxxxx" \
-d '{
"amount": 2.00,
"currency": "USD",
"recipient_email": "customer@example.com",
"order_id": "PAYOUT-2026-0417",
"note": "Withdrawal payout",
"metadata": {
"customer_id": "CUST-8871"
}
}'

Request Parameters

ParameterTypeRequiredDescription
amountfloatYesPayout amount, 2 decimal places
currencystringYesISO currency code (e.g. USD)
recipient_emailstringYesRecipient's PayPal email address
order_idstringYesYour internal reference ID
notestringNoNote shown to the recipient
metadataobjectNoAny custom data attached to the payout

Success Response

{
"success": true,
"data": {
"payout_id": "PAYOUT-0001",
"paypal_batch_id": "BATCH-2026-0417-001"
}
}

Validation Errors

ScenarioHTTP StatusNotes
Missing recipient_email400recipient_email is required
Invalid email format400Must be a valid email address
Missing payout permission401/403API key lacks can_payout / is_admin — not authorized to send payouts
Insufficient float balance400Payout amount exceeds your available merchant balance

Webhooks

PesaVoucher receives PayPal's webhook events directly and processes them internally to keep transaction status current. You don't need to build a listener for raw PayPal event types — deposit and payout status updates are delivered to your own configured webhook URL (see Webhooks) in PesaVoucher's normalized format.

The table below documents what each PayPal event does on PesaVoucher's side, so you understand the status changes you'll see:

EventTriggerEffect
PAYMENT.CAPTURE.COMPLETEDPayPal confirms a captureMarks the transaction Completed and credits the merchant wallet. Idempotent — skipped if the return URL already credited it
CHECKOUT.ORDER.APPROVEDBuyer approves in PayPalTriggers an automatic capture if the return URL hasn't already captured the order
PAYMENT.CAPTURE.DENIEDCapture is declinedTransaction marked Failed
PAYMENT.CAPTURE.REFUNDEDA refund is issuedTransaction marked Refunded; refund_id and refund_amount recorded
PAYMENT.PAYOUTSBATCH.SUCCESSA payout batch completesAll payout rows in the batch updated to Success
PAYOUTS.PAYMENT.SUCCESSAn individual payout item succeedsThat specific payout row marked Success
PAYOUTS.PAYMENT.FAILEDAn individual payout item failsRow marked Failed with the error reason (e.g. RECEIVER_UNREGISTERED)
PAYOUTS.PAYMENT.UNCLAIMEDRecipient has no PayPal accountPayPal emails them to claim; funds are held 30 days, then returned

Why both a return URL and a webhook? Same reasoning as the M-Pesa flow: the return URL is the primary capture path. CHECKOUT.ORDER.APPROVED and PAYMENT.CAPTURE.COMPLETED are the fallback safety net for cases where the customer closes the tab before returning, or the return URL request never reaches your server.

Signature Verification

  • Sandbox mode: the webhook handler bypasses PayPal's signature check, so simulated or unsigned events are still processed — this is expected for testing.
  • Production: requests must carry a valid PayPal signature or they are rejected.

Idempotency

Each webhook event has a unique id. Resending the same event id (e.g. a redelivery from PayPal) is safe — duplicates are recognized and ignored rather than reprocessed.


Testing

Sandbox URL

https://sandbox.payments.pesavoucher.com/api/v1/

Sandbox Setup

  1. Use your sandbox API key/secret plus a PayPal sandbox buyer account to approve test payments
  2. Run Create Order, open the returned approval_url in a browser, log in as the sandbox buyer, and approve
  3. Run Capture Order to complete the deposit
  4. Use the Webhook (manual event simulation) requests in the Postman collection to POST event payloads directly to /paypal/webhook and observe status updates — no live PayPal callback required

In sandbox mode the webhook handler bypasses PayPal's signature check, so simulated events are processed without a real signature. In production, unsigned or invalid-signature requests are rejected. To test real signature verification, use PayPal's Simulate webhook event tool in the Developer Dashboard, or expose your local server with ngrok.

Test Flow

  1. Create order (try both a USD and a KES amount) → copy approval_url
  2. Approve as sandbox buyer in browser
  3. Capture order — retry the same capture request once to confirm it's idempotent
  4. Confirm your merchant wallet balance updated
  5. Run the negative-test requests (below-minimum amount, unsupported currency, missing/invalid payout email, insufficient balance, missing payout permission) to confirm each returns the expected error
  6. Resend a webhook event with the same id to confirm duplicates are ignored

Best Practices

  • Redirect the customer to approval_url immediately — PayPal order links expire after a few hours
  • Treat the return URL capture as primary and the webhook as a fallback, never the other way around
  • Use a unique order_id for every attempt to avoid capture collisions
  • Store paypal_order_id, transaction_id, and order_id together so you can reconcile Create Order → Capture → Webhook
  • Only send USD or KES as the deposit currency — other codes are rejected
  • Retrying a capture or resending a webhook is safe — both are idempotent, so build your integration to retry rather than guess
  • Confirm your API key has can_payout / is_admin before attempting payouts, and check available float balance before sending large payouts
  • Never expose X-API-SECRET in frontend code
  • Test the full happy path plus denied, refunded, and duplicate-webhook scenarios in sandbox before going live