Search by

contipay / php-sdk

iamnigelzw

The Contipay PHP Client provides a simple and secure way to integrate Contipay payments into any PHP application

Package info

github.com/iamnigelzw/php-sdk

pkg:composer/contipay/php-sdk

Statistics

Installs: 262

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.1 2026-10-16 08:34 UTC

This package is auto-updated.

Last update: 2026-10-16 08:51:48 UTC


README

Accept mobile money and card payments, and send payouts, through ContiPay from any PHP application.

Latest Version License Why PHP

$response = $mobile->ecocash([
    'amount'    => '10.00',
    'currency'  => 'USD',
    'phone'     => '0782000340',
    'reference' => 'ORDER-1001',
]);

Contents

Installation

Requires PHP 8.1+ with the json and openssl extensions, and a ContiPay merchant account (API key, API secret and merchant ID).

composer require contipay/php-sdk

On PHP 7.4 or 8.0, use the 1.x line: composer require contipay/php-sdk:^1.2.

Quick start

Charge an EcoCash wallet in the test environment, then check whether it was paid.

<?php

require __DIR__ . '/vendor/autoload.php';

use ContiPay\PhpSdk\Mobile;

$mobile = (new Mobile(getenv('CONTIPAY_API_KEY'), getenv('CONTIPAY_API_SECRET'), 'dev'))
    ->setMerchantId((int) getenv('CONTIPAY_MERCHANT_ID'))
    ->setWebhookUrl('https://your-site.com/contipay/webhook');

$reference = 'ORDER-1001'; // your own unique reference, saved against the order

$response = json_decode($mobile->ecocash([
    'amount'    => '10.00',
    'currency'  => 'USD',
    'phone'     => '0782000340',
    'reference' => $reference,
]), true);

// The customer confirms on their phone. ContiPay then notifies your webhook,
// or you can ask for the status yourself:
$status = json_decode($mobile->getTransactionStatus($reference), true);

That is the whole flow: create a payment with your reference, then confirm it by webhook or status lookup before you fulfil the order.

Key concepts

Environments

Mode API Use it for
dev https://api-uat.contipay.net Building and testing (default)
live https://api.contipay.net Real money

Pass the mode as the third constructor argument. It is case-insensitive. To point at different hosts, call updateContipayURL($devUrl, $liveUrl).

Direct vs redirect

Mobile and Card take a fourth constructor argument, the payment flow:

  • direct (default): you collect the details and ContiPay charges the wallet or card straight away.
  • redirect: ContiPay returns a payment page URL (redirectUrl) to send the customer to. Set setSuccessUrl() and setErrorUrl() for where they come back to.

Checkout always uses redirect, and Disbursement always uses direct.

References

Every transaction has a reference: your own ID for it, sent to ContiPay as merchantRef. It is how you look up status and how you match webhooks to orders, so:

  • make it unique per transaction (e.g. your order ID plus an attempt number),
  • save it before you call ContiPay, so you can recover if the response never arrives.

Where a reference is optional and you leave it out, the SDK generates one like V-4829104738291045.

Amounts

Pass amounts as strings or numbers in major units: '10.50' means ten dollars fifty. They must be positive with at most 2 decimal places. '10', '10.5', 10.5 and 10 are all fine. '-5', '0', '1e3' and '1.005' are rejected before anything is sent.

Responses

Payment, payout and status methods return ContiPay's response as a JSON string. Decode it with json_decode($response, true). If the request could not complete, you get an SDK-built JSON error instead (see Handling errors). Checkout is the exception: it returns a CheckoutSession object.

Accept payments

Hosted checkout

Recommended for most integrations. The customer pays on ContiPay's page and picks their own method, so card numbers and wallet PINs never touch your server.

use ContiPay\PhpSdk\Checkout;

$checkout = (new Checkout($apiKey, $apiSecret, 'live'))
    ->setMerchantId($merchantId)
    ->setWebhookUrl('https://your-site.com/contipay/webhook')
    ->setSuccessUrl('https://your-site.com/orders/1001/thanks')
    ->setCancelUrl('https://your-site.com/orders/1001');

$session = $checkout->create([
    'amount'      => '50.00',
    'currency'    => 'USD',
    'phone'       => '0782000340',
    'reference'   => 'ORDER-1001',
    'description' => 'Order #1001',
]);

if ($session->successful()) {
    header('Location: ' . $session->url());
    exit;
}

