Skip to content

PHP SDK

xaalis-php (in integrations/sdk-php) wraps the API for PHP 8.1+ servers. No runtime dependencies beyond ext-curl and ext-json. Not on Packagist yet: use it from the monorepo, through Composer’s PSR-4 autoload (Xaalis\ → src/) or the bundled autoload.php.

require __DIR__ . '/vendor/autoload.php';
$xaalis = new Xaalis\Client(getenv('XAALIS_SECRET_KEY'), ['base_url' => 'http://localhost:4000']);
$payment = $xaalis->payments->create(
[
'amount' => 15000, // int, XOF — never a float or a string
'description' => 'Commande #1042',
'client_reference' => 'order_1042',
'success_url' => 'https://shop.example/merci',
'metadata' => ['order_id' => '1042'],
],
['idempotency_key' => 'payment:order_1042'],
);
header('Location: ' . $payment['checkout_url']); // hosted checkout

Responses are associative arrays with the same fields as the API (payment object).

Service Methods
payments create, retrieve, list, all (generator over every page)
payouts create, retrieve, list, all
balance retrieve
account retrieve, update(['webhook_url' => 'https://…'])
webhookDeliveries list(['limit' => 20])
testHelpers simulatePayment($id, 'succeeded' | 'failed') — test keys only
Xaalis\Webhook::constructEvent($raw, $header, $secret) verify and parse a webhook
Option Default
base_url https://api.xaalis.sn a live key over http:// is refused
timeout 30 seconds per attempt
max_retries 2 retries after the first attempt
http curl fn($method, $url, $headers, $body, $timeout) returning ['status', 'body', 'headers'] — for tests

The key must match sk_test_… / sk_live_… (40 characters after the prefix); it never appears in exception messages or in var_dump($xaalis). $xaalis->testMode is true for test keys.

  • Idempotency: every create sends an Idempotency-Key — a random UUID v4 unless you pass idempotency_key — and reuses it on every retry. Pass your own (your order id) to stay safe across process restarts too. See Idempotency.
  • Retries: network errors, 429 and 5xx, with exponential backoff and Retry-After — only when replaying is safe: GET, PATCH, or a create carrying its key. 4xx is never retried.
  • Integers only: amount must be a PHP int; 150.5, 15000.0 and "15000" throw \InvalidArgumentException before anything is sent.
use Xaalis\Exception\ApiException;
use Xaalis\Exception\ConnectionException;
try {
$xaalis->payouts->create(['amount' => 5000, 'provider' => 'wave', 'recipient' => ['phone' => '+221770000001']]);
} catch (ApiException $e) {
if ($e->getErrorCode() === 'insufficient_funds') {
// show "balance too low"
} else {
throw $e;
}
} catch (ConnectionException $e) {
// unknown outcome: retry later with the SAME idempotency_key
}
Exception When
Xaalis\Exception\XaalisException base class of the three below
ApiException the API answered an error: getStatus(), getErrorCode() (stable, see Errors), getMessage(), getDetails()
ConnectionException network failure or timeout after the retries
SignatureException a webhook failed verification — answer 400
$raw = file_get_contents('php://input'); // the exact bytes, never a re-encoded array
try {
$event = Xaalis\Webhook::constructEvent($raw, $_SERVER['HTTP_XAALIS_SIGNATURE'] ?? null, getenv('XAALIS_WEBHOOK_SECRET'));
} catch (Xaalis\Exception\SignatureException $e) {
http_response_code(400);
exit;
}
if ($event['livemode'] !== $expectLive || alreadyProcessed($event['id'])) { // test + live share one URL; at-least-once
http_response_code(200);
exit;
}
if ($event['type'] === 'payment.succeeded') {
$payment = $event['data']['object'];
// check $payment['amount'] and $payment['metadata'] against your order, then fulfil it once
}
http_response_code(200);

constructEvent rejects a missing or malformed header, a timestamp more than 300 s away from now (replays), and a signature that doesn’t match any v1 (compared with hash_equals).

integrations/sdk-php/tests/docker-test.sh runs php -l and the test suite in php:8.3-cli. With ADMIN_TOKEN in the repo-root .env it also runs a live flow against the local API: create a merchant, a 15 000 payment (fee 225), simulate success, check the balance is 14 775, and check a 99 000 000 payout fails with insufficient_funds.

For WooCommerce stores, use the WooCommerce plugin instead — it doesn’t need Composer.