Skip to content

Webhooks & IPN / APN ​

SOAMTAR notifie votre serveur en temps réel par webhooks, aussi appelés IPN (Instant Payment Notification) ou APN (Automated Payment Notification).

  • IPN : notification instantanée dès que le paiement est confirmé (ex. payment.success).
  • APN : notifications automatiques pour les changements d'état secondaires et le cycle de vie (ex. payment.expired).

La livraison est signée (HMAC-SHA256), idempotente par événement, retentée avec backoff, et le secret peut être changé sans coupure.

Événements ​

Voici les seuls événements auxquels vous pouvez vous abonner :

ÉvénementQuand
payment.successOrange Money Sierra Leone a confirmé le paiement (le client a saisi son code PIN)
payment.failedOrange Money a refusé le paiement, renvoyé une erreur ou expiré
payment.cancelledLe payeur a annulé
payment.expiredLa fenêtre de paiement s'est écoulée sans finalisation
payment.refundedLes remboursements couvrent désormais la totalité du paiement
payment.partially_refundedUn remboursement a réussi et une partie du paiement reste remboursable
payout.successUn paiement sortant vers un compte Orange Money a été confirmé
payout.failedUn paiement sortant a échoué
payout.cancelledUn paiement sortant a été annulé
refund.successUn remboursement a été crédité sur le compte Orange Money du payeur
refund.failedUn remboursement a été refusé ou n'a pas pu aboutir (voir failure_reason)
payout_request.submittedUne demande de retrait vers votre banque / wallet a été soumise
payout_request.approvedLa demande de retrait a été approuvée
payout_request.rejectedLa demande de retrait a été rejetée
payout_request.completedLe retrait a été versé
payout_request.failedLe retrait a échoué

Vous choisissez les événements de chaque endpoint (Dashboard → Webhooks, ou subscribed_events sur POST /v1/webhooks). Si vous omettez subscribed_events, les SDK abonnent l'endpoint à payment.success, payment.failed, payout.success et payout.failed.

Il existe un événement de plus, webhook.test, envoyé par le bouton de test du dashboard et par POST /v1/webhooks/test. Il est livré à tous les endpoints actifs de l'environnement quels que soient leurs abonnements, et on ne peut pas s'y abonner. Répondez-y par un 2xx comme pour tout autre événement.

Configuration ​

Dashboard → Développeurs → Webhooks → Add endpoint

  • URL : HTTPS publique obligatoire.
  • Événements : sous-ensemble de la liste ci-dessus.
  • Secret : généré aléatoirement. À conserver côté serveur.

Un endpoint est suspendu automatiquement après un certain nombre d'échecs consécutifs (seuil interne).

Contenu ​

json
{
  "event": "payment.success",
  "created": "2026-10-21T09:41:12+00:00",
  "data": {
    "id": "0e85b3c2-7c4f-4f1d-9b3a-1c8a2d4e5f6a",
    "reference": "TXN-1A2B3C4D-ABCDEF123456",
    "merchant_reference": "ORDER-001",
    "provider_reference": "CI190304.1139.A01071",
    "amount": "150.00",
    "fee_amount": "2.25",
    "net_amount": "147.75",
    "currency": "SLE",
    "status": "success",
    "is_test": false,
    "paid_at": "2026-10-21T09:41:10+00:00",
    "metadata": { "cart_id": "abc-123" }
  }
}

data.id est l'identifiant SOAMTAR de la transaction ; data.merchant_reference est le merchant_ref que vous avez envoyé.

Événements de remboursement ​

Chaque remboursement produit son propre jeu d'événements (un paiement peut porter plusieurs remboursements partiels) :

  • un remboursement réussi envoie refund.success et payment.partially_refunded (ou payment.refunded quand les remboursements couvrent tout le paiement) ;
  • un remboursement refusé envoie seulement refund.failed.

Pour refund.success / refund.failed, data est l'objet remboursement complété de transaction_reference, merchant_reference et is_test :

json
{
  "event": "refund.success",
  "created": "2026-10-22T10:05:31+00:00",
  "data": {
    "id": "9b1f4c2e-5d6a-4e8b-a1c3-2f4e6a8b0c1d",
    "object": "refund",
    "reference": "REF-7K2M9Q4XW8T1B3NC",
    "transaction_id": "0e85b3c2-7c4f-4f1d-9b3a-1c8a2d4e5f6a",
    "amount": "40.00",
    "currency": "SLE",
    "reason": "customer_request",
    "description": null,
    "status": "success",
    "fee_refunded": false,
    "fee_refunded_amount": "0.60",
    "tax_refunded_amount": "0.00",
    "failure_reason": null,
    "processed_at": "2026-10-22T10:05:30+00:00",
    "failed_at": null,
    "created_at": "2026-10-22T10:04:58+00:00",
    "updated_at": "2026-10-22T10:05:30+00:00",
    "transaction_reference": "TXN-1A2B3C4D-ABCDEF123456",
    "merchant_reference": "ORDER-001",
    "is_test": false
  }
}

Pour payment.refunded / payment.partially_refunded, data est le payload habituel de la transaction (voir ci-dessus) avec un objet data.refund supplémentaire : le remboursement qui a déclenché l'événement.

Signature ​

Chaque requête porte le header :

X-Soamtar-Signature: t=<unix>,v1=<hex_hmac_sha256>

Pendant un changement de secret, deux signatures peuvent coexister :

X-Soamtar-Signature: t=<unix>,v1=<hmac_secret_courant>,v2=<hmac_secret_suivant>

