Skip to content

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 noms Soamtar\.
  • PHP 8.2+ avec ext-curl et ext-json. Aucune autre dépendance.
  • Devise SLE par 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), fournisseur orange_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 ​

bash
composer require soamtar/soamtar-php

Configuration ​

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.

php
use Soamtar\Soamtar;

Soamtar::configure(getenv('SOAMTAR_SECRET_KEY')); // ou Soamtar::configure() pour lire SOAMTAR_SECRET_KEY

Ou instanciez un client explicitement (pratique pour l'injection de dépendances) :

php
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([...]);
OptionDéfautRemarques
base_urlhttps://app.so-amtar.com/v1HTTPS obligatoire (HTTP accepté sur localhost). Repli sur SOAMTAR_API_URL, puis SOAMTAR_BASE_URL (ancien nom, déprécié).
timeout30Délai total de la requête, en secondes.
connect_timeout10Délai de connexion, en secondes.
sign_requestsfalseAjoute Soamtar-Timestamp / Soamtar-Signature à chaque POST (voir Signature des requêtes).
transportcURLcallable(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 :

php
$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) :

php
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_url

currency 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 :

php
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) ​

php
$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.

php
$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.

php
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 :

php
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 ​

php
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 actifs

Sans 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() :

php
use Soamtar\SoamtarClient;

$this->app->singleton(SoamtarClient::class, fn () => new SoamtarClient(config('services.soamtar.secret')));
php
// config/services.php
'soamtar' => [
    'secret'         => env('SOAMTAR_SECRET_KEY'),
    'webhook_secret' => env('SOAMTAR_WEBHOOK_SECRET'),
],

Contrôleur :

php
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 :

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['soamtar/webhook']);
})

Erreurs ​

ExceptionCas
Soamtar\Exceptions\ApiExceptionStatut HTTP ≥ 400. getHttpStatus(), getErrorCode(), getRawResponse() (secrets masqués), isRetryable() (429 et 5xx).
Soamtar\Exceptions\SignatureExceptionSignature de webhook invalide, expirée ou mal formée.
Soamtar\Exceptions\SoamtarExceptionClasse de base : configuration, réseau ou réponse non JSON.
php
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çadeMé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 :

php
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();
});

SOAMTAR Payment API Documentation.