API reference: every endpoint, field, error and data model, without the guides
# Check Payment Status
> Look up the current status of a transaction by its reference number.
GET Returns the current status of a transaction. Use this as a fallback or reconciliation check alongside the [result callback](/webhooks/result-callback/) — see [Checking Payment Status](/payments/checking-status/) for when to use each. | Environment | URL | | ----------- | -------------------------------------------------------------------------------- | | Production | `https://api.pesepay.com/api/payments-engine/v1/payments/check-payment` | | Sandbox | `https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/check-payment` | ## Headers [Section titled “Headers”](#headers) | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------- | | `authorization` | string | Yes | Your application’s integration key | | `content-type` | string | Yes | Must be `application/json` | ## Query parameters [Section titled “Query parameters”](#query-parameters) | Parameter | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `referenceNumber` | string | The reference number of the transaction to check | ## Code examples [Section titled “Code examples”](#code-examples) * cURL ```bash curl -G https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/check-payment \ -H "authorization: YOUR_INTEGRATION_KEY" \ -H "content-type: application/json" \ --data-urlencode "referenceNumber=YOUR_REFERENCE_NUMBER" ``` * Node.js ```javascript const url = new URL( 'https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/check-payment' ); url.searchParams.set('referenceNumber', referenceNumber); const response = await fetch(url, { headers: { authorization: 'YOUR_INTEGRATION_KEY', 'content-type': 'application/json', }, }); const { payload } = await response.json(); const transaction = decrypt(payload, ENCRYPTION_KEY); console.log(transaction.transactionStatus); ``` * Python ```python import requests response = requests.get( "https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/check-payment", params={"referenceNumber": reference_number}, headers={ "authorization": "YOUR_INTEGRATION_KEY", "content-type": "application/json", }, ) transaction = decrypt(response.json()["payload"], ENCRYPTION_KEY) print(transaction["transactionStatus"]) ``` * PHP ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); String transactionJson = decrypt(extractPayload(response.body()), encryptionKey); ``` ## Response [Section titled “Response”](#response) The response body is `{ "payload": "..." }`. Decrypt the `payload` with your encryption key to get the transaction result — the **same object** the [result callback](/webhooks/result-callback/) delivers and [Make Payment](/api/make-payment/) returns. Its complete field list, including the `amountDetails` breakdown, is on the [result callback page](/webhooks/result-callback/#payload). The fields you check most often: | Field | Type | Description | | ------------------------------ | ------ | --------------------------------------------------------------------------------------- | | `referenceNumber` | string | The transaction’s reference — match it to your order | | `transactionStatus` | string | Where the payment stands — see [Transaction Statuses](/resources/transaction-statuses/) | | `transactionStatusCode` | number | Numeric equivalent of `transactionStatus` | | `transactionStatusDescription` | string | Human-readable status message | | `amountDetails` | object | Amounts and fees applied to the transaction | This is **not** the full [Transaction](/resources/transaction-model/) entity — it is the smaller result view. There is no `redirectUrl` on it.
# Errors
> The shape of a Pesepay error response, what each HTTP status means, and how to handle failed transactions.
There are two different kinds of “error” to handle: the **API call itself** failing, and a **transaction** completing with a non-success status. They look nothing alike, and only the first one is a bug in your integration. ## The error response [Section titled “The error response”](#the-error-response) Failed requests return a JSON body with this shape: ```json { "timestamp": "2026-09-01T10:32:14.000+00:00", "message": "Currency code should be provided", "description": null, "status": "400" } ``` | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------- | | `timestamp` | string | When the error occurred | | `message` | string | What went wrong. This is the field to log and show your team | | `description` | string | Extra detail. Usually `null`; carries the per-field list for validation failures | | `status` | string | The HTTP status code, as a string | Caution **Error bodies are plain JSON — don’t try to decrypt them.** Successful responses come back [encrypted](/security/encryption/); error responses don’t. If decryption throws, check the HTTP status first: you’re probably holding an error body, not a corrupted payload. ## HTTP statuses [Section titled “HTTP statuses”](#http-statuses) | Status | What it means | Example `message` | What to do | | ------ | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | The request was rejected — bad field, failed validation, or a payload that decrypted into something that isn’t valid JSON | `Payload was decrypted but did not produced a valid json representation`; `Currency code should be provided`; `Invalid Ecocash phone number supplied` | Fix the request. These are integration bugs, not transient failures — don’t retry unchanged | | `401` | Authentication failed | `Invalid username or password` | Check the credentials you’re using | | `403` | Your integration key was rejected, or no payment method is available | `Integration key for the application is not valid (Not active or disabled)`; `No payment method that supports {currency} is available at the moment` | Confirm the key is for the right environment and still active — see [API keys](/security/api-keys/) | | `404` | The record you asked for doesn’t exist | Lookup misses, e.g. an unknown `referenceNumber` | Check the reference number you’re sending | | `406` | The operation isn’t allowed in the transaction’s current state | — | Re-check the transaction’s [status](/resources/transaction-statuses/) before retrying the operation | | `500` | Something failed server-side, including decryption failures and downstream service errors | `Failed to decrypt your data` | If it mentions decryption, check your encryption key and [IV rule](/security/encryption/). Otherwise retry once, and contact support with the `referenceNumber` if it persists | ## Validation errors [Section titled “Validation errors”](#validation-errors) Field validation failures come back as `400`, and **multiple problems are joined into a single `message`** separated by `;` — so parse the whole string rather than assuming one error per response. Real examples: ```plaintext Invalid Visa Card Number length, its either 13, 16, 19 numbers.; Invalid Credit card expiry date provided ``` Each payment method validates its own fields. See the failure-modes table on the relevant [payment method page](/payment-methods/overview/) for what each one rejects. ## Transaction-level outcomes [Section titled “Transaction-level outcomes”](#transaction-level-outcomes) A request can succeed at the API level — HTTP 200, valid encrypted response — while the payment itself fails. A declined card and an EcoCash timeout are both HTTP 200. Read `transactionStatus` in the decrypted response, not the HTTP status: * `SUCCESS` is the only status that means you were paid. * `DECLINED`, `INSUFFICIENT_FUNDS`, `AUTHORIZATION_FAILED`, `TIME_OUT`, and `CANCELLED` are normal, expected outcomes your UI should handle gracefully. * Non-terminal statuses (`PENDING`, `PROCESSING`, `PARTIALLY_PAID`) mean the payment is still in flight — keep checking. See [Transaction statuses](/resources/transaction-statuses/) for the full list with numeric codes. `transactionStatusDescription` on a failed transaction often carries text straight from the payment provider (EcoCash, the card issuer, and so on). It’s useful to log and sometimes useful to show the customer, but it isn’t a stable value to branch on.
# Get Active Currencies
> Retrieve the currencies currently active on your gateway.
GET Returns the currencies currently active on the gateway. Unlike the integration endpoints, this one returns plain JSON — no encryption required. | Environment | URL | | ----------- | ------------------------------------------------------------------ | | Production | `https://api.pesepay.com/api/payments-engine/v1/currencies/active` | ## Headers [Section titled “Headers”](#headers) | Parameter | Type | Required | Description | | -------------- | ------ | -------- | -------------------------- | | `content-type` | string | Yes | Must be `application/json` | ## Code examples [Section titled “Code examples”](#code-examples) * cURL ```bash curl https://api.pesepay.com/api/payments-engine/v1/currencies/active \ -H "content-type: application/json" ``` * Node.js ```javascript const response = await fetch( 'https://api.pesepay.com/api/payments-engine/v1/currencies/active', { headers: { 'content-type': 'application/json' } } ); const currencies = await response.json(); ``` * Python ```python import requests response = requests.get( "https://api.pesepay.com/api/payments-engine/v1/currencies/active", headers={"content-type": "application/json"}, ) currencies = response.json() ``` * PHP ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ## Response [Section titled “Response”](#response) An array of [Currency](/resources/currency-model/) objects. Caution This endpoint currently returns `USD` and `ZiG` only. `ZWG` is **also chargeable** — it is the currency code for [Zimswitch](/payment-methods/zimswitch/) and [Omari](/payment-methods/omari/) in Zimbabwe dollars — but it does not appear here. If you drive a currency selector purely off this response, you will silently drop those methods; see [Payment method codes](/resources/payment-method-codes/).
# Get Payment Methods by Currency
> Retrieve the payment methods available for a given currency.
GET Returns the payment methods available to complete a transaction for a specified currency. Plain JSON — no encryption required. Use this to build a dynamic payment method picker in your own UI for the [seamless flow](/payments/seamless-flow/), instead of hardcoding [payment method codes](/resources/payment-method-codes/). | Environment | URL | | ----------- | ----------------------------------------------------------------------------- | | Production | `https://api.pesepay.com/api/payments-engine/v1/payment-methods/for-currency` | ## Headers [Section titled “Headers”](#headers) | Parameter | Type | Required | Description | | -------------- | ------ | -------- | -------------------------- | | `content-type` | string | Yes | Must be `application/json` | ## Query parameters [Section titled “Query parameters”](#query-parameters) | Parameter | Type | Description | | -------------- | ------ | ------------------------------------------------------------- | | `currencyCode` | string | The currency to get available payment methods for, e.g. `USD` | ## Code examples [Section titled “Code examples”](#code-examples) * cURL ```bash curl -G https://api.pesepay.com/api/payments-engine/v1/payment-methods/for-currency \ -H "content-type: application/json" \ --data-urlencode "currencyCode=USD" ``` * Node.js ```javascript const url = new URL( 'https://api.pesepay.com/api/payments-engine/v1/payment-methods/for-currency' ); url.searchParams.set('currencyCode', 'USD'); const response = await fetch(url, { headers: { 'content-type': 'application/json' }, }); const paymentMethods = await response.json(); ``` * Python ```python import requests response = requests.get( "https://api.pesepay.com/api/payments-engine/v1/payment-methods/for-currency", params={"currencyCode": "USD"}, headers={"content-type": "application/json"}, ) payment_methods = response.json() ``` * PHP ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ## Response [Section titled “Response”](#response) An array of [Payment Method](/resources/payment-method-model/) objects.
# Initiate Transaction
> Create a transaction and get a redirect URL for the Pesepay-hosted payment page.
POST Creates a transaction and returns a `redirectUrl` where your customer completes payment. This is the first call in the [redirect flow](/payments/redirect-flow/). | Environment | URL | | ----------- | --------------------------------------------------------------------------- | | Production | `https://api.pesepay.com/api/payments-engine/v1/payments/initiate` | | Sandbox | `https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate` | ## How it fits [Section titled “How it fits”](#how-it-fits) 1. Build your request body with the payment details, then [encrypt it](/security/encryption/) with your application’s encryption key. 2. POST the encrypted payload to the URL above, with your integration key in the `authorization` header. 3. Decrypt the response to get `referenceNumber`, `redirectUrl` and `pollUrl`. 4. Store `referenceNumber` for tracking, then redirect the customer to `redirectUrl` to complete payment. ## Headers [Section titled “Headers”](#headers) | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------- | | `authorization` | string | Yes | Your application’s integration key | | `content-type` | string | Yes | Must be `application/json` | ## Request body [Section titled “Request body”](#request-body) The fields below are the **plaintext** shape — encrypt the entire object before sending (see [Encryption Guide](/security/encryption/)). | Field | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `amountDetails` | object | Yes | `{ amount: number, currencyCode: string }` | | `reasonForPayment` | string | Yes | A short summary of the transaction | | `resultUrl` | string | Yes | Where Pesepay posts the final transaction result — see [The Result Callback](/webhooks/result-callback/) | | `returnUrl` | string | Yes | Where the customer’s browser is sent after completing or cancelling | | `merchantReference` | string | No | Your own order/invoice reference, echoed back on the transaction | | `paymentMethodCode` | string | No | Pre-selects a method so the hosted page opens straight into it — see [Payment Method Codes](/resources/payment-method-codes/) | | `paymentMetadata` | object | No | String key/value pairs carried on the transaction and returned as `transactionMetadata` on the result. **Required** on an application with [split payments](/payments/split-payments/), which must carry `beneficiaryMerchantEmail` | Plaintext request body (before encryption) ```json { "amountDetails": { "amount": 10.00, "currencyCode": "USD" }, "reasonForPayment": "Order #1042 — running shoes", "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return" } ``` ## Code examples [Section titled “Code examples”](#code-examples) Each sample builds the request, encrypts it, sends it, and decrypts the response — see the [Encryption Guide](/security/encryption/) for the `encrypt`/`decrypt` helpers used here. * cURL ```bash # Encrypt your JSON body first (see the Encryption Guide), then: curl -X POST https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate \ -H "authorization: YOUR_INTEGRATION_KEY" \ -H "content-type: application/json" \ -d '{"payload": "ENCRYPTED_BASE64_STRING"}' ``` * Node.js ```javascript const body = { amountDetails: { amount: 10.0, currencyCode: 'USD' }, reasonForPayment: 'Order #1042 — running shoes', resultUrl: 'https://example.com/payments/result', returnUrl: 'https://example.com/payments/return', }; const response = await fetch( 'https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate', { method: 'POST', headers: { authorization: 'YOUR_INTEGRATION_KEY', 'content-type': 'application/json', }, body: JSON.stringify({ payload: encrypt(body, ENCRYPTION_KEY) }), } ); const { payload } = await response.json(); const transaction = decrypt(payload, ENCRYPTION_KEY); console.log(transaction.redirectUrl); // send the customer here console.log(transaction.referenceNumber); // store this for tracking ``` * Python ```python import requests body = { "amountDetails": {"amount": 10.00, "currencyCode": "USD"}, "reasonForPayment": "Order #1042 — running shoes", "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return", } response = requests.post( "https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate", json={"payload": encrypt(body, ENCRYPTION_KEY)}, headers={ "authorization": "YOUR_INTEGRATION_KEY", "content-type": "application/json", }, ) transaction = decrypt(response.json()["payload"], ENCRYPTION_KEY) print(transaction["redirectUrl"]) # send the customer here print(transaction["referenceNumber"]) # store this for tracking ``` * PHP ```php ['amount' => 10.00, 'currencyCode' => 'USD'], 'reasonForPayment' => 'Order #1042 — running shoes', 'resultUrl' => 'https://example.com/payments/result', 'returnUrl' => 'https://example.com/payments/return', ]; $ch = curl_init('https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'payload' => encrypt($body, $encryptionKey), ])); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'authorization: YOUR_INTEGRATION_KEY', 'content-type: application/json', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); curl_close($ch); $transaction = decrypt($response['payload'], $encryptionKey); echo $transaction['redirectUrl']; // send the customer here echo $transaction['referenceNumber']; // store this for tracking ``` * Java ```java String bodyJson = """ {"amountDetails":{"amount":10.00,"currencyCode":"USD"}, "reasonForPayment":"Order #1042 — running shoes", "resultUrl":"https://example.com/payments/result", "returnUrl":"https://example.com/payments/return"}"""; String encryptedPayload = encrypt(bodyJson, encryptionKey); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.test.sandbox.pesepay.com/payments-engine/v1/payments/initiate")) .header("authorization", "YOUR_INTEGRATION_KEY") .header("content-type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"payload\":\"" + encryptedPayload + "\"}" )) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); String transactionJson = decrypt(extractPayload(response.body()), encryptionKey); ``` ## Response [Section titled “Response”](#response) The response is an encrypted `payload`. Decrypt it to get these three fields — and only these three. There is no status or amount detail here; call [Check Payment Status](/api/check-payment-status/) for that. | Field | Type | Description | | ----------------- | ------ | ---------------------------------------------------------------------------------------- | | `referenceNumber` | string | **Store this** — used to track status and match result callbacks | | `redirectUrl` | string | **Redirect your customer here** to complete payment | | `pollUrl` | string | A ready-made [Check Payment Status](/api/check-payment-status/) URL for this transaction | Caution After decrypting the response, store `referenceNumber` and redirect the customer to `redirectUrl`. Don’t consider the payment complete until you receive the [result callback](/webhooks/result-callback/) or check status and see a terminal [transaction status](/resources/transaction-statuses/).
# Introduction
> Base URLs, authentication, the encrypted payload envelope, and versioning for the Pesepay API.
New to working with HTTP APIs? Start with [REST & JSON basics](/api/rest-basics/) for the vocabulary this reference uses. ## Base URLs [Section titled “Base URLs”](#base-urls) | Environment | Base URL | | ----------- | --------------------------------------------------------- | | Production | `https://api.pesepay.com/api/payments-engine/v1` | | Sandbox | `https://api.test.sandbox.pesepay.com/payments-engine/v1` | Every endpoint page in this reference shows both — use sandbox while building, and see the [go-live checklist](/getting-started/go-live-checklist/) before switching to production. ## Authentication [Section titled “Authentication”](#authentication) Send your application’s **integration key** in the `authorization` header on every request: ```plaintext authorization: YOUR_INTEGRATION_KEY content-type: application/json ``` Find your integration key under [Onboarding](/getting-started/onboarding/). Never send this header from browser or mobile app code — see [API Keys & Credentials](/security/api-keys/). ## The encrypted envelope [Section titled “The encrypted envelope”](#the-encrypted-envelope) Integration endpoints (initiate, make payment, check status) don’t accept or return plain JSON. Every request body and response body is an AES-256-CBC encrypted string, wrapped like this: ```json { "payload": "base64_encoded_encrypted_string" } ``` Encrypt your request JSON and decrypt every response using your application’s encryption key before reading or sending any fields. Full walkthrough with code in five languages: [Encryption Guide](/security/encryption/). ## Versioning [Section titled “Versioning”](#versioning) The current API version is `v1`, reflected in the base URL path. Breaking changes will ship under a new version path; additive changes (new optional fields, new payment methods) won’t require a version bump — so treat unrecognised response fields as something to ignore, not something to fail on. The exception already in the wild is [Make Payment](/api/make-payment/), which is `v2`. ## Rate limits [Section titled “Rate limits”](#rate-limits) Pesepay does not currently enforce rate limits on the API — there are no per-second or per-day request quotas, and no `429` responses to handle. That is not a licence to poll aggressively. When you are [checking payment status](/payments/checking-status/), poll on a sensible interval (a few seconds) and stop as soon as the transaction reaches a [terminal status](/resources/transaction-statuses/), so your integration keeps working if limits are introduced later. ## Machine-readable spec [Section titled “Machine-readable spec”](#machine-readable-spec) The API is also published as an [OpenAPI 3.1 document](/api/openapi/) with a generated Postman collection — use it to generate client types or drive an API console instead of transcribing fields from these pages. ## Using these docs with an AI assistant [Section titled “Using these docs with an AI assistant”](#using-these-docs-with-an-ai-assistant) Every page on this site is also published as plain Markdown, and the whole site is available as a single file. Paste a URL into your assistant, or fetch it. | File | What’s in it | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `https://developers.pesepay.com/llms.txt` | An index of the documentation, plus the handful of rules that are most often got wrong (encryption, the two currency codes, redirect-only cards) | | `https://developers.pesepay.com/llms-full.txt` | The complete documentation as one file | | `https://developers.pesepay.com/llms-small.txt` | The same, with asides and non-essential content stripped, for smaller context windows | | `https://developers.pesepay.com/_llms-txt/api-reference.txt` | Just this API reference and the data models | | `https://developers.pesepay.com/_llms-txt/payment-methods.txt` | Just the per-method codes, limits and required fields | | `https://developers.pesepay.com/_llms-txt/getting-started.txt` | Onboarding, the quickstart, both flows and the encryption scheme | For a single page, add `.md` to its path — this page is at `https://developers.pesepay.com/api/introduction.md`. Caution An assistant working from these files still can’t test against the sandbox for you, and it will happily invent field names. Check anything it generates against the endpoint pages here before you ship it.
# Make Payment
> Submit a payment directly for a specific payment method, without redirecting to a hosted page.
POST Creates and processes a transaction for a specific payment method in one call. This is the core request in the [seamless flow](/payments/seamless-flow/) — your UI collects the method’s required fields and submits them directly, without redirecting to a Pesepay-hosted page. | Environment | URL | | ----------- | ------------------------------------------------------------------------------- | | Production | `https://api.pesepay.com/api/payments-engine/v2/payments/make-payment` | | Sandbox | `https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment` | ## How it fits [Section titled “How it fits”](#how-it-fits) 1. Pick a [payment method](/payment-methods/overview/) and its required fields. 2. Build and [encrypt](/security/encryption/) the request body. 3. POST it with your integration key in the `authorization` header. 4. Decrypt the response, store the `referenceNumber`, and wait for the [result callback](/webhooks/result-callback/) or poll `pollUrl` — the customer still has to approve the payment on their side. ## Headers [Section titled “Headers”](#headers) | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------- | | `authorization` | string | Yes | Your application’s integration key | | `content-type` | string | Yes | Must be `application/json` | ## Request body [Section titled “Request body”](#request-body) | Field | Type | Required | Description | | ----------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `amountDetails` | object | Yes | `{ amount: number, currencyCode: string }` | | `paymentMethodCode` | string | Yes | e.g. `PZW211` for EcoCash, `PZW212` for InnBucks — see [Payment Method Codes](/resources/payment-method-codes/) | | `paymentMethodRequiredFields` | object | Yes | The chosen method’s required fields as key/value pairs. Send `{}` for methods that need none (InnBucks, PayGo). Keys come from the method’s [Payment Methods](/payment-methods/overview/) page or from [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/) | | `reasonForPayment` | string | Yes | A short summary of the transaction | | `resultUrl` | string | Yes | Where Pesepay posts the final result — see [The Result Callback](/webhooks/result-callback/) | | `returnUrl` | string | No | Where the customer is sent after the payment. Defaults to `resultUrl` if omitted | | `merchantReference` | string | No | Your own order/invoice reference, echoed back on the transaction | | `customer` | object | No | `{ email, phoneNumber, name }` | | `paymentMetadata` | object | No | String key/value pairs carried on the transaction and returned as `transactionMetadata` on the result. **Required** on an application with [split payments](/payments/split-payments/), which must carry `beneficiaryMerchantEmail` | Plaintext request body — EcoCash example (before encryption) ```json { "amountDetails": { "amount": 10.00, "currencyCode": "USD" }, "merchantReference": "ORDER-1042", "reasonForPayment": "Order #1042 — running shoes", "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return", "paymentMethodCode": "PZW211", "customer": { "email": "customer@example.com", "phoneNumber": "0777777777", "name": "Jane Customer" }, "paymentMethodRequiredFields": { "customerPhoneNumber": "0777777777" } } ``` ## Code examples [Section titled “Code examples”](#code-examples) * cURL ```bash # Encrypt your JSON body first (see the Encryption Guide), then: curl -X POST https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment \ -H "authorization: YOUR_INTEGRATION_KEY" \ -H "content-type: application/json" \ -d '{"payload": "ENCRYPTED_BASE64_STRING"}' ``` * Node.js ```javascript const body = { amountDetails: { amount: 10.0, currencyCode: 'USD' }, merchantReference: 'ORDER-1042', reasonForPayment: 'Order #1042 — running shoes', resultUrl: 'https://example.com/payments/result', returnUrl: 'https://example.com/payments/return', paymentMethodCode: 'PZW211', customer: { email: 'customer@example.com', phoneNumber: '0777777777', name: 'Jane Customer', }, paymentMethodRequiredFields: { customerPhoneNumber: '0777777777' }, }; const response = await fetch( 'https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment', { method: 'POST', headers: { authorization: 'YOUR_INTEGRATION_KEY', 'content-type': 'application/json', }, body: JSON.stringify({ payload: encrypt(body, ENCRYPTION_KEY) }), } ); const { payload } = await response.json(); const transaction = decrypt(payload, ENCRYPTION_KEY); ``` * Python ```python import requests body = { "amountDetails": {"amount": 10.00, "currencyCode": "USD"}, "merchantReference": "ORDER-1042", "reasonForPayment": "Order #1042 — running shoes", "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return", "paymentMethodCode": "PZW211", "customer": { "email": "customer@example.com", "phoneNumber": "0777777777", "name": "Jane Customer", }, "paymentMethodRequiredFields": {"customerPhoneNumber": "0777777777"}, } response = requests.post( "https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment", json={"payload": encrypt(body, ENCRYPTION_KEY)}, headers={ "authorization": "YOUR_INTEGRATION_KEY", "content-type": "application/json", }, ) transaction = decrypt(response.json()["payload"], ENCRYPTION_KEY) ``` * PHP ```php ['amount' => 10.00, 'currencyCode' => 'USD'], 'merchantReference' => 'ORDER-1042', 'reasonForPayment' => 'Order #1042 — running shoes', 'resultUrl' => 'https://example.com/payments/result', 'returnUrl' => 'https://example.com/payments/return', 'paymentMethodCode' => 'PZW211', 'customer' => [ 'email' => 'customer@example.com', 'phoneNumber' => '0777777777', 'name' => 'Jane Customer', ], 'paymentMethodRequiredFields' => ['customerPhoneNumber' => '0777777777'], ]; $ch = curl_init('https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'payload' => encrypt($body, $encryptionKey), ])); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'authorization: YOUR_INTEGRATION_KEY', 'content-type: application/json', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); curl_close($ch); $transaction = decrypt($response['payload'], $encryptionKey); ``` * Java ```java String bodyJson = """ {"amountDetails":{"amount":10.00,"currencyCode":"USD"}, "merchantReference":"ORDER-1042", "reasonForPayment":"Order #1042 — running shoes", "resultUrl":"https://example.com/payments/result", "returnUrl":"https://example.com/payments/return", "paymentMethodCode":"PZW211", "customer":{"email":"customer@example.com","phoneNumber":"0777777777","name":"Jane Customer"}, "paymentMethodRequiredFields":{"customerPhoneNumber":"0777777777"}}"""; String encryptedPayload = encrypt(bodyJson, encryptionKey); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.test.sandbox.pesepay.com/payments-engine/v2/payments/make-payment")) .header("authorization", "YOUR_INTEGRATION_KEY") .header("content-type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"payload\":\"" + encryptedPayload + "\"}" )) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); String transactionJson = decrypt(extractPayload(response.body()), encryptionKey); ``` ## Response [Section titled “Response”](#response) Decrypt the `payload` to get the transaction result. It is the **same object** you get from [Check Payment Status](/api/check-payment-status/) and in the [result callback](/webhooks/result-callback/) — the [result callback page](/webhooks/result-callback/#payload) has the complete field list and the `amountDetails` breakdown. There is no `redirectUrl` on it. | Field | Type | Description | | ------------------------------ | ------ | ---------------------------------------------------------------------------------------- | | `referenceNumber` | string | **Store this** — used to track status and match result callbacks | | `pollUrl` | string | A ready-made [Check Payment Status](/api/check-payment-status/) URL for this transaction | | `transactionStatus` | string | See [Transaction statuses](/resources/transaction-statuses/) | | `transactionStatusCode` | number | Numeric equivalent of `transactionStatus` | | `transactionStatusDescription` | string | Human-readable description of the status | | `amountDetails` | object | Amounts and fees applied to the transaction | | `transactionMetadata` | object | String key/value pairs carried on the transaction | Caution A `200` here does **not** mean you have been paid. Most methods are still awaiting the customer at this point — the status will be non-terminal. Wait for the [result callback](/webhooks/result-callback/) or poll `pollUrl` until the status is [terminal](/resources/transaction-statuses/), and only treat `SUCCESS` as paid.
# OpenAPI spec & Postman
> The machine-readable Pesepay API description, and a Postman collection generated from it.
The Pesepay API is described as an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document. Use it to generate client types, build request tables, or drive an API console — instead of copying fields out of these pages by hand. | File | Use it for | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | `https://developers.pesepay.com/openapi.yaml` | The full spec — endpoints, schemas, examples, error shapes | | `https://developers.pesepay.com/postman/pesepay.postman_collection.json` | A Postman collection, generated from the spec — import it and fill in `apiKey` | It covers the endpoints a merchant integration uses: [Initiate Transaction](/api/initiate-transaction/), [Make Payment](/api/make-payment/), [Check Payment Status](/api/check-payment-status/), [Get Active Currencies](/api/get-active-currencies/) and [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/), plus the [result callback](/webhooks/result-callback/) as an OpenAPI `webhooks` entry. ## The one thing the spec can’t model for you [Section titled “The one thing the spec can’t model for you”](#the-one-thing-the-spec-cant-model-for-you) The three authenticated endpoints don’t send or receive plain JSON. The real body, both ways, is the encrypted envelope: ```json { "payload": "" } ``` The spec shows the **decrypted** JSON as each operation’s request and response body, because that is what is useful for generating models and field tables — but a generated client, or a “try it” console, will still send the plaintext and get a `400` until you add the encryption step yourself. Operations that need it are marked `x-pesepay-transport: aes-256-cbc-envelope`. See the [Encryption Guide](/security/encryption/) for the cipher, and [Introduction](/api/introduction/#the-encrypted-envelope) for the envelope. [Get Active Currencies](/api/get-active-currencies/) and [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/) are plain JSON with no auth — those work straight from a generated client. ## Generating a client [Section titled “Generating a client”](#generating-a-client) * TypeScript ```bash npx openapi-typescript https://developers.pesepay.com/openapi.yaml -o pesepay.d.ts ``` * Python ```bash pip install openapi-python-client openapi-python-client generate --url https://developers.pesepay.com/openapi.yaml ``` * Any (OpenAPI Generator) ```bash npx @openapitools/openapi-generator-cli generate \ -i https://developers.pesepay.com/openapi.yaml \ -g -o ./pesepay-client ``` Caution The spec is currently hand-maintained alongside these pages, not generated from the backend. Treat a mismatch between the two as a bug and [report it](https://developers.pesepay.com). If you need a guarantee, verify against a real sandbox call.
# REST & JSON basics
> The HTTP and JSON vocabulary this reference assumes — for developers new to working with web APIs.
If you’ve worked with a REST API before, skip this — head to [Introduction](/api/introduction/). If not, here’s the vocabulary the rest of this reference uses. ## REST [Section titled “REST”](#rest) The Pesepay API is a **REST** API: you interact with it by sending HTTP requests to URLs (endpoints), and it follows HTTP conventions for methods, headers and status codes. Every Pesepay endpoint is one of two methods: | Method | Meaning | Pesepay endpoints | | ------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET` | Read something, changing nothing | [Check Payment Status](/api/check-payment-status/), [Get Active Currencies](/api/get-active-currencies/), [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/) | | `POST` | Create or submit something | [Initiate Transaction](/api/initiate-transaction/), [Make Payment](/api/make-payment/) | ## HTTP [Section titled “HTTP”](#http) Requests are made over **HTTPS** (HTTP encrypted with TLS) — plain `http://` is rejected. A request has four parts: the **method**, the **endpoint** URL, **headers** (Pesepay uses `authorization` and `content-type`), and a **body**. A response has three: a **status code**, **headers**, and a **body**. The **status code** tells you what happened: `2xx` succeeded, `4xx` means your request was wrong (bad field, missing auth), `5xx` means the server failed. The full table is on the [Errors](/api/errors/) page. ## JSON [Section titled “JSON”](#json) Request and response bodies are **JSON** — though with Pesepay the meaningful JSON is [encrypted inside an envelope](/api/introduction/#the-encrypted-envelope). JSON has a small set of types, and the field tables in this reference use these names: | Type | Example | | ------- | --------------------------------------------------------------------- | | string | `"Order #1042"` | | number | `10.50` | | boolean | `true` / `false` | | null | `null` — no value | | object | `{ "amount": 10, "currencyCode": "USD" }` — key/value pairs in braces | | array | `["USD", "ZiG"]` — an ordered list in brackets |
# Currency model
> Fields on the Currency objects returned by Get Active Currencies.
Currency ```json { "active": true, "code": "string", "defaultCurrency": true, "description": "string", "id": 0, "name": "string", "rateToDefault": 0 } ``` | Field | Type | Description | | ----------------- | ------- | ----------------------------------------------------- | | `active` | boolean | Whether the currency is currently active | | `code` | string | The unique code assigned to the currency (e.g. `USD`) | | `defaultCurrency` | boolean | Whether this is the system’s default currency | | `description` | string | Description of the currency | | `name` | string | The currency’s name | | `rateToDefault` | number | Exchange rate to the default currency | Returned by [Get Active Currencies](/api/get-active-currencies/). Caution Zimbabwe dollars are addressed by **two** currency codes — `ZiG` for [EcoCash](/payment-methods/ecocash/) and [PayGo](/payment-methods/paygo/), `ZWG` for [Zimswitch](/payment-methods/zimswitch/) and [Omari](/payment-methods/omari/). Both are chargeable, but [Get Active Currencies](/api/get-active-currencies/) lists only `ZiG`, so take the code from the method you are charging — see [Payment method codes](/resources/payment-method-codes/).
# Payment method codes
> The paymentMethodCode to send for each payment method, in each currency.
Send one of these as `paymentMethodCode` in [Make Payment](/api/make-payment/). Codes are per payment method **and** currency — the same wallet has a different code in each currency it supports. | Method | US dollars (`USD`) | Zimbabwe dollars | Currency code | Seamless flow | | --------------------------------------------- | ------------------ | ---------------- | ------------- | --------------- | | [EcoCash](/payment-methods/ecocash/) | `PZW211` | `PZW201` | `ZiG` | ✅ | | [InnBucks](/payment-methods/innbucks/) | `PZW212` | — | — | ✅ | | [Omari](/payment-methods/omari/) | `PZW216` | `PZW217` | `ZWG` | ✅ | | [PayGo](/payment-methods/paygo/) | — | `PZW210` | `ZiG` | ✅ | | [Zimswitch](/payment-methods/zimswitch/) | `PZW215` | `PZW213` | `ZWG` | ❌ redirect only | | [Visa](/payment-methods/card-payments/) | `PZW204` | — | — | ❌ redirect only | | [Mastercard](/payment-methods/card-payments/) | `PZW205` | — | — | ❌ redirect only | The **currency code** column is the string to send as `currencyCode` alongside the Zimbabwe dollar code on that row. Caution Zimbabwe dollars use two different currency codes — `ZiG` for EcoCash and PayGo, `ZWG` for Omari and Zimswitch. Both are chargeable; send the one in the row for the method you’re using. [Get Active Currencies](/api/get-active-currencies/) returns only `ZiG`, so don’t use it to decide whether `ZWG` is available. Amount limits differ per code; see the [payment methods overview](/payment-methods/overview/) for the full matrix.
# Payment method model
> Fields on the Payment Method objects returned by Get Payment Methods by Currency.
Payment Method ```json { "active": true, "code": "string", "currencies": ["string"], "description": "string", "id": 0, "maximumAmount": 0, "minimumAmount": 0, "name": "string", "processingPaymentMessage": "string", "redirectRequired": true, "redirectURL": "string", "requiredFields": [ { "displayName": "string", "fieldType": "DATE", "name": "string", "optional": true } ] } ``` | Field | Type | Description | | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `active` | boolean | Whether the payment method is currently active | | `code` | string | The unique code assigned to the payment method (e.g. `PZW211`) — see [Payment Method Codes](/resources/payment-method-codes/) | | `currencies` | array | Currencies this method accepts | | `description` | string | Description of the payment method | | `maximumAmount` | number | Maximum transactable amount for this method | | `minimumAmount` | number | Minimum transactable amount for this method | | `name` | string | The payment method’s name | | `processingPaymentMessage` | string | Message to display while the transaction is processing | | `redirectRequired` | boolean | Whether this method requires a redirect to complete | | `redirectURL` | string | The redirect URL, when `redirectRequired` is `true` | | `requiredFields` | array | Fields you must collect and send in `paymentMethodRequiredFields` for the [seamless flow](/payments/seamless-flow/). `fieldType` is one of `DATE`, `FILE`, `NUMBER`, or `TEXT` | Returned by [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/). Build your seamless-flow forms from `requiredFields` dynamically instead of hardcoding fields per method.
# Transaction model
> Every field on the full Transaction entity, and how it relates to what the API actually returns.
This is the **full transaction entity**. The endpoints you integrate against return smaller views of it: * [Initiate Transaction](/api/initiate-transaction/#response) returns only `referenceNumber`, `redirectUrl` and `pollUrl`. * [Make Payment](/api/make-payment/#response), [Check Payment Status](/api/check-payment-status/) and the [result callback](/webhooks/result-callback/#payload) return the transaction **result** — the subset listed on the result callback page. It has no `redirectUrl`, `redirectRequired`, `settlementMode`, `liquidationStatus` or `paymentMethodDetails`. Use the table below to understand a field’s meaning; use the pages above for what each call actually gives you. Transaction (full entity) ```json { "amountDetails": { "amount": 0, "currencyCode": "string", "customerPayableAmount": 0, "defaultCurrencyAmount": 0, "defaultCurrencyCode": "string", "formattedMerchantAmount": "string", "merchantAmount": 0, "totalTransactionAmount": 0, "transactionServiceFee": 0 }, "applicationCode": "string", "applicationName": "string", "chargeType": "NO_CHARGE", "customer": { "contactNumbers": ["string"], "email": "string", "name": "string" }, "customerAmountPaid": { "amountPaid": 0, "currencyCode": "string" }, "dateOfTransaction": "string", "id": 0, "internalReference": "string", "liquidationStatus": "COMPLETED", "liquidationTransactionReference": "string", "merchantReference": "string", "paymentMetadata": {}, "paymentMethodDetails": { "paymentMethodCode": "string", "paymentMethodId": 0, "paymentMethodMessage": "string", "paymentMethodName": "string", "paymentMethodReference": "string", "paymentMethodStatus": "string" }, "pollUrl": "string", "reasonForPayment": "string", "redirectRequired": true, "redirectUrl": "string", "referenceNumber": "string", "resultUrl": "string", "returnUrl": "string", "settlementMode": "DIRECTLY_SETTLED", "transactionStatus": "AUTHORIZATION_FAILED", "transactionType": "BASIC" } ``` | Field | Type | Description | | --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- | | `amountDetails` | object | Currency, amount charged to the customer, and fees for the transaction | | `applicationCode` | string | The unique code assigned to your application on creation | | `applicationName` | string | Your application’s name | | `chargeType` | string | `NO_CHARGE`, `SHARED_TRANSACTIONAL_CHARGE`, `TRANSACTIONAL_CHARGE_FOR_CUSTOMER`, or `TRANSACTIONAL_CHARGE_FOR_MERCHANT` | | `customer` | object | The customer’s details | | `customerAmountPaid` | object | The amount actually paid by the customer | | `dateOfTransaction` | string | Date and time the transaction was initiated | | `internalReference` | string | Pesepay’s internal transaction reference | | `liquidationStatus` | string | `COMPLETED`, `DUE_FOR_LIQUIDATION`, `IN_PROGRESS`, `NO_LIQUIDATION_REQUIRED`, `PENDING`, or `WAITING_FOR_DETERMINATION` | | `liquidationTransactionReference` | string | The liquidation transaction reference | | `merchantReference` | string | Your own reference for the transaction | | `paymentMetadata` | object | Additional payment information | | `paymentMethodDetails` | object | Details of the payment method used | | `pollUrl` | string | URL to poll for a change in transaction status | | `reasonForPayment` | string | The subject of the transaction | | `redirectRequired` | boolean | Whether the payment method requires a redirect | | `redirectUrl` | string | The redirect URL, when applicable | | `referenceNumber` | string | The transaction’s reference number — your primary tracking key | | `resultUrl` | string | The URL Pesepay posts the transaction result to | | `returnUrl` | string | The URL the customer is returned to after processing | | `settlementMode` | string | How the transaction settles | | `transactionStatus` | string | See [Transaction Statuses](/resources/transaction-statuses/) | | `transactionType` | string | `BASIC` or `INVOICE` |
# Transaction statuses
> Every value transactionStatus can hold, its numeric code, and whether it's terminal.
`transactionStatus` tells you where a payment stands. It appears in [Check Payment Status](/api/check-payment-status/) responses and in the [result callback](/webhooks/result-callback/), alongside `transactionStatusCode` (the numeric equivalent) and `transactionStatusDescription` (a human-readable message). **Terminal** statuses are final — the payment will not change again, and a [result callback](/webhooks/result-callback/) fires when one is reached. Stop polling once you see one. ## In progress (keep checking) [Section titled “In progress (keep checking)”](#in-progress-keep-checking) | Status | Code | Meaning | | ---------------- | ----- | --------------------------------- | | `INITIATED` | `301` | Transaction has been initiated | | `PROCESSING` | `302` | Transaction is being processed | | `PENDING` | `303` | Transaction is pending processing | | `PARTIALLY_PAID` | `315` | Transaction is partially paid | Caution `PARTIALLY_PAID` is **not** terminal, and no callback fires for it — it’s a payment still in flight, not a final outcome. Don’t fulfil an order on it, and don’t stop polling. ## Terminal — paid [Section titled “Terminal — paid”](#terminal--paid) | Status | Code | Meaning | | --------- | ----- | -------------------------------------- | | `SUCCESS` | `304` | Transaction was successfully completed | `SUCCESS` is the only status that means you have been paid. Treat every other terminal status as unpaid. ## Terminal — not paid [Section titled “Terminal — not paid”](#terminal--not-paid) | Status | Code | Meaning | | ----------------------- | ----- | ------------------------------------------------------- | | `FAILED` | `300` | Transaction has failed | | `TERMINATED` | `305` | Transaction was terminated | | `TIME_OUT` | `306` | Transaction timed out | | `CLOSED` | `307` | Transaction is closed | | `CLOSED_PERIOD_ELAPSED` | `307` | Closed by Pesepay — the transaction’s period elapsed | | `INSUFFICIENT_FUNDS` | `308` | Transaction failed due to insufficient funds | | `CANCELLED` | `309` | Transaction was cancelled | | `ERROR` | `310` | An error occurred | | `DECLINED` | `311` | Declined by the service provider | | `AUTHORIZATION_FAILED` | `312` | Authorization failed at the customer’s service provider | | `SERVICE_UNAVAILABLE` | `313` | The payment provider was unavailable | | `REVERSED` | `314` | A previously successful payment was reversed | `REVERSED` is what you see when an [EcoCash payment above the $500 limit](/payment-methods/ecocash/#payments-above-the-limit) was collected in several legs and one of them failed: the legs that succeeded were refunded automatically. `DECLINED`, `INSUFFICIENT_FUNDS`, `AUTHORIZATION_FAILED`, `TIME_OUT`, and `CANCELLED` are all normal, expected outcomes — handle them gracefully in your UI rather than treating them as integration bugs. See [Checking Payment Status](/payments/checking-status/) for how to read this field via the [result callback](/webhooks/result-callback/) or [Check Payment Status](/api/check-payment-status/).