Vérification ​

  1. Extraire l'horodatage t et la signature v1 (et v2 si présente).
  2. Vérifier que t est récent (dans la fenêtre de tolérance).
  3. Recalculer expected = HMAC-SHA256(t + "." + raw_body, secret).
  4. Comparer en temps constant (hash_equals en PHP, crypto.timingSafeEqual en Node).
  5. Si une v2 est présente, accepter si v1 OU v2 correspond.
php
$header = $_SERVER['HTTP_X_SOAMTAR_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$secrets = [getenv('SOAMTAR_WEBHOOK_SECRET'), getenv('SOAMTAR_WEBHOOK_SECRET_NEXT')];

if (! preg_match('/t=(\d+)/', $header, $tm)) throw new RuntimeException('bad sig');
$ts = (int) $tm[1];
if (abs(time() - $ts) > 300) throw new RuntimeException('expired');

$ok = false;
foreach (['v1', 'v2'] as $i => $version) {
    if (! preg_match("/{$version}=([a-f0-9]{64})/", $header, $vm)) continue;
    $secret = $secrets[$i] ?? null;
    if ($secret === null || $secret === '') continue;
    if (hash_equals(hash_hmac('sha256', "{$ts}.{$body}", $secret), $vm[1])) {
        $ok = true; break;
    }
}
if (! $ok) throw new RuntimeException('invalid');

Tolérance

L'exemple utilise 300 secondes, un plafond raisonnable qui correspond à la tolérance appliquée par SOAMTAR.

Headers complémentaires ​

HeaderSens
X-Soamtar-TimestampHorodatage de signature (même valeur que t)
X-Soamtar-EventType d'événement (payment.success, refund.success, webhook.test, ...)
X-Soamtar-DeliveryIdentifiant de la tentative de livraison — utile dans les logs
User-AgentSoamtar-Webhooks/1.0 (+https://app.so-amtar.com/docs/webhooks)

Idempotence côté votre serveur ​

Le même événement peut arriver plusieurs fois (réessai après timeout, rejeu après incident). Dédupliquez sur le couple (event, data.id) et ignorez les doublons. Pour payment.refunded / payment.partially_refunded, ajoutez data.refund.id à la clé : un même paiement peut émettre payment.partially_refunded plusieurs fois, une par remboursement.

sql
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, object_id, refund_id);

Réessais et livraison ​

  • 5 tentatives au maximum : la livraison initiale, puis des réessais après 60 s, 5 min, 30 min et 2 h.
  • 2xx = succès, SOAMTAR s'arrête.
  • 3xx = considéré comme un échec (votre endpoint doit répondre 2xx directement).
  • 4xx, 5xx, timeout = échec, réessai programmé.
  • Après un certain nombre d'échecs consécutifs, l'endpoint est suspendu et vous recevez un email.

Vous pouvez relancer manuellement depuis Dashboard → Webhook logs → Resend.

callback_url par transaction ​

POST /v1/payments et POST /v1/payouts acceptent un callback_url optionnel (HTTPS). SOAMTAR y envoie tous les événements de cette transaction, en plus de vos endpoints enregistrés :

  • même payload ({event, created, data}), mêmes headers ;
  • même X-Soamtar-Signature, signée avec le secret de votre plus ancien endpoint webhook actif du même environnement (test ou live). Il vous faut donc au moins un endpoint enregistré pour recevoir les callbacks : sans endpoint, il n'y a aucun secret pour signer et le callback n'est pas envoyé ;
  • mêmes réessais (60 s, 5 min, 30 min, 2 h) ;
  • pas d'envoi en double : si callback_url est égal à l'URL d'un de vos endpoints enregistrés, seule la livraison à l'endpoint a lieu.

Le callback reçoit les événements de la transaction quels que soient les abonnements de vos endpoints (événements de remboursement compris) : vérifiez la signature et dédupliquez exactement comme pour un endpoint.

Changement de secret sans coupure ​

  1. Dashboard → Webhooks → Rotate secret. SOAMTAR affiche le nouveau secret (copiez-le maintenant, il n'est jamais réaffiché).
  2. Pendant la fenêtre de rotation, chaque envoi peut être signé avec les deux secrets.
  3. Mettez à jour votre vérification pour accepter v1 OU v2.
  4. Cliquez sur Promote : le nouveau secret devient le secret courant, l'ancien est invalidé.

Tester un webhook ​

Envoyez un événement webhook.test signé avec votre vrai secret depuis le dashboard (Dashboard → Webhooks → Test) ou avec POST /v1/webhooks/test et votre clé secrète. Il part vers tous les endpoints actifs de l'environnement de la clé, quels que soient leurs abonnements. Sans endpoint actif, l'API répond 400 no_webhook_endpoints.

bash
curl -X POST https://app.so-amtar.com/v1/webhooks/test \
  -H "Authorization: Bearer sk_test_..."
json
{
  "message": "Test webhooks dispatched to 1 endpoint(s).",
  "event_type": "webhook.test",
  "payload": { "event": "webhook.test", "created": "...", "data": { "id": "evt_test_...", "reference": "TXN-TEST-12345", "status": "success", "is_test": true } }
}

Si vous ratez un webhook ​

  1. Endpoint de vérification : POST /v1/payments/{id}/verify fait interroger Orange Money par SOAMTAR et réémet l'événement si la transition a eu lieu.
  2. Un rapprochement automatique interne rattrape les transactions bloquées : en général, vous n'avez rien à faire.

SOAMTAR Payment API Documentation.