Skip to content

PHP SDK

xaalis/xaalis-php wraps the API for PHP 8.1+ servers. No runtime dependencies beyond ext-curl and ext-json; MIT licence. Until it is on Packagist (composer require xaalis/xaalis-php), download xaalis-php-0.1.0.zip into a folder (here packages/) and let Composer install it from there:

{
"repositories": [{ "type": "artifact", "url": "packages/" }],
"require": { "xaalis/xaalis-php": "^0.1" }
}

Without Composer, unzip it into xaalis-php/ and require 'xaalis-php/autoload.php';. Every release: Downloads.

require __DIR__ . '/vendor/autoload.php';
$xaalis = new Xaalis\Client(getenv('XAALIS_SECRET_KEY'), ['base_url' => 'https://sandbox-api.xaalis.org']);
$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 from the key https://sandbox-api.xaalis.org for a sk_test_… key, https://api.xaalis.org for sk_live_…. Refused: a live key over http:// or to sandbox-api.xaalis.org, a test key to api.xaalis.org or http://sandbox-api.xaalis.org, and any URL with credentials, a query or a fragment. Other hosts (a proxy, http://localhost:4000) are accepted. SDK 0.1.0 defaulted to https://api.xaalis.sn for every key, which does not answer, and refuses none of these hosts: on 0.1.0 always pass the base_url that matches your key
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.