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-phpCreate 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 |
|---|---|
AuthenticationError | 401 invalid_credential |
PermissionError | 403 scope_denied, access_denied, developer_access_denied |
AccountRestrictedError | 403 account_restricted (also a PermissionError) |
NotFoundError | 404 not_found |
ConflictError | 409 idempotency_conflict, expired payout quote |
ValidationError | 422 codes such as invalid_request and unsupported_market |
RateLimitError | 429 rate_limited, with retryAfter |
ApiError | Any other HTTP error; base class of all of the above |
ConnectionError, TimeoutError | No HTTP response |
WebhookVerificationError | Bad 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