error_log('ContiPay checkout failed: ' . $session->message());
Field Required Notes
amount Yes See Amounts
currency Yes e.g. USD, ZWG
phone Yes Customer phone number
reference No Generated if left out
description No Shown on the payment page
firstName, lastName No Defaults to the phone number
email No
country No Defaults to ZW
cod, coc No Cash on delivery / collection. Defaults to false

create() returns a CheckoutSession:

Method Returns
successful() Whether ContiPay returned a payment page
url() The payment page URL, or null
reference() The reference to save against the order
message() ContiPay's message, or the SDK error
toArray(), toJson() The full response

The success URL is not proof of payment. Anyone can visit it. Confirm by webhook or status lookup. A status lookup made straight after create() may return "not found" until the customer pays.

Create the session on your server. A "Pay" button in the browser should link to your own endpoint, which calls create() and redirects. It must never call ContiPay directly, because that would expose your API secret.

Mobile money

use ContiPay\PhpSdk\Mobile;

$mobile = (new Mobile($apiKey, $apiSecret, 'live')) // 4th argument: 'direct' (default) or 'redirect'
    ->setMerchantId($merchantId)
    ->setWebhookUrl('https://your-site.com/contipay/webhook');

$response = $mobile->ecocash([
    'amount'      => '50.00',
    'currency'    => 'USD',
    'phone'       => '0782000340',
    'reference'   => 'ORDER-1001',
    'description' => 'Order #1001', // optional
]);
Method Provider Code Payouts
ecocash([]) EcoCash EC ✅
onemoney([]) OneMoney OM ✅
omari([]) Omari OC ✅
innbucks([]) InnBucks IB ✅
telecash([]) TeleCash TC —
mtn([]) MTN MN ✅
airtel([]) AirTel AT ✅
zamtel([]) ZamTel ZT ✅
equitel([]) EquiTel ET —
mpesa([]) MPesa MP ✅
orange([]) Orange OG —
tigo([]) Tigo TG —
mobile([]) Any. Pass provider and code — ✅

Required for every provider: amount, currency, phone, reference. Optional: description and countryCode (defaults to ZW — set it for providers outside Zimbabwe, e.g. 'KE' for M-Pesa).

mobile() also needs provider (e.g. 'EcoCash') and code (e.g. 'EC'), which lets you route to a provider chosen at runtime:

$response = $mobile->mobile([
    'amount'    => '50.00',
    'currency'  => 'USD',
    'phone'     => '0782000340',
    'reference' => 'ORDER-1001',
    'provider'  => 'OneMoney',
    'code'      => 'OM',
]);

For the redirect flow, construct with 'redirect' and set setSuccessUrl() and setErrorUrl().

Vouchers

Vouchers behave exactly like Mobile money — same fields, same direct and redirect flows — but route to the voucher provider (VC).

use ContiPay\PhpSdk\Voucher;

$voucher = (new Voucher($apiKey, $apiSecret, 'dev'))
    ->setMerchantId(116)
    ->setWebhookUrl('https://shop.test/webhook');

$response = $voucher->voucher([
    'amount'      => '50.00',
    'currency'    => 'USD',
    'phone'       => '0782000340',
    'reference'   => 'ORDER-1001',
    'description' => 'Order #1001', // optional
]);
Method Provider Code Currencies Payouts
voucher([]) Voucher VC ZAR, USD, ZWG, ZMW ✅

Vouchers only settle in the four currencies above; anything else throws an InvalidArgumentException before the request is sent. Mobile providers aren't callable on Voucher (and vice versa) — each class exposes only its own provider type, so $voucher->ecocash() throws a BadMethodCallException.

Payouts to vouchers go through Disbursement like any other provider.

Cards

PCI DSS: with the direct flow, card numbers and CVVs pass through your server, which puts you in full PCI DSS scope. Unless you are PCI compliant, use Hosted checkout or Card with the redirect flow.

use ContiPay\PhpSdk\Card;

$card = (new Card($apiKey, $apiSecret, 'live', 'redirect'))
    ->setMerchantId($merchantId)
    ->setWebhookUrl('https://your-site.com/contipay/webhook')
    ->setSuccessUrl('https://your-site.com/orders/1001/thanks')
    ->setErrorUrl('https://your-site.com/orders/1001');

$response = $card->visa([
    'accountNumber' => '4111111111111111',
    'accountExpiry' => '12/26',
    'cvv'           => '123',
    'phone'         => '0782000340',
    'amount'        => '50.00',
    'currency'      => 'USD',
    'reference'     => 'ORDER-1001',
    // optional
    'firstName'     => 'Tariro',
    'lastName'      => 'Moyo',
    'email'         => 'tariro@example.com',
    'country'       => 'ZW',
    'description'   => 'Order #1001',
]);
Method Card Code Required fields
visa([]) Visa VA amount, currency, phone, accountNumber, accountExpiry, cvv, reference
mastercard([]) MasterCard MA amount, currency, phone, accountNumber, accountExpiry, cvv
zimswitch([]) ZimSwitch ZS amount, currency, phone, reference
card([]) Any — As mastercard, plus provider and code

Where reference is not required, one is generated if you leave it out. Passing your own is still strongly recommended.

Send payouts

Disbursement sends money from your merchant account to a mobile wallet. Each payout is signed with your merchant RSA private key, which ContiPay verifies.

use ContiPay\PhpSdk\Disbursement;

$payouts = (new Disbursement(
    $apiKey,
    $apiSecret,
    '/secure/path/contipay-private.pem', // a path, or the PEM string itself
    'live'
))
    ->setMerchantId($merchantId)
    ->setWebhookUrl('https://your-site.com/contipay/webhook');

$reference = 'PAYOUT-2026-0042'; // save this before sending

$response = $payouts->ecocash([
    'amount'      => '20.00',
    'currency'    => 'USD',
    'phone'       => '0782000340',
    'reference'   => $reference,
    // optional
    'accountName' => 'Tariro Moyo',
    'firstName'   => 'Tariro',
    'lastName'    => 'Moyo',
    'email'       => 'tariro@example.com',
    'description' => 'Commission payout',
]);

Supports every provider marked ✅ in the Payouts column of Mobile money — ecocash(), onemoney(), omari(), innbucks(), mtn(), airtel(), zamtel(), mpesa() and mobile() — plus voucher(), using the same provider codes. amount, currency, phone and reference are always required; countryCode is optional and defaults to ZW.

TeleCash, EquiTel, Orange and Tigo are acquire-only. Calling them here throws a BadMethodCallException rather than sending a payout the provider cannot fulfil. Voucher payouts are limited to the voucher currencies.

Use $payouts->providers() to list the payout providers available at runtime ($mobile->providers() and $voucher->providers() do the same for collections).

An invalid key throws an InvalidArgumentException when you construct the object, so a bad deployment fails at startup instead of at payout time.

Never blindly retry a payout. If the response has "outcome": "unknown", the money may already have been sent. Check getDisbursementStatus($reference) first. See Handling errors.

Check a transaction's status

Every class (Mobile, Voucher, Card, Checkout, Disbursement) can look up a transaction by your reference. Call setMerchantId() first, because ContiPay requires it for lookups.

$response = $mobile->getTransactionStatus('ORDER-1001');                // payments
$response = $payouts->getDisbursementStatus('PAYOUT-2026-0042');       // payouts
$response = $payouts->getDisbursementStatus('PAYOUT-2026-0042', 'EC'); // narrowed to a provider

Read the result with the same Webhook class you use for notifications:

use ContiPay\PhpSdk\Webhook;

$status = new Webhook(json_decode($response, true));

if ($status->isPaid()) {
    // fulfil the order
}

The lookups call GET /acquire/payment and GET /disburse/payment with merchantRef and merchantId (plus providerCode when given).

Webhooks

ContiPay POSTs a JSON notification to your webhook URL whenever a transaction changes status.

use ContiPay\PhpSdk\Webhook;

$webhook = Webhook::fromRequest(); // or Webhook::fromJson($rawBody)

$order = findOrderByReference($webhook->reference());

if ($order === null || $order->isSettled()) {
    http_response_code(200); // unknown or already handled: acknowledge and stop
    exit;
}

if ($webhook->isPaid()) {
    // Notifications are not signed, so confirm with ContiPay before fulfilling.
    $confirmed = new Webhook(json_decode($mobile->getTransactionStatus($order->reference), true));

    if ($confirmed->isPaid() && round($confirmed->amount(), 2) === round($order->amount, 2)) {
        $order->markPaid();
    }
} elseif ($webhook->isFailed()) {
    $order->markFailed();
}
// isPending(): another notification will follow

http_response_code(200);
Check True when the status is
isPaid() Paid or confirmed
isPending() Pending, queued, approved or submitted
isRefunded() Refunded
isFailed() Error, declined, or anything unrecognised

An unrecognised status always counts as failed, never as paid. statusCode() gives the raw Webhook::STATUS_* value.

Fields: reference(), contiPayReference(), providerReference() (e.g. the EcoCash transaction ID), merchantId(), amount(), currency(), providerCode(), providerName(), methodCode(), message(), plus get($key) and toArray() for anything else.

Webhook rules of thumb

  • Confirm before fulfilling. Notifications are not signed, so anyone who finds your URL can post one.
  • Be idempotent. ContiPay may send the same notification more than once.
  • Respond quickly with 200. Do slow work (emails, fulfilment) in a queue.

Handling errors

Errors come in two kinds.

1. Exceptions: a mistake in your code. These are thrown before anything is sent to ContiPay, so fix the code rather than catching them.

Exception When
InvalidArgumentException A required field is missing, the amount is invalid, the currency isn't supported by the provider, or the payout private key can't be read
LogicException A request was made before setMerchantId()
BadMethodCallException The class has no such provider (e.g. $mobile->paypal()), or the provider doesn't support that operation (e.g. $payouts->tigo())

2. JSON errors: the request did not succeed. API rejections come back as ContiPay's own JSON. Network and server failures come back as SDK-built JSON:

{ "status": "error", "message": "cURL error 28: Operation timed out", "outcome": "unknown" }

outcome tells you whether it is safe to try again:

outcome Meaning What to do
"failed" ContiPay never received the request (e.g. connection refused, DNS failure) or rejected it Safe to retry
"unknown" It may have been processed (e.g. timed out after sending, 5xx from a gateway) Check status first
$result = json_decode($response, true);

if (($result['outcome'] ?? null) === 'unknown') {
    $status = new Webhook(json_decode($mobile->getTransactionStatus($reference), true));
    // isPaid(): treat as success. isPending(): wait for the webhook.
    // Still not found after a short wait: retry.
}

The SDK waits 10 seconds to connect and 60 seconds for a response.

Going live checklist

  • Live API key, secret and merchant ID, loaded from environment variables or a secrets manager rather than committed to code
  • Mode set to 'live'
  • Webhook URL is public, served over HTTPS, and returns 200 quickly
  • Every transaction has a unique reference, saved before calling ContiPay
  • Orders are fulfilled only after a webhook confirmed with a status lookup, never on the success URL alone
  • Responses with "outcome": "unknown" trigger a status check, not a blind retry
  • Webhook handler is idempotent (ignores already-settled orders)
  • Payout private key is stored outside the web root, with restricted file permissions
  • Cards go through hosted checkout or the redirect flow, unless you are PCI DSS compliant

Reference

Constructors

Class Signature
Mobile new Mobile($apiKey, $apiSecret, $mode = 'dev', $method = 'direct')
Voucher new Voucher($apiKey, $apiSecret, $mode = 'dev', $method = 'direct')
Card new Card($apiKey, $apiSecret, $mode = 'dev', $method = 'direct')
Checkout new Checkout($apiKey, $apiSecret, $mode = 'dev')
Disbursement new Disbursement($apiKey, $apiSecret, $privateKey, $mode = 'dev')
Webhook Webhook::fromRequest(), Webhook::fromJson($json), new Webhook($array)

Setters (chainable)

Method Available on Purpose
setMerchantId(int) all Your ContiPay merchant ID (required)
setWebhookUrl(string) all Where ContiPay sends notifications
setSuccessUrl(string) Mobile, Voucher, Card, Checkout Return URL after paying (redirect flows)
setErrorUrl(string) Mobile, Voucher, Card Return URL after a failure or cancel
setCancelUrl(string) Checkout Return URL after cancelling
updateContipayURL($dev, $live) all Override the API hosts

Each setter has a matching getter, e.g. getMerchantId().

Security

  • Keep your API secret and private key on the server. Never ship them to a browser or mobile app.
  • All traffic uses HTTPS with certificate verification on. Don't point updateContipayURL() at a plain http:// host.
  • The SDK does not log requests or responses. If you log them yourself, strip card numbers, CVVs and credentials first.
  • Report security issues privately to info@contipay.co.zw, not in a public issue.

Contributing and tests

composer install
composer test

The test suite uses a mocked HTTP client and never contacts ContiPay.

Support

ContiPay: contipay.co.zw · info@contipay.co.zw

License

MIT. See LICENSE.md.