Webhooks e IPN / APN
SOAMTAR notifica a su servidor en tiempo real mediante webhooks, también llamados IPN (Instant Payment Notification) o APN (Automated Payment Notification).
- IPN: notificación inmediata en cuanto se confirma el pago (p. ej.
payment.success). - APN: notificaciones automáticas para los cambios de estado secundarios y los eventos del ciclo de vida (p. ej.
payment.expired).
Los envíos están firmados (HMAC-SHA256), son idempotentes por evento, se reintentan con espera exponencial y el secreto puede rotarse sin interrupción.
Eventos
| Evento | Cuándo |
|---|---|
payment.success | Orange Money Sierra Leona confirmó el pago (el cliente introdujo su PIN) |
payment.failed | Orange Money rechazó el pago, devolvió un error o agotó el tiempo de espera |
payment.cancelled | El pagador canceló |
payment.expired | Terminó el plazo de pago sin que se completara |
payment.refunded | Los reembolsos cubren ahora la totalidad del pago (data.refund = el reembolso) |
payment.partially_refunded | Un reembolso parcial se ha completado (data.refund = el reembolso) |
payout.success / payout.failed / payout.cancelled | Resultado de un pago saliente a un monedero Orange Money |
refund.success / refund.failed | Resultado de un reembolso (data = el objeto refund + transaction_reference, merchant_reference, is_test) |
payout_request.submitted / payout_request.approved / payout_request.rejected / payout_request.completed / payout_request.failed | Ciclo de vida de una solicitud de retiro de su saldo |
Usted elige los eventos a los que se suscribe cada endpoint (Panel → Webhooks). Cada reembolso genera su propio juego de eventos: refund.success + payment.refunded / payment.partially_refunded, o refund.failed. Ver Reembolsos (en inglés).
El evento webhook.test (enviado por el botón de prueba del panel y por POST /v1/webhooks/test) se entrega a todos sus endpoints activos sin importar sus suscripciones; no se puede suscribir a él.
Configuración
Panel → Desarrolladores → Webhooks → Add endpoint
- URL: debe ser HTTPS público.
- Eventos: subconjunto de la lista anterior.
- Secreto: generado aleatoriamente. Guárdelo en su servidor.
Un endpoint se suspende automáticamente tras varios fallos seguidos (umbral interno).
callback_url por transacción
Al crear un pago (POST /v1/payments) o un pago saliente (POST /v1/payouts) puede pasar un callback_url HTTPS opcional. SOAMTAR envía a esa URL todos los eventos de esa transacción, con el mismo payload, la misma firma X-Soamtar-Signature y los mismos reintentos que para sus endpoints.
- La firma usa el secreto de su endpoint de webhook activo más antiguo del mismo entorno (sandbox o live). Necesita por tanto al menos un endpoint registrado; sin ninguno, el
callback_urlno se llama. - Si el
callback_urles igual a la URL de un endpoint registrado, el evento no se envía dos veces.
Contenido del webhook
Cuerpo JSON enviado a su URL (formato por defecto):
{
"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 es el identificador de la transacción SOAMTAR; data.merchant_reference es el merchant_ref que usted envió.
Firma
Cada solicitud lleva la cabecera:
X-Soamtar-Signature: t=<unix>,v1=<hex_hmac_sha256>Durante una rotación de secreto pueden coexistir dos firmas:
X-Soamtar-Signature: t=<unix>,v1=<hmac_current_secret>,v2=<hmac_next_secret>Verificación
- Extraiga la marca de tiempo
ty la firmav1(yv2si existe). - Compruebe que
tes reciente (dentro de la ventana de tolerancia). - Recalcule
expected = HMAC-SHA256(t + "." + raw_body, secret). - Compare en tiempo constante (
hash_equalsen PHP,crypto.timingSafeEqualen Node). - Si hay una
v2, acepte cuando coincida v1 O v2.
$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');Tolerancia
El ejemplo usa 300 segundos, un límite razonable que coincide con la tolerancia que aplica SOAMTAR.
Cabeceras adicionales
| Cabecera | Significado |
|---|---|
X-Soamtar-Timestamp | Marca de tiempo de la firma (mismo valor que t) |
X-Soamtar-Event | Tipo de evento (payment.success, ...) |
X-Soamtar-Delivery | Identificador del intento de entrega, útil en los registros |
User-Agent | Soamtar-Webhooks/1.0 (+https://app.so-amtar.com/docs/webhooks) |
Idempotencia en su servidor
El mismo evento puede llegar varias veces (reintento tras un tiempo de espera, reenvío tras un incidente). Elimine duplicados según el par (event, data.id) e ignore los repetidos.
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);Reintentos y entrega
- 5 intentos como máximo: el envío inicial y luego reintentos tras 60 s, 5 min, 30 min y 2 h.
- 2xx = éxito, SOAMTAR se detiene.
- 3xx = se considera un fallo (su endpoint debe responder 2xx directamente).
- 4xx, 5xx o tiempo agotado = fallo, se programa un reintento.
- Tras varios fallos seguidos, el endpoint se suspende y recibe un correo.
Puede reenviar una entrega manualmente desde Panel → Webhook logs → Resend.
Rotación del secreto sin interrupción
- Panel → Webhooks → Rotate secret. SOAMTAR muestra el nuevo secreto (cópielo ahora: no se vuelve a mostrar).
- Durante la ventana de rotación, cada payload puede firmarse con los dos secretos.
- Actualice su verificador para aceptar
v1Ov2. - Haga clic en Promote: el nuevo secreto pasa a ser el actual y el anterior queda invalidado.
Probar un webhook
Use el panel para enviar un evento de prueba firmado con su secreto real (Panel → Webhooks → Test), o POST /v1/webhooks/test con su clave secreta. Ambos envían el evento webhook.test, entregado a todos sus endpoints activos sin importar sus suscripciones.
Si se pierde un webhook
- Consulta activa:
POST /v1/payments/{id}/verifyhace que SOAMTAR consulte a Orange Money y vuelva a emitir el evento si la transición se produjo. - Una conciliación automática interna recupera las transacciones bloqueadas: normalmente no tiene que hacer nada.