SDK PHP / Laravel
Client PHP officiel de l'API SOAMTAR : encaissez et envoyez des fonds avec Orange Money Sierra Leone, et vérifiez les webhooks.
- Paquet :
soamtar/soamtar-php(version 1.1.0), espace de nomsSoamtar\. - PHP 8.2+ avec
ext-curletext-json. Aucune autre dépendance. - Devise
SLEpar défaut (USD et EUR aussi acceptés), montants en chaînes à 2 décimales ("150.00"), numéros au format E.164 (+232+ 8 chiffres), fournisseurorange_money_sl.
Pas encore sur Packagist
Tant que le paquet n'est pas publié, installez-le depuis le dépôt du SDK (dépôt Composer vcs ou path).
Installation
composer require soamtar/soamtar-phpConfiguration
Utilisez votre clé secrète (sk_test_… ou sk_live_…) uniquement côté serveur. Les clés de test passent par la simulation, sur le même hôte : aucun argent réel ne circule.
use Soamtar\Soamtar;
Soamtar::configure(getenv('SOAMTAR_SECRET_KEY')); // ou Soamtar::configure() pour lire SOAMTAR_SECRET_KEYOu instanciez un client explicitement (pratique pour l'injection de dépendances) :
use Soamtar\SoamtarClient;
$soamtar = new SoamtarClient(getenv('SOAMTAR_SECRET_KEY'), [
'base_url' => 'https://app.so-amtar.com/v1', // valeur par défaut ; lue aussi dans SOAMTAR_API_URL, puis SOAMTAR_BASE_URL
'timeout' => 30, // secondes
'connect_timeout' => 10,
'sign_requests' => false, // signature facultative des requêtes, voir plus bas
]);
$soamtar->payments->create([...]);| Option | Défaut | Remarques |
|---|---|---|
base_url | https://app.so-amtar.com/v1 | HTTPS obligatoire (HTTP accepté sur localhost). Repli sur SOAMTAR_API_URL, puis SOAMTAR_BASE_URL (ancien nom, déprécié). |
timeout | 30 | Délai total de la requête, en secondes. |
connect_timeout | 10 | Délai de connexion, en secondes. |
sign_requests | false | Ajoute Soamtar-Timestamp / Soamtar-Signature à chaque POST (voir Signature des requêtes). |
transport | cURL | callable(string $method, string $url, array $headers, ?string $body): array{status: int, body: string}, pour les tests ou une pile HTTP maison. |
Chaque POST porte un en-tête Idempotency-Key (UUID v4). Passez le vôtre en dernier argument de create() : une nouvelle tentative ne débitera jamais deux fois.
Signature des requêtes
Avec 'sign_requests' => true, chaque POST porte aussi :
Soamtar-Timestamp: <secondes unix>
Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHODE>.<chemin>"))<chemin> est le chemin de l'URL sans la barre oblique initiale (par exemple v1/payments) ; l'horodatage doit être à ±300 secondes de l'horloge du serveur. C'est un durcissement facultatif : la clé Bearer sur HTTPS suffit à elle seule. $client->signatureHeaders('POST', $url) renvoie les deux en-têtes si vous construisez vos requêtes vous-même. Voir Authentification.
Encaisser un paiement
Demande envoyée directement sur le téléphone du client, qui valide avec son code PIN Orange Money :
$payment = Soamtar::payments()->create([
'merchant_ref' => 'ORDER-1001',
'amount' => '150.00', // Le 150.00
'provider' => 'orange_money_sl',
'phone' => '+23276000001',
'description' => 'Commande n° 1001',
'callback_url' => 'https://shop.example.sl/soamtar/webhook', // facultatif
], 'order-1001'); // Idempotency-Key
$payment['data']['status']; // "pending" tant que le client n'a pas saisi son PIN
$payment['data']['reference']; // référence SOAMTAR, ex. TXN-…Page de paiement hébergée (SOAMTAR demande lui-même le numéro) :
Soamtar::payments()->create([
'merchant_ref' => 'ORDER-1002',
'amount' => '75.50',
'success_url' => 'https://shop.example.sl/merci',
'failed_url' => 'https://shop.example.sl/panier',
])->redirect(); // 302 vers data.payment_urlcurrency vaut SLE par défaut. L'API accepte encore return_url / cancel_url comme alias de success_url / failed_url.
callback_url (facultatif, HTTPS) est une URL de webhook propre au paiement : SOAMTAR y envoie tous les événements de ce paiement, avec le même payload et la même X-Soamtar-Signature que pour vos endpoints enregistrés ; vérifiez-le donc avec Webhook::constructEvent() de la même façon. Il est signé avec le secret de votre plus ancien endpoint actif : enregistrez-en au moins un. Les payouts l'acceptent aussi.
Suivre un paiement :
Soamtar::payments()->retrieve($id); // id ou référence SOAMTAR
Soamtar::payments()->verify($id); // interroge Orange Money pour le dernier statut
Soamtar::payments()->status($idOrReference); // data.id ou data.reference (pas votre merchant_ref)
Soamtar::payments()->list(['status' => 'success', 'per_page' => 20]);Envoyer un paiement sortant (payout)
$payout = Soamtar::payouts()->create([
'merchant_ref' => 'PAYOUT-042',
'amount' => '250.00',
'recipient' => ['phone' => '+23276000001', 'name' => 'Aminata Kamara'],
'description' => 'Commission agent octobre',
]);provider vaut orange_money_sl et currency SLE par défaut. La clé doit disposer du scope can_payout. Le résultat final arrive par les webhooks payout.success / payout.failed.
Rembourser un paiement
Remboursez un paiement réussi, en totalité ou en partie : SOAMTAR recrédite le montant sur le numéro Orange Money du payeur et débite votre solde. La clé doit disposer du scope can_refund.
$refund = Soamtar::refunds()->create([
'transaction_id' => $payment['data']['id'], // identifiant (ou référence) SOAMTAR du paiement
'amount' => '50.00', // omis : remboursement total du restant
'reason' => 'customer_request',
'description' => 'Article en rupture de stock',
], 'refund-order-1001'); // Idempotency-Key
$refund['data']['reference']; // "REF-…"
$refund['data']['status']; // "success" (clé de test), "processing" (Orange Money) ou "failed"
Soamtar::refunds()->retrieve($refund['data']['reference']); // identifiant ou référence
Soamtar::refunds()->list(['transaction_id' => $payment['data']['id'], 'status' => 'success']);Le remboursement est soumis dans la même requête ; avec Orange Money il reste processing jusqu'à ce qu'Orange confirme le crédit. Le résultat arrive par les webhooks refund.success / refund.failed, plus payment.refunded / payment.partially_refunded en cas de succès. Voir Remboursements pour l'objet et les codes d'erreur.
Webhooks
Les notifications partent vers les endpoints webhook que vous avez enregistrés (tableau de bord ou webhooks()->create()), signées avec le secret de chaque endpoint. Chaque envoi porte X-Soamtar-Signature: t=<unix>,v1=<hex> (plus v2= pendant une rotation du secret). La signature vaut HMAC-SHA256(secret, "<t>.<corps brut>") et reste valable 300 secondes.
use Soamtar\Exceptions\SignatureException;
use Soamtar\Webhook;
try {
$event = Webhook::constructEvent(
file_get_contents('php://input'), // corps BRUT
$_SERVER['HTTP_X_SOAMTAR_SIGNATURE'] ?? '',
getenv('SOAMTAR_WEBHOOK_SECRET'),
);
} catch (SignatureException $e) {
http_response_code(400);
exit;
}
match ($event['event']) {
'payment.success' => markOrderPaid($event['data']['merchant_reference']),
'payment.failed', 'payment.cancelled', 'payment.expired' => markOrderFailed($event['data']['merchant_reference']),
'refund.success' => markOrderRefunded($event['data']['merchant_reference'], $event['data']['amount']),
default => null, // webhook.test et le reste : répondez simplement 2xx
};
http_response_code(200);Un même événement peut être renvoyé : rendez votre traitement idempotent sur data.id.
Événements auxquels s'abonner : payment.success, payment.failed, payment.cancelled, payment.expired, payment.refunded, payment.partially_refunded, payout.success, payout.failed, payout.cancelled, refund.success, refund.failed, payout_request.submitted, payout_request.approved, payout_request.rejected, payout_request.completed, payout_request.failed. L'événement de test webhook.test (envoyé par sendTest() ou le tableau de bord) part quels que soient vos abonnements et n'attend qu'un 2xx. Voir Webhooks.
Sans secret stocké (AutoVerifier)
Si vous ne conservez pas SOAMTAR_WEBHOOK_SECRET, AutoVerifier récupère le secret de l'endpoint avec votre clé secrète (scope can_manage_webhooks) et le met en cache :
use Soamtar\SoamtarClient;
use Soamtar\Webhook\AutoVerifier;
use Soamtar\Webhook\FilesystemWebhookSecretCache;
$verifier = new AutoVerifier(
new SoamtarClient(getenv('SOAMTAR_SECRET_KEY')),
new FilesystemWebhookSecretCache(__DIR__.'/storage/soamtar-cache'),
);
$event = $verifier->constructEvent($payload, $signatureHeader, getenv('SOAMTAR_WEBHOOK_ENDPOINT_ID'));Gérer les endpoints
Soamtar::webhooks()->create(['url' => 'https://shop.example.sl/soamtar/webhook']); // idempotent par URL
Soamtar::webhooks()->list();
Soamtar::webhooks()->lookup('https://shop.example.sl/soamtar/webhook');
Soamtar::webhooks()->secret($endpointId);
Soamtar::webhooks()->sendTest(); // envoie webhook.test à tous les endpoints actifsSans subscribed_events, create() abonne l'endpoint à payment.success, payment.failed, payout.success et payout.failed.
Alias dépréciés, toujours fonctionnels : webhooks()->all() (utilisez list()), webhooks()->revealSecret() (utilisez secret()), balance()->show() (utilisez retrieve()).
Laravel
Déclarez le client une fois, dans AppServiceProvider::register() :
use Soamtar\SoamtarClient;
$this->app->singleton(SoamtarClient::class, fn () => new SoamtarClient(config('services.soamtar.secret')));// config/services.php
'soamtar' => [
'secret' => env('SOAMTAR_SECRET_KEY'),
'webhook_secret' => env('SOAMTAR_WEBHOOK_SECRET'),
],Contrôleur :
use Illuminate\Http\Request;
use Soamtar\Exceptions\SignatureException;
use Soamtar\SoamtarClient;
use Soamtar\Webhook;
final class CheckoutController
{
public function __construct(private readonly SoamtarClient $soamtar) {}
public function pay(Order $order)
{
$payment = $this->soamtar->payments->create([
'merchant_ref' => $order->reference,
'amount' => number_format($order->total, 2, '.', ''),
'success_url' => route('orders.thanks', $order),
'failed_url' => route('orders.show', $order),
], 'order-'.$order->id);
return redirect()->away($payment['data']['payment_url']);
}
public function webhook(Request $request)
{
try {
$event = Webhook::constructEvent(
$request->getContent(),
(string) $request->header('X-Soamtar-Signature'),
config('services.soamtar.webhook_secret'),
);
} catch (SignatureException) {
abort(400);
}
if ($event['event'] === 'payment.success') {
Order::where('reference', $event['data']['merchant_reference'])->first()?->markPaid();
}
return response()->noContent();
}
}Excluez la route du webhook de la protection CSRF dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['soamtar/webhook']);
})Erreurs
| Exception | Cas |
|---|---|
Soamtar\Exceptions\ApiException | Statut HTTP ≥ 400. getHttpStatus(), getErrorCode(), getRawResponse() (secrets masqués), isRetryable() (429 et 5xx). |
Soamtar\Exceptions\SignatureException | Signature de webhook invalide, expirée ou mal formée. |
Soamtar\Exceptions\SoamtarException | Classe de base : configuration, réseau ou réponse non JSON. |
use Soamtar\Exceptions\ApiException;
try {
Soamtar::payouts()->create($params, $idempotencyKey);
} catch (ApiException $e) {
if ($e->isRetryable()) {
// réessayez plus tard avec la MÊME clé d'idempotence
}
error_log($e->getErrorCode().': '.$e->getMessage());
}Référence
| Façade | Méthodes |
|---|---|
Soamtar::payments() | create(array $params, string $idempotencyKey = ''), retrieve($id), list(array $params = []), status($idOrReference), verify($id), listSplits($transactionId), createSplit($transactionId, array $params, string $idempotencyKey = '') |
Soamtar::payouts() | create(array $params, string $idempotencyKey = ''), retrieve($id), list(array $params = []) |
Soamtar::refunds() | create(array $params, string $idempotencyKey = ''), retrieve($idOrReference), list(array $params = []) |
Soamtar::payoutRequests() | create(array $params, string $idempotencyKey = ''), list(array $params = []), retrieve($id) |
Soamtar::subscriptions() | create(['plan_id' => …, 'customer_id' => …]), retrieve($id), list(array $params = []), cancel($id) |
Soamtar::webhooks() | list(), create(array $payload), lookup(string $url), secret($endpointId), sendTest() |
Soamtar::balance() | retrieve() |
Soamtar::providers() | all() |
Soamtar::status() | retrieve(string $reference) (identique à payments()->status()) |
Soamtar::disputes() | list(array $params = []), retrieve($id), respond($id, array $params, string $idempotencyKey = '') |
Soamtar::settlements() | list(array $params = []), retrieve($id) |
Soamtar::health() | GET/v1/health |
Les mêmes ressources existent en propriétés d'une instance de SoamtarClient ($soamtar->payments, $soamtar->payouts, …). Chaque méthode renvoie un Soamtar\SoamtarResponse (accès tableau, get(), toArray(), successful(), redirect()).
Tester votre webhook
Webhook::sign() produit un en-tête valide, pratique avec Pest ou PHPUnit :
use Soamtar\Webhook;
it('accepte un payment.success signé', function () {
config()->set('services.soamtar.webhook_secret', 'whsec_test');
$payload = json_encode(['event' => 'payment.success', 'data' => ['id' => 'tx_1', 'merchant_reference' => 'ORDER-1001']]);
$this->call('POST', '/soamtar/webhook', [], [], [], [
'HTTP_X_SOAMTAR_SIGNATURE' => Webhook::sign($payload, 'whsec_test'),
'CONTENT_TYPE' => 'application/json',
], $payload)->assertNoContent();
});