TeePayDocs
SDKs

PHP SDK

Accept payments, create hosted checkouts, send payouts and verify webhooks from PHP 8.1+.

Package
teepay/teepay-php
Install
composer require teepay/teepay-php
Runtime
PHP 8.1 or newer with ext-curl and ext-json, no other dependencies
Autoload
PSR-4, namespace TeePay\

Install

composer require teepay/teepay-php

Create a client

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

$teepay = new \TeePay\TeePay(getenv('TEEPAY_API_KEY')); // or new \TeePay\TeePay() to read TEEPAY_API_KEY

echo $teepay->environment; // "sandbox" or "live"
$caps = $teepay->capabilities();
var_dump($caps->features->collection_create, $caps['supported_countries']);

Pass an array for options:

$teepay = new \TeePay\TeePay([
    'api_key' => getenv('TEEPAY_API_KEY'),
    'timeout' => 30,       // seconds per attempt
    'max_retries' => 2,
    // 'api_version' => '2026-10-18', // pins the TeePay-Version header
]);

Mobile money push

Sends a USSD prompt to the customer's phone. See Mobile money push.

$payment = $teepay->collections->create([
    'money' => ['amount_minor' => '500000', 'currency' => 'TZS'], // TZS 5,000.00
    'customer' => ['country' => 'TZ', 'phone' => '+255712345678', 'name' => 'Asha Juma'],
    'description' => 'Order 1001',
], ['idempotency_key' => 'order-1001']);

echo $payment->public_reference; // TP8K2M4QZ1
echo $payment->status;           // "pending": the prompt was sent, not paid yet

$latest = $teepay->collections->retrieve($payment->public_reference);
if ($latest->status === 'succeeded') {
    // fulfil the order
}

Hosted checkout session

See Checkout sessions.

$session = $teepay->checkoutSessions->create([
    'title' => 'Order 1001',
    'description' => '2 x Coffee bag',
    'amount_minor' => '500000',
    'currency' => 'TZS',
    'reference' => 'order-1001',
    'metadata' => ['cart' => 'c-77'], // keys must be lowercase
], ['idempotency_key' => 'checkout-order-1001']);

header('Location: ' . $session->checkout_url, true, 303);

// Later, on your server:
$session = $teepay->checkoutSessions->retrieve($session->id);
if ($session->status === 'paid') {
    echo $session->payment->reference;
}

Control Numbers and payouts

$number = $teepay->controlNumbers->create([
    'kind' => 'bill',
    'customer' => ['name' => 'Asha Juma', 'phone' => '0712345678'],
    'description' => 'School fees, term 1',
    'amount_minor' => '5000000',
], ['idempotency_key' => 'invoice-2026-0077']);
if ($number->getLastResponse()->status === 202) {
    // Not issued yet: call create again later with the SAME idempotency_key.
}

$quote = $teepay->disbursements->quote([
    'money' => ['amount' => 2500000, 'currency' => 'TZS'], // payout quotes take an integer
    'destination' => ['channel' => 'mobile', 'name' => 'Juma Ali', 'phone' => '255712345678'],
    'narration' => 'Supplier payment October',
]);
$payout = $teepay->disbursements->create([
    'quote_id' => $quote->id,
    'idempotency_key' => 'payout-2026-10-0001',
]);

Lists

$page = $teepay->collections->list(['limit' => 50, 'status' => 'succeeded']);
foreach ($page['items'] as $item) {
    echo $item['public_reference'], PHP_EOL;
}

foreach ($teepay->collections->listAll(['status' => 'succeeded']) as $payment) { // follows next_cursor
    echo $payment->public_reference, PHP_EOL;
}

Results are read-only TeePay\TeePayObjects: use array or property access, and toArray() for a plain array. Control Number lists put their rows in data.

Verify webhooks

Read the raw body from php://input before decoding it.

use TeePay\Exception\WebhookVerificationError;
use TeePay\Webhook;

$payload = file_get_contents('php://input');
try {
    $event = Webhook::verify($payload, getallheaders(), getenv('TEEPAY_WEBHOOK_SECRET'));
} catch (WebhookVerificationError $e) {
    http_response_code(400);
    exit;
}

if ($event->event === 'payment.completed') {
    // Find the order by $event->data->reference; process each $event->id once.
}
http_response_code(200);

Webhook::verify($payload, $headers, $secret, $tolerance = 300) checks the signature in constant time. Headers can come from getallheaders(), $_SERVER or a PSR-7 $request->getHeaders(). $teepay->webhooks->verify(...) does the same, Webhook::isValid(...) returns a bool, and Webhook::generateTestHeaders($payload, $secret) signs a payload for your tests.

Errors

use TeePay\Exception;

try {
    $teepay->collections->create($params, ['idempotency_key' => 'order-1001']);
} catch (Exception\ValidationError $e) {
    echo $e->errorCode; // invalid_request, unsupported_market, invalid_amount, ...
} catch (Exception\AccountRestrictedError $e) {
    // 403 account_restricted: contact TeePay support
} catch (Exception\RateLimitError $e) {
    sleep($e->retryAfter ?? 1);
} catch (Exception\ConnectionError $e) {
    // No response: retry with the SAME key or read the payment.
} catch (Exception\ApiError $e) {
    error_log("TeePay {$e->status} {$e->errorCode} request {$e->requestId}");
}
Class (TeePay\Exception\)When
AuthenticationError401 invalid_credential
PermissionError403 scope_denied, access_denied, developer_access_denied
AccountRestrictedError403 account_restricted (also a PermissionError)
NotFoundError404 not_found
ConflictError409 idempotency_conflict, expired payout quote
ValidationError422 codes such as invalid_request and unsupported_market
RateLimitError429 rate_limited, with retryAfter
ApiErrorAny other HTTP error; base class of all of the above
ConnectionError, TimeoutErrorNo HTTP response
WebhookVerificationErrorBad webhook signature or timestamp

The API error code is $e->errorCode (PHP reserves getCode() for the HTTP status). Mistakes the SDK can catch locally, such as an uppercase metadata key, throw \InvalidArgumentException before sending.

Idempotency and retries

Collections, checkout sessions and Control Numbers send an Idempotency-Key header; disbursements->create() sends idempotency_key in the body. Without one, the SDK generates a key and reuses it for its own retries. Derive keys from your own records so a retry cannot create a second payment. See Idempotency.

Only GET requests and keyed creates are retried, on connection errors, timeouts, 408, 429 and 5xx, using Retry-After when present. Per call: ['idempotency_key' => …, 'timeout' => 10, 'max_retries' => 0].

Response metadata

$info = $session->getLastResponse();
$info->status;             // 201
$info->requestId;          // quote to support
$info->idempotentReplayed; // true when an earlier session was returned
$info->rateLimit;          // ['limit' => 120, 'remaining' => 117, 'reset' => 42] or null

On this page