Getting started: onboarding, the quickstart, both payment flows and the encryption scheme
# Go-live checklist
> What to verify before switching an application from sandbox to production credentials.
Work through this before you point an application at production. ## Credentials and environment [Section titled “Credentials and environment”](#credentials-and-environment) * [ ] Your Pesepay merchant account has been approved (see [Onboarding](/getting-started/onboarding/)) * [ ] You’ve swapped sandbox base URLs (`api.test.sandbox.pesepay.com`) for production (`api.pesepay.com`) — see [API Introduction](/api/introduction/) * [ ] Your production **integration key** and **encryption key** are stored as server-side secrets, never in client code — see [API Keys & Credentials](/security/api-keys/) * [ ] Sandbox keys are gone from the production build entirely, not just unused * [ ] Your [payout account](/getting-started/onboarding/#3-payout-accounts) is configured and approved, for **every currency** you charge in ## Getting paid reliably [Section titled “Getting paid reliably”](#getting-paid-reliably) * [ ] `resultUrl` is a publicly reachable HTTPS endpoint on your server that handles the [result callback](/webhooks/result-callback/) * [ ] Your handler **verifies before fulfilling** — it calls [Check Payment Status](/api/check-payment-status/) rather than trusting the callback body ([why](/webhooks/verifying-callbacks/)) * [ ] Your handler is **idempotent**, keyed on `referenceNumber` * [ ] You run a **reconciliation job** for orders whose callback never arrived — [callbacks are never retried](/webhooks/retries/) * [ ] Nothing marks an order paid on `returnUrl` alone ## Payments and amounts [Section titled “Payments and amounts”](#payments-and-amounts) * [ ] You’ve tested at least one full payment per method you plan to support, using real small-value transactions * [ ] You’ve tested a **failed** payment per method, not just successes * [ ] You have made a small-value production payment for every method the [sandbox can’t exercise](/testing/sandbox-environment/) — InnBucks, Omari, Zimswitch, PayGo, and any Zimbabwe dollar checkout * [ ] Your checkout validates amounts against each method’s limits — they differ per method and currency, and [Get Payment Methods by Currency](/api/get-payment-methods-by-currency/) returns the live values * [ ] If you accept EcoCash, you handle [payments above $500](/payment-methods/ecocash/#payments-above-the-limit) — the customer sees several prompts, and a partial failure reverses the whole payment * [ ] Your error handling covers the statuses in the [error catalog](/api/errors/) and the non-success [transaction statuses](/resources/transaction-statuses/), not just the happy path ## If you take cards [Section titled “If you take cards”](#if-you-take-cards) * [ ] You are sending customers to the hosted page for card entry — card payments are [redirect-only](/payment-methods/card-payments/), so no card data should ever reach your servers
# Onboarding
> Create your Pesepay account, add an application, and find your integration and encryption keys.
Set up your account once, then create an **application** per website or app you want to accept payments on — each application gets its own integration key and encryption key. ## 1. Create your account [Section titled “1. Create your account”](#1-create-your-account) 1. [Register for a Pesepay account](http://dashboard.pesepay.com/#/auth/register). 2. Check your email for your username and a system-generated password, then log in and change your password. 3. Open **My Business** in the left-hand menu and choose how you’re registering — **Personal** (individual or sole trader) or **Corporate** (registered company) — then click **Get Started**. 4. Fill in your **Business Profile** and click **Submit**. The documents are uploaded in the same form, alongside your business details, so have them ready before you start: | Registering as | Uploads the form requires | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Personal** | **Identification Document** (government-issued national ID or passport) and **Proof of Address** | | **Corporate** | **CR14 / Deed of Trust**, **Certificate of Incorporation**, and — for each business director — their email, phone number, full name and **Proof of Identification** | Bank account proof isn’t part of this form — it’s uploaded per application when you add a [payout account](#3-payout-accounts). 5. Sign the **Merchant Agreement**. Once your profile is saved, **My Business** shows a link to download the agreement: download it, sign it, upload it under **Upload Signed Merchant Agreement Contract**, and click **Submit**. This sends your account for review. ## 2. Add your first application [Section titled “2. Add your first application”](#2-add-your-first-application) 1. In the Merchant Control Panel, open **Applications** in the left-hand menu. 2. Click **Create** and fill in the application details — this can represent a website, a mobile app, or any product surface you’re integrating. 3. If you want a custom payment page instead of the default Pesepay-hosted one, opt in and provide its URL. 4. Submit. Your application now appears in the applications list. ### Find your keys [Section titled “Find your keys”](#find-your-keys) In the applications list, click **View Details** on the application. The **Application Details** tab shows both keys: | Key | Used for | | ------------------- | ---------------------------------------------------------------------------------------- | | **Integration key** | The `authorization` header on every API request | | **Encryption key** | Encrypting requests and decrypting responses ([Encryption Guide](/security/encryption/)) | The other tabs on that page are **Payout Accounts (Indirect Settlement)**, **Payout Accounts (Direct Settlement)**, **Transactional Fees**, **Invoices** and **Tracking Orders**. Caution Each application’s keys belong to **one environment**. Sandbox keys work only against the sandbox URLs and live keys only against the production URLs — see [Sandbox vs live keys](/security/api-keys/#sandbox-vs-live-keys) for what mixing them looks like when it goes wrong. Caution Treat both keys like passwords. Never call the Pesepay API from client-side/browser code — see [API Keys & Credentials](/security/api-keys/). ## 3. Payout accounts [Section titled “3. Payout accounts”](#3-payout-accounts) Settlement happens **T+2 days** from the day a customer completes a transaction, once the minimum amount defined in your Merchant Agreement is reached. Payout accounts are configured **per application**, and any change (adding or editing an account) requires Pesepay approval — while a request is pending, new sales continue to process but settlement to that account is on hold. To add one: **Applications → View Details → Payout Accounts (Indirect Settlement) → Create**, then provide the account name, account number, settlement notification email address, currency, bank, and an uploaded bank statement. ## 4. Give your team access [Section titled “4. Give your team access”](#4-give-your-team-access) Other people in your organisation get into the Merchant Control Panel through **groups**. A group is a set of users with the same role — Accounts Clerks, Supervisors, Managers — and permissions are granted to the group, not to individuals. A user takes on their group’s permissions the moment they’re added to it. 1. **Create the groups.** **User Manager → Groups → Create Group**, give it a name and description, and submit. 2. **Assign permissions.** On the group’s ⋮ menu choose **View Group Permissions**. The screen has two lists — **Unassigned Permissions** on the left, **Permissions to Assign** on the right. Select the permissions the role needs and use **Assign Selected** (or **Assign All**) to move them across; **Remove Selected** / **Remove All** takes them back. You can come back and change this at any time. 3. **Create the users.** **User Manager → Users**, then add each person — initials, first and last name, username, phone number and email — and put them in a group. They receive login credentials by email and inherit that group’s permissions automatically. The users list shows each person’s username, email, group and status (**Active**, **Inactive** or **Account Locked**), and a user can be moved to a different group later — their permissions change with them, immediately. Group the permissions by what the role actually does. A common starting set is one group per function — administration, development, finance, support — so that, for example, a developer can read transactions and manage applications without being able to change payout accounts. ## 5. Working in the dashboard [Section titled “5. Working in the dashboard”](#5-working-in-the-dashboard) Once you’re live, day-to-day operations live under **Payments** in the left-hand menu, per application (use the application dropdown to switch): * **Payments → Transactions** — every payment for the selected application, with its status, reference number, amount and date. This is where you reconcile against your own records — see [Checking payment status](/payments/checking-status/) for doing the same thing programmatically. * **Payments → Invoices** — generate an invoice for an application and email it to a customer; they pay it through the same Pesepay flow. * **Payments → Payouts** — track settlements and their status against each payout account (see [section 3](#3-payout-accounts)). **Sandbox → Payments / Transactions** is the same pair of screens against the [sandbox environment](/testing/sandbox-environment/). ## Next step [Section titled “Next step”](#next-step) You have your keys — send your [first sandbox payment](/getting-started/quickstart/).
# Overview
> What Pesepay is, and the two ways to integrate payments into your product.
Pesepay is a payment gateway for Zimbabwe. One integration gives your application access to EcoCash, InnBucks, Omari, Zimswitch, PayGo and Visa/Mastercard, in US dollars and Zimbabwe dollars. Every request and response that carries payment details is encrypted with AES-256-CBC using a key unique to your application. See the [Encryption Guide](/security/encryption/) for the full spec. ## The two integration styles [Section titled “The two integration styles”](#the-two-integration-styles) ### [Redirect (checkout) flow](/payments/redirect-flow/) You **initiate** a transaction and redirect the customer to a hosted Pesepay payment page, where they choose a method and pay. Simplest to build — Pesepay’s page handles the payment method UI for you. 1. 1 Initiate 2. 2 Pesepay checkout 3. 3 Result callback Redirect flow guide ### [Seamless (direct) flow](/payments/seamless-flow/) You collect the payment method and its required fields (for example a phone number for EcoCash) in your own UI and call **make payment** directly. More control, more UI to build. 1. 1 Collect details 2. 2 Make payment 3. 3 Result callback Seamless flow guide Both flows return a `referenceNumber` you use to check status, and both can notify your server automatically via the [result callback](/webhooks/result-callback/) instead of you having to poll. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) 1 ### [Create a sandbox account](/getting-started/onboarding/) Set up an account and locate your integration key and encryption key. Find your keys 2 ### [Send your first payment](/getting-started/quickstart/) Take a sandbox transaction end to end in about five minutes. Run the quickstart 3 ### [Pick your payment methods](/payment-methods/overview/) Decide which channels to support and what each one requires. Browse methods 4 ### [Move to production](/getting-started/go-live-checklist/) Work through the checklist before you switch to live keys. Go-live checklist Using an AI coding assistant? Read [Building with AI tools](/sdks/ai-tools/) first — it links machine-readable versions of these docs and gives you a prompt that keeps assistants from inventing Pesepay endpoints, fields and currency codes.
# Quickstart: Your first payment
> Send a real sandbox payment in about five minutes using an official Pesepay SDK.
By the end of this page you’ll have redirected a test customer to the Pesepay sandbox payment page and gotten a transaction reference back — the core loop behind every Pesepay integration. ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * A [Pesepay account](/getting-started/onboarding/) with a sandbox **integration key** and **encryption key** * Node.js, Python, PHP, or Java installed locally Caution The official SDKs currently call the **production** API only — they have no sandbox base URL. To run this against the sandbox, use the raw HTTP calls in the [API reference](/api/initiate-transaction/), which show the sandbox URLs, and switch to an SDK once you’re integrating for real. 1. ### Install the SDK [Section titled “Install the SDK”](#install-the-sdk) * Node.js ```bash npm install pesepay ``` * Python ```bash pip install pesepay ``` * PHP ```bash composer require codevirtus/pesepay ``` * Java ```xml com.pesepay pesepay 1.0.0 ``` 2. ### Initialize the client [Section titled “Initialize the client”](#initialize-the-client) Use your **sandbox** integration key and encryption key from [Onboarding](/getting-started/onboarding/). * Node.js pesepay.js ```js const { Pesepay } = require('pesepay'); const pesepay = new Pesepay('YOUR_INTEGRATION_KEY', 'YOUR_ENCRYPTION_KEY'); pesepay.resultUrl = 'https://example.com/payments/result'; pesepay.returnUrl = 'https://example.com/payments/return'; ``` * Python pesepay\_client.py ```python from pesepay import Pesepay pesepay = Pesepay("YOUR_INTEGRATION_KEY", "YOUR_ENCRYPTION_KEY") pesepay.result_url = "https://example.com/payments/result" pesepay.return_url = "https://example.com/payments/return" ``` * PHP pesepay.php ```php returnUrl = 'https://example.com/payments/return'; $pesepay->resultUrl = 'https://example.com/payments/result'; ``` * Java PesepayClient.java ```java Pesepay pesepay = new Pesepay(integrationKey, encryptionKey); pesepay.setResultUrl("https://example.com/payments/result"); pesepay.setReturnUrl("https://example.com/payments/return"); ``` 3. ### Create and initiate a transaction [Section titled “Create and initiate a transaction”](#create-and-initiate-a-transaction) This is the [redirect flow](/payments/redirect-flow/): create a transaction for an amount and currency, then initiate it to get a `redirectUrl`. * Node.js ```js const transaction = pesepay.createTransaction( 10.0, 'USD', 'Order #1042 — running shoes' ); pesepay .initiateTransaction(transaction) .then((response) => { console.log(response.redirectUrl); // send the customer here console.log(response.referenceNumber); // store this for later }) .catch((error) => console.error(error)); ``` * Python ```python transaction = pesepay.create_transaction( 10.0, "USD", "Order #1042 — running shoes" ) try: response = pesepay.initiate_transaction(transaction) print(response.redirect_url) # send the customer here print(response.reference_number) # store this for later except Exception as err: print(err) ``` * PHP ```php $transaction = $pesepay->createTransaction( 10.0, 'USD', 'Order #1042 — running shoes' ); $response = $pesepay->initiateTransaction($transaction); if ($response->success()) { $redirectUrl = $response->redirectUrl(); // send the customer here $referenceNumber = $response->referenceNumber(); // store this for later } else { echo $response->message(); } ``` * Java ```java Transaction transaction = pesepay.createTransaction( 10.0, "USD", "Order #1042 — running shoes" ); Response response = pesepay.initiateTransaction(transaction); if (response.isSuccess()) { String redirectUrl = response.getRedirectUrl(); // send the customer here String referenceNumber = response.getReferenceNumber(); // store this for later } else { System.out.println(response.getMessage()); } ``` 4. ### Redirect your customer [Section titled “Redirect your customer”](#redirect-your-customer) Send the customer’s browser to `redirectUrl`. In the sandbox, use the [test payment methods](/testing/test-credentials/) to complete or deliberately fail the payment. 5. ### Confirm the result [Section titled “Confirm the result”](#confirm-the-result) Pesepay calls your `resultUrl` with the final status as soon as the customer finishes — see [The Result Callback](/webhooks/result-callback/). As a fallback, you can also poll: [Check Payment Status](/api/check-payment-status/) using the `referenceNumber` you stored in step 3. ## What’s next [Section titled “What’s next”](#whats-next) * [Add the payment methods you want to support](/payment-methods/overview/) * [Handle the result callback on your server](/webhooks/result-callback/) * [Move to production](/getting-started/go-live-checklist/)
# Checking payment status
> When to rely on the result callback versus polling for a transaction's status.
You have two ways to find out how a transaction ended up — use both. ## Push: the result callback [Section titled “Push: the result callback”](#push-the-result-callback) Pesepay posts the final status to the `resultUrl` you set when you created the transaction, as soon as the customer finishes (or the transaction times out). This is the primary way production integrations should reconcile orders. See [The Result Callback](/webhooks/result-callback/). ## Pull: check payment status [Section titled “Pull: check payment status”](#pull-check-payment-status) Call [Check Payment Status](/api/check-payment-status/) with a transaction’s `referenceNumber` any time you need the current state — useful for: * A safety net if a callback delivery is ever delayed or missed * Rendering a “checking your payment…” state on your `returnUrl` page * Reconciliation jobs that sweep pending transactions ## Reading the status [Section titled “Reading the status”](#reading-the-status) `transactionStatus` is one of a fixed set of values — see [Transaction Statuses](/resources/transaction-statuses/) for the full list and which ones are terminal (safe to stop checking) versus in-progress.
# How payments work
> The lifecycle of a Pesepay transaction, from initiation to settlement.
Every Pesepay payment — regardless of method — follows the same shape: Customer Your server Pesepay Payment provider 1 Create the transaction encrypted payload 2 referenceNumber store it now 3 Customer pays hosted page, or your UI 4 Processes the payment 5 Outcome 6 Result callback 7 Confirm the status 8 Settles to your account 1. Your server → Pesepay Create the transaction encrypted payload 2. Pesepay → Your server referenceNumber store it now 3. Customer → Pesepay Customer pays hosted page, or your UI 4. Pesepay → Payment provider Processes the payment 5. Payment provider → Pesepay Outcome 6. Pesepay → Your server Result callback 7. Your server → Pesepay Confirm the status 8. Pesepay Settles to your account The lifecycle of a Pesepay transaction 1. **Your server creates a transaction** with an amount, currency, and reason for payment, encrypts it, and sends it to Pesepay. 2. **The customer pays** through one of two flows: they’re redirected to a Pesepay-hosted page ([redirect flow](/payments/redirect-flow/)), or you collect their payment method details and submit them directly ([seamless flow](/payments/seamless-flow/)). 3. **Pesepay processes the payment** with the relevant provider (EcoCash, a card network, a bank, etc). 4. **Your server finds out the result** — either pushed to you via the [result callback](/webhooks/result-callback/), or pulled by [checking status](/payments/checking-status/) with the `referenceNumber`. 5. **Funds settle** to your payout account roughly T+2 days after the transaction completes. ## Reference number: your source of truth [Section titled “Reference number: your source of truth”](#reference-number-your-source-of-truth) Every transaction gets a `referenceNumber` the moment it’s created. Store it against your order/invoice immediately — it’s what ties together the initiation request, the result callback, and any status checks you make later. See the [transaction result fields](/webhooks/result-callback/#payload). ## Picking a flow [Section titled “Picking a flow”](#picking-a-flow) | | Redirect flow | Seamless flow | | ----------------------------------------- | --------------------- | ------------------------------------------ | | Where the customer enters payment details | Pesepay’s hosted page | Your own UI | | PCI/compliance surface on your servers | Minimal | Larger — you handle method-specific fields | | Best for | Fastest to ship | Fully branded checkout | [Redirect flow →](/payments/redirect-flow/) · [Seamless flow →](/payments/seamless-flow/) Either flow can settle part of each payment to a second merchant automatically — see [Split payments](/payments/split-payments/).
# Redirect (checkout) flow
> Send customers to a Pesepay-hosted payment page and get notified when they finish.
The redirect flow is the fastest way to accept payments: you create a transaction, redirect the customer to a page Pesepay hosts, and they pick a payment method and complete the payment there. Pesepay handles the method-specific UI for you — including [card entry](/payment-methods/card-payments/), which keeps card data out of your systems entirely. Customer Your server Pesepay Payment provider 1 Starts checkout 2 Create transaction POST /payments/initiate 3 redirectUrl + referenceNumber 4 Redirect to Pesepay 5 Picks a method and pays 6 Charges the customer 7 Result callback POST resultUrl 8 Confirm the status GET /payments/check-payment 1. Customer → Your server Starts checkout 2. Your server → Pesepay Create transaction POST /payments/initiate 3. Pesepay → Your server redirectUrl + referenceNumber 4. Your server → Customer Redirect to Pesepay 5. Customer → Pesepay Picks a method and pays 6. Pesepay → Payment provider Charges the customer 7. Pesepay → Your server Result callback POST resultUrl 8. Your server → Pesepay Confirm the status GET /payments/check-payment Redirect flow — the customer pays on Pesepay's hosted page ## Building it [Section titled “Building it”](#building-it) 1. **Build the request** — amount, currency, reason for payment, plus your `resultUrl` and `returnUrl`. 2. **Encrypt and send it** to [Initiate Transaction](/api/initiate-transaction/). 3. **Decrypt the response** to get a `redirectUrl` and `referenceNumber`. Store the `referenceNumber` against your order now. 4. **Redirect the customer’s browser** to `redirectUrl`. 5. **The customer chooses a method and pays** on Pesepay’s page. 6. **They land back on your `returnUrl`**, and separately, Pesepay posts the final status to your `resultUrl` — see [The Result Callback](/webhooks/result-callback/). 7. **Confirm before you fulfil** with [Check Payment Status](/api/check-payment-status/), and only act on `SUCCESS`. Caution `returnUrl` is where the *browser* goes — treat it as UI only. Never mark an order paid because the customer landed on `returnUrl`; only trust an explicit [status check](/payments/checking-status/). Steps 7 and 8 in the diagram are two halves of one thing: the callback tells you *something happened*, the status check tells you *what*. Full request/response fields, headers, and code samples in five languages: [Initiate Transaction reference →](/api/initiate-transaction/)
# Seamless (direct) flow
> Collect payment method details in your own UI and submit them directly to Pesepay.
In the seamless flow, your own UI collects the payment method and any fields it requires (like a phone number for EcoCash), and you submit everything directly — no redirect to a Pesepay-hosted page. Customer Your server Pesepay Wallet provider 1 Submits checkout method + phone number 2 Make payment POST /v2/payments/make-payment 3 Requests the debit 4 PIN prompt on the phone 5 referenceNumber + pollUrl status not yet final 6 Enters PIN 7 Result callback POST resultUrl 8 Confirm the status GET /payments/check-payment 1. Customer → Your server Submits checkout method + phone number 2. Your server → Pesepay Make payment POST /v2/payments/make-payment 3. Pesepay → Wallet provider Requests the debit 4. Wallet provider → Customer PIN prompt on the phone 5. Pesepay → Your server referenceNumber + pollUrl status not yet final 6. Customer → Wallet provider Enters PIN 7. Pesepay → Your server Result callback POST resultUrl 8. Your server → Pesepay Confirm the status GET /payments/check-payment Seamless flow — an EcoCash payment approved on the customer's phone The gap between step 5 and step 7 is the part that catches people out: your API call has already returned while the customer is still holding their phone. Show a waiting state, and drive it from the callback or `pollUrl`. ## Building it [Section titled “Building it”](#building-it) 1. **Pick a payment method** and check its [required fields](/payment-methods/overview/) — e.g. EcoCash needs `customerPhoneNumber`, InnBucks needs no extra fields at all. 2. **Build the request** — amount, currency, `paymentMethodCode`, the method’s required fields, plus `resultUrl` and `returnUrl`. 3. **Encrypt and send it** to [Make Payment](/api/make-payment/). 4. **Decrypt the response** and store the `referenceNumber`. The customer still has to approve the payment on their side, so the status you get back is usually not final. 5. **Get the result** via the [result callback](/webhooks/result-callback/) or by [checking status](/payments/checking-status/) with the `referenceNumber`. Caution A `200` from Make Payment does not mean you were paid. Only a terminal [`transactionStatus`](/resources/transaction-statuses/) of `SUCCESS` means that, and it arrives via the callback or a status check — never fulfil an order on the Make Payment response alone. Full request/response fields, headers, and code samples in five languages: [Make Payment reference →](/api/make-payment/)
# Split payments
> Settle part of every payment to a second Pesepay merchant automatically, and what turning it on changes about your integration.
A **split payment** lets one application collect a payment and have Pesepay settle part of it to a *different* Pesepay merchant, automatically, with no transfer between you afterwards. The application that collects is the **master merchant**; the merchant that receives a share is the **beneficiary**. Typical uses: a marketplace paying a vendor out of each sale, a booking platform paying a venue, a fees portal collecting on behalf of a school. Turning splits on changes the contract for every transaction From the moment your first beneficiary accepts, **every** transaction on that application must carry the beneficiary’s email in `paymentMetadata` — including ordinary sales that have nothing to do with the arrangement. Transactions without it are refused at initiation: > Transaction cannot be initiated without Beneficiary merchant email in paymentMetadata because this application has settlement split beneficiaries. A merchant who adds their first beneficiary will break their existing checkout on the next payment unless the code is deployed first. Plan the two together, or set the arrangement up on a **separate application**. ## How the money moves [Section titled “How the money moves”](#how-the-money-moves) Customer Your server Pesepay Beneficiary 1 Create the transaction beneficiary email in paymentMetadata 2 Pays once one reference number 3 Divides the amount on success 4 Result callback no split detail on it 5 Credits the master merchant share 6 Credits the remainder their application, their payout account 1. Your server → Pesepay Create the transaction beneficiary email in paymentMetadata 2. Customer → Pesepay Pays once one reference number 3. Pesepay Divides the amount on success 4. Pesepay → Your server Result callback no split detail on it 5. Pesepay → Your server Credits the master merchant share 6. Pesepay → Beneficiary Credits the remainder their application, their payout account One payment, two settlement credits 1. **The customer pays once.** One transaction, one reference number, one [result callback](/webhooks/result-callback/) — the customer never sees the arrangement. 2. **On success, Pesepay creates two settlement records** against the parent transaction: a credit to the master merchant for their share, and a credit to the beneficiary for the rest. Both are booked in the currency of the parent payment. 3. **Each side is paid out on its own.** The beneficiary’s credit lands against *their* Pesepay application and is settled to *their* payout account under the normal settlement schedule. You never hold or forward their money. The split arithmetic runs on the transaction amount. Pesepay’s own transaction service fee is applied as usual for your application’s fee charge type — it is not part of the calculation below. ## Before you start [Section titled “Before you start”](#before-you-start) Both sides need to be approved Pesepay merchants before an arrangement can exist: | Requirement | Applies to | Why | | -------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | An approved Pesepay merchant account | Beneficiary | The invitation is matched to an existing business by email. An unapproved account is rejected with *Beneficiary merchant account has not yet been approved.* | | An approved [payout account](/getting-started/onboarding/#3-payout-accounts) **in the currency you charge in** | Beneficiary | Credits inherit the parent transaction’s currency and are **never converted**. A `ZWG` payment to a beneficiary who only holds a USD payout account produces a balance they cannot withdraw | | The email address the beneficiary’s business is registered under | Master merchant | It is both the invitation address and the value you send on every transaction | Caution The currency point is an onboarding prerequisite, not a runtime check. Nothing fails at payment time — the money simply sits in a currency the beneficiary has no approved account for. Confirm their payout accounts cover every currency you will charge in **before** you send the invitation. ## Setting up an arrangement [Section titled “Setting up an arrangement”](#setting-up-an-arrangement) Arrangements are configured per application, in the Merchant Control Panel under **Applications → View Details → Manage Splits**. 1. **The master merchant configures the split.** **Configure Split** opens a form asking for the beneficiary merchant email, effective date, split amount mode, allocation model, and the fixed amount or percentage — the [settings below](#configuration-reference). Submitting it sends the invitation. 2. **The beneficiary gets an emailed invitation** headed *“\ split settlement invitation”*, showing the agreement details and a link to review and accept it. 3. **The beneficiary accepts.** Pesepay creates a dedicated application on their account — named ` - Beneficiary` — and their share of each payment is credited to it. They add a payout account to that application if they have not already. 4. **The arrangement becomes active.** Deploy the `paymentMetadata` change described [below](#what-changes-in-your-requests) at this point, not after. An application can hold several arrangements — one per beneficiary you collect for. Each transaction names exactly one of them. ### Agreement status [Section titled “Agreement status”](#agreement-status) **Manage Splits** lists every arrangement on the application with its beneficiary, allocation model, split amount mode, share, effective date and status; expanding a row also shows the agreement reference and the beneficiary’s settlement account. | Status | Meaning | Payments | | ---------------------- | --------------------------------------------------------------- | ---------------------------------------------------- | | `INVITED` | Sent, not yet answered | Not yet required, and not yet accepted | | `ENABLED` / `ACCEPTED` | Live | Accepted for this beneficiary | | `DECLINED` | The beneficiary refused the invitation | Never active | | `DISABLED` | Suspended because the beneficiary’s application was deactivated | Refused, and the metadata requirement stays in force | | `REVOKED` | Ended by the master merchant | Refused | ## Configuration reference [Section titled “Configuration reference”](#configuration-reference) | Setting | Values | Notes | | ----------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `allocationModel` | `PERCENTAGE`, `FIXED_AMOUNT` | Required | | `percentage` | Greater than 0, less than 100 | Required for `PERCENTAGE`. **This is the master merchant’s share, not the beneficiary’s** | | `fixedAmount` | Greater than 0 | Required for `FIXED_AMOUNT`. Also the master merchant’s share | | `splitAmountMode` | `PRINCIPAL` (default), `ADD_ON` | Set on the arrangement, not per transaction — see [below](#principal-vs-add_on) | | `effectiveDate` | A date; defaults to the day the invitation is created | Shown on the invitation | percentage is the master merchant's share The field sits on a record called *split beneficiary*, which reads as though it describes what the beneficiary gets. It does not. `percentage: 30` on a 100.00 payment means the **master merchant keeps 30.00** and the beneficiary receives 70.00. Getting this backwards is the easiest misconfiguration to make — the invitation email calls it *Master Merchant Share* for exactly this reason. ### PRINCIPAL vs ADD\_ON [Section titled “PRINCIPAL vs ADD\_ON”](#principal-vs-add_on) `PRINCIPAL` takes the master merchant’s share **out of** the amount you submit. `ADD_ON` **adds it on top**, so the customer is charged more than the amount you sent. Submitting `amount: 100.00` against a 30% arrangement: | | `PRINCIPAL` | `ADD_ON` | | ---------------------------------- | ----------- | ---------- | | Amount you submit | 100.00 | 100.00 | | **Amount the customer is charged** | 100.00 | **130.00** | | Master merchant receives | 30.00 | 30.00 | | Beneficiary receives | 70.00 | 100.00 | The same payment with a fixed share of 5.00: | | `PRINCIPAL` | `ADD_ON` | | ---------------------------------- | ----------- | ---------- | | Amount you submit | 100.00 | 100.00 | | **Amount the customer is charged** | 100.00 | **105.00** | | Master merchant receives | 5.00 | 5.00 | | Beneficiary receives | 95.00 | 100.00 | ADD\_ON rewrites the amount before the payment is created Under `ADD_ON`, the amount on the transaction — and so on the hosted checkout, the callback and every report — is the amount you submitted **plus** the master merchant’s share. If your order totals, receipts or reconciliation are built against your own figure, they will disagree with Pesepay. Read the amount back from the transaction rather than assuming it is the one you sent. ## What changes in your requests [Section titled “What changes in your requests”](#what-changes-in-your-requests) Add the beneficiary’s email to `paymentMetadata` on every [Initiate Transaction](/api/initiate-transaction/) or [Make Payment](/api/make-payment/) request for the application: Plaintext request body (before encryption) ```diff { "amountDetails": { "amount": 100.00, "currencyCode": "USD" }, "reasonForPayment": "Order #1042 — running shoes", +"paymentMetadata": { +"beneficiaryMerchantEmail": "vendor@example.com" + }, "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return" } ``` `beneficiaryMerchantEmail` is the canonical key. These aliases are also accepted, case-insensitively, for integrations that already use them: `beneficiaryEmail`, `beneficiary_email`, `schoolEmail`, `school_email`. Prefer the canonical key in new code. ### Metadata Pesepay writes back [Section titled “Metadata Pesepay writes back”](#metadata-pesepay-writes-back) Pesepay adds two keys to the transaction’s metadata, and you get them back as `transactionMetadata` on the [callback](/webhooks/result-callback/) and from [check payment status](/api/check-payment-status/): | Key | Value | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `splitAmountMode` | `PRINCIPAL` or `ADD_ON`, taken from the arrangement | | `splitPrincipalAmount` | The amount before the master merchant’s share was added. Under `PRINCIPAL` this equals the amount charged; under `ADD_ON` it is the amount you submitted | Under `ADD_ON`, `splitPrincipalAmount` is the figure to reconcile your own order total against — `amountDetails.amount` is the larger, charged amount. ## Errors [Section titled “Errors”](#errors) Split payments add three failures, all raised when you create the transaction and all `400` responses in the standard [error shape](/api/errors/). The `message` text is reproduced exactly. ### No beneficiary email on the request [Section titled “No beneficiary email on the request”](#no-beneficiary-email-on-the-request) ```text Transaction cannot be initiated without Beneficiary merchant email in paymentMetadata because this application has settlement split beneficiaries. ``` The application has at least one beneficiary in `ENABLED`, `ACCEPTED` or `DISABLED`, and this request carried no beneficiary email. **Fix:** send `beneficiaryMerchantEmail` on *every* transaction for this application, not only the ones you think of as split. ### The email isn’t an accepted beneficiary [Section titled “The email isn’t an accepted beneficiary”](#the-email-isnt-an-accepted-beneficiary) ```text Transaction cannot be initiated because the supplied split beneficiary merchant email is not accepted. ``` There is no accepted arrangement on this application for that address — it was never invited, is still `INVITED`, was `DECLINED`, or has been `REVOKED`. **Fix:** check the spelling, and check the arrangement’s status in **Manage Splits**. This is also what you get in the gap while an arrangement is being replaced. ### The beneficiary is disabled [Section titled “The beneficiary is disabled”](#the-beneficiary-is-disabled) ```text PAYMENTS TO THE MERCHANT ARE CURRENTLY DISABLED, PLEASE CONTACT ADMINISTRATOR, OR TRY AGAIN LATER ``` The arrangement is `DISABLED` because the beneficiary’s own application was deactivated. **Fix:** nothing in your code — the beneficiary’s account needs attention. Note this message is customer-facing in tone but arrives as an API error; don’t show it verbatim at checkout. One failure happens after the customer has paid Allocation runs when the payment succeeds. If it cannot produce a valid division — most often because a **fixed master-merchant share is equal to or larger than the payment amount** — the payment still succeeds and the customer is still charged, but the split is not allocated and the transaction is flagged internally as a failed settlement split. Keep fixed shares comfortably below your smallest expected payment, and treat a beneficiary reporting a missing credit on an otherwise successful payment as a support case, with the `referenceNumber`. ## Current limits [Section titled “Current limits”](#current-limits) * **One beneficiary per transaction.** A payment divides between the master merchant and exactly one beneficiary. There is no n-way split, and no way to name more than one beneficiary on a request. * **One current arrangement per beneficiary, per application.** Inviting a business that already has one is rejected — revoke the existing arrangement first. * **No currency conversion.** Both credits inherit the parent transaction’s currency. * **Nothing about the split appears on the callback.** The [result callback](/webhooks/result-callback/) describes the customer’s payment. The two credits are settlement records, visible in each merchant’s own dashboard. ## Changing or ending an arrangement [Section titled “Changing or ending an arrangement”](#changing-or-ending-an-arrangement) Terms are fixed once accepted. **To change a share, mode or model, revoke the arrangement and send a fresh invitation**, which the beneficiary must accept again. Revoking is the ⋮ action on the arrangement’s row in **Manage Splits**; there is no edit, and no un-revoke. Schedule this — do not do it mid-trade Between the revocation and the new acceptance there is a gap in which transactions carrying that beneficiary’s email are refused with *the supplied split beneficiary merchant email is not accepted*. Revoking is also not reversible: a revoked arrangement cannot be switched back on, only replaced by a new invitation. Do it in a maintenance window, or outside trading hours. Revoking your *only* beneficiary also lifts the metadata requirement, so transactions that omit the email start succeeding again while any request still sending the revoked address keeps failing. During a change-over, old and new code therefore fail in different ways — deploy the request change and the new acceptance close together.
# API keys & credentials
> The two keys every Pesepay application has, and how to keep them safe.
Each application in your Pesepay account (see [Onboarding](/getting-started/onboarding/)) has two keys: | Key | Used for | | ------------------- | ------------------------------------------------------------------------------------------------ | | **Integration key** | Sent as the `authorization` header on every API request | | **Encryption key** | Encrypting requests and decrypting responses — see the [Encryption Guide](/security/encryption/) | ## Sandbox vs live keys [Section titled “Sandbox vs live keys”](#sandbox-vs-live-keys) Sandbox and production are **separate accounts with separate applications**, so you hold two pairs of keys. A pair only works against the environment it was issued for: | Keys from | Base URL they work against | | --------- | ---------------------------------------------------------- | | Sandbox | `https://api.test.sandbox.pesepay.com/payments-engine/...` | | Live | `https://api.pesepay.com/api/payments-engine/...` | Caution **Crossing the two is the most common setup failure, and it doesn’t announce itself clearly.** A sandbox integration key sent to production fails authentication — `403`, *Integration key for the application is not valid* — and a mismatched encryption key fails at the cipher instead, giving you a `500` with *Failed to decrypt your data* or a payload that decrypts into nothing usable. Neither message says “wrong environment”. Before debugging the [encryption scheme](/security/encryption/), confirm that the key and the URL come from the same environment. Label them unmistakably in your configuration — `PESEPAY_SANDBOX_*` and `PESEPAY_LIVE_*` rather than one `PESEPAY_KEY` that changes meaning per deploy — and never let a sandbox key reach a production build, or the reverse. ## Keep requests server-side [Section titled “Keep requests server-side”](#keep-requests-server-side) Danger Never call the Pesepay API directly from frontend/browser code. Doing so exposes your integration key and encryption key to anyone who opens your browser’s network tab. All requests to Pesepay must originate from your server, where the keys stay out of reach. ## If a key is compromised [Section titled “If a key is compromised”](#if-a-key-is-compromised) Deactivate the affected key from the Merchant Control Panel and issue a new one immediately, then update it everywhere it’s used. There’s no grace period — treat any suspected exposure as urgent. ## Rotating keys [Section titled “Rotating keys”](#rotating-keys) Rotating a key changes what your live servers need to encrypt/decrypt with — plan rotations as a deploy, not a dashboard-only change: 1. Generate the new key in the Merchant Control Panel. 2. Deploy your servers with the new key. 3. Confirm new transactions encrypt/decrypt correctly. 4. Deactivate the old key.
# Best practices
> Security recommendations for integrating with the Pesepay API.
* **Always use HTTPS.** Every Pesepay API request must be made over HTTPS — requests are rejected otherwise. * **Keep keys server-side.** See [API Keys & Credentials](/security/api-keys/) — your integration key and encryption key should never reach a browser or mobile app bundle. No traditional backend? Put them in a [serverless function](/sdks/serverless/) instead. * **Verify, don’t assume.** Treat a customer landing on `returnUrl` as a UI event only. Confirm payment success via the [result callback](/webhooks/result-callback/) or a server-to-server [status check](/api/check-payment-status/) before fulfilling an order. * **Treat your `resultUrl` as hostile input.** It’s a public endpoint and callbacks carry no signature, so cross-check the `referenceNumber` against an order you actually created, and confirm the outcome server-to-server — see [Verifying Callbacks](/webhooks/verifying-callbacks/). * **Rotate compromised keys immediately.** Don’t wait for a scheduled rotation if you suspect exposure. * **Log encrypted payloads, not decrypted ones**, if you log requests/responses for debugging — decrypted transaction and customer data shouldn’t sit in plaintext logs any longer than necessary.
# Encryption guide
> How to encrypt requests and decrypt responses for Pesepay's integration endpoints, in every supported language.
Every integration endpoint ([Initiate Transaction](/api/initiate-transaction/), [Make Payment](/api/make-payment/), [Check Payment Status](/api/check-payment-status/)) sends and receives data as an **encrypted payload**, not plain JSON. This is the one page every endpoint page links back to — read it once. ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Value | | -------------------------- | ----------------------------------------------------------------------------------------------- | | Algorithm | AES-256-CBC | | Key | Your application’s 32-character encryption key, from [Onboarding](/getting-started/onboarding/) | | IV (Initialization Vector) | The **first 16 characters** of your encryption key | | Key size | 256 | | Encoding | UTF-8 | | Padding | PKCS5/PKCS7 | Danger If a key is ever compromised, deactivate it and issue a new one from the Merchant Control Panel immediately — see [API Keys & Credentials](/security/api-keys/). ## Try it [Section titled “Try it”](#try-it) Paste your key and a request body below to see the exact payload the API expects — or paste a response payload to read it back. This is the fastest way to check that your own `encrypt` implementation produces the same bytes: encrypt the same JSON here, and the two strings should match character for character. ## Encrypting a request [Section titled “Encrypting a request”](#encrypting-a-request) 1. Build your request body as JSON. 2. Encrypt the JSON string with AES-256-CBC, using your encryption key and the first 16 characters of that key as the IV. 3. Base64-encode the encrypted bytes. 4. Send it as `{"payload": "encrypted_base64_string"}`. * TypeScript/Angular ```typescript import * as CryptoJS from 'crypto-js'; function encrypt(data: object, encryptionKey: string): string { const key = CryptoJS.enc.Utf8.parse(encryptionKey); const iv = CryptoJS.enc.Utf8.parse(encryptionKey.substring(0, 16)); const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); } ``` * JavaScript (Node.js) ```javascript const crypto = require('crypto'); function encrypt(data, encryptionKey) { const iv = Buffer.from(encryptionKey.substring(0, 16), 'utf8'); const key = Buffer.from(encryptionKey, 'utf8'); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64'); encrypted += cipher.final('base64'); return encrypted; } ``` * Python ```python import json import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt(data: dict, encryption_key: str) -> str: key = encryption_key.encode('utf-8') iv = encryption_key[:16].encode('utf-8') cipher = AES.new(key, AES.MODE_CBC, iv) padded = pad(json.dumps(data).encode('utf-8'), AES.block_size) encrypted = cipher.encrypt(padded) return base64.b64encode(encrypted).decode('utf-8') ``` * Java ```java import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public String encrypt(String jsonData, String encryptionKey) throws Exception { SecretKeySpec key = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES"); IvParameterSpec iv = new IvParameterSpec( encryptionKey.substring(0, 16).getBytes("UTF-8") ); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key, iv); byte[] encrypted = cipher.doFinal(jsonData.getBytes("UTF-8")); return Base64.getEncoder().encodeToString(encrypted); } ``` * PHP ```php dict: key = encryption_key.encode('utf-8') iv = encryption_key[:16].encode('utf-8') cipher = AES.new(key, AES.MODE_CBC, iv) decrypted = unpad( cipher.decrypt(base64.b64decode(payload)), AES.block_size ) return json.loads(decrypted.decode('utf-8')) ``` * Java ```java import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public String decrypt(String payload, String encryptionKey) throws Exception { SecretKeySpec key = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES"); IvParameterSpec iv = new IvParameterSpec( encryptionKey.substring(0, 16).getBytes("UTF-8") ); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, key, iv); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(payload)); return new String(decrypted, "UTF-8"); } ``` * PHP ```php