Solución de problemas
Mi webhook no llega
Diagnóstico
- Panel → Webhooks → Registros: ¿aparece la solicitud?
- No → el evento no se generó. Compruebe que la transacción llegó a
success/failed/cancelled/expired. Fuerce una consulta conPOST/v1/payments/{id}/verify. - Sí, con un
http_statusdistinto de 2xx → su servidor la rechazó. Vea «Mi endpoint responde 4xx/5xx». - Sí, con
error_message: "timeout"→ su servidor tardó más de 15 segundos. Vea «Tiempo de espera agotado».
- No → el evento no se generó. Compruebe que la transacción llegó a
Mi endpoint responde 4xx/5xx
| http_status | Causa habitual |
|---|---|
| 400 | Lectura del cuerpo: su servidor lee el payload antes de verificar la firma |
| 401 / 403 | Un middleware de autenticación bloquea las solicitudes externas: excluya /webhooks/soamtar |
| 404 | Ruta incorrecta: compruebe la URL exacta en el panel |
| 422 | Validación del framework: acepte el payload tal cual |
| 500 | Error de la aplicación: consulte sus registros |
| 502 / 503 | Proxy inverso caído o aplicación no iniciada |
Solución: excluya su ruta de webhook de la protección CSRF (p. ej. withoutMiddleware([VerifyCsrfToken::class]) en Laravel) y de su middleware de autenticación.
Tiempo de espera agotado
Su handler debe responder 2xx en menos de 15 segundos. Para un procesamiento largo:
// Bad
public function webhook(Request $request) {
$this->processOrder($request); // 30 seconds
$this->sendEmail($request);
return response('ok');
}
// Good
public function webhook(Request $request) {
ProcessSoamtarWebhookJob::dispatch($request->all());
return response('ok'); // immediate
}Mi servidor recibe el webhook 5 veces
Es un reintento, algo esperado. Causas:
- Responde 2xx pero tras más de 15 s → SOAMTAR ya registró un tiempo agotado
- Responde con un código distinto de 2xx
- Responde 2xx pero SOAMTAR nunca recibe la respuesta (red)
Solución: elimine duplicados en su lado según el par (event, data.id).
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);Firma de webhook no válida
Síntoma
Su verificación HMAC falla aunque usa el secreto correcto.
Causa 1: cuerpo modificado antes de la verificación
Express, Symfony y Laravel interpretan el JSON automáticamente. Debe verificar la firma sobre el cuerpo en bruto, antes de cualquier interpretación.
// Bad
app.use(express.json()) // parses JSON
app.post('/webhook', (req, res) => {
verify(req.body) // req.body is already an object, not the raw body
})
// Good
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
verify(req.body.toString()) // raw
const event = JSON.parse(req.body.toString())
})Causa 2: codificación del HMAC
Compare hexadecimal en minúsculas en ambos lados (hash_hmac de PHP devuelve minúsculas; compruébelo en Node). Use hash_equals / crypto.timingSafeEqual, no ===.
Causa 3: marca de tiempo caducada
|now - t| > 300s se rechaza. Compruebe el reloj de su servidor (NTP). No amplíe la tolerancia para procesar entregas antiguas: reenvíelas desde Dashboard → Webhook logs → Resend (cada intento se vuelve a firmar con una marca de tiempo nueva) o use la consulta verify.
Conflicto de Idempotency-Key
Síntoma
409 idempotency_conflict al reenviar la solicitud.
Diagnóstico
curl -X POST .../v1/payments \
-H "Idempotency-Key: abc-123" \
-d '{"merchant_ref":"ORDER-7","amount":"150.00","currency":"SLE"}'
# 201
curl -X POST .../v1/payments \
-H "Idempotency-Key: abc-123" \
-d '{"merchant_ref":"ORDER-7","amount":"150.00","currency":"SLE","description":"oops"}'
# 409 — different bodySolución:
- Para reintentar: misma clave y exactamente el mismo cuerpo.
- Para una nueva solicitud: una nueva clave.
Buena práctica
Genere la clave cuando el cliente pulsa «Pagar» y consérvela (almacenamiento local o base de datos) hasta el resultado final. No la regenere en cada reintento HTTP.
Transacción bloqueada en pending
Diagnóstico
curl -X POST https://app.so-amtar.com/v1/payments/{id}/verify \
-H "Authorization: Bearer sk_test_xxx"La consulta verify pregunta a Orange Money y sincroniza el estado. Si Orange Money confirma, la transacción avanza y se le envía un webhook.
Si nada cambia
- El cliente no ha confirmado la solicitud de Orange Money con su PIN (la solicitud queda abierta unos 5 minutos): pídale que revise su teléfono o inicie un nuevo pago.
- Orange Money tiene un incidente: consulte con el soporte.
- La transacción caduca automáticamente en
expires_at(2 horas por defecto). - Si sigue bloqueada: escriba a
support@so-amtar.comcon la referencia SOAMTAR (TXN-...).
Reembolsar un pago Orange Money
Use POST/v1/refunds (clave con el scope can_refund):
- Envíe el
transaction_iddel pago; omitaamountpara reembolsar el importe restante o indíquelo para un reembolso parcial. - El importe se abona al número Orange Money del pagador y se descuenta de su saldo. El reembolso queda en
processinghasta la confirmación de Orange; el resultado llega porrefund.success/refund.failed. - Si recibe
refund_payer_phone_missing, el pago no tiene número de pagador: contacte con el soporte.
Ver Reembolsos (en inglés).
El saldo mostrado no coincide con mis liquidaciones
| Componente | ¿Incluido en el saldo mostrado? |
|---|---|
success aún no liquidado | Sí |
pending | No |
success ya liquidado | No |
| Reembolso o pago saliente emitido | Sí (deducido) |
| Contracargo abierto | Reservado en un saldo aparte |
El saldo «disponible» es distinto del saldo «contable»; el panel muestra ambos.
Una clave live no encuentra mi objeto
Está usando una clave live sobre un objeto creado en sandbox (o al revés). Use la clave del modo correspondiente: los identificadores dependen estrictamente del modo.
Problemas de conexión a la API (SSL, tiempo de espera…)
Error SSL: SSL certificate problem: unable to get local issuer certificate
Suele ocurrir en servidores Windows o en instalaciones antiguas de PHP/cURL sin un paquete de certificados CA actualizado.
Solución:
- Descargue el
cacert.pemoficial desde curl.se/ca/cacert.pem. - Apunte
php.inihacia él:inicurl.cainfo = "C:/chemin/vers/votre/cacert.pem" openssl.cafile = "C:/chemin/vers/votre/cacert.pem" - Reinicie su servidor web.
Error: Connection Timeout
Si su servidor no puede llegar a app.so-amtar.com:
- Cortafuegos: su servidor debe poder hacer solicitudes salientes en el puerto 443 (HTTPS).
- DNS: compruebe que
app.so-amtar.comse resuelve desde su servidor. - Proxy: si su servidor usa un proxy, configure el SDK en consecuencia (o las variables de entorno
HTTP_PROXY).
Error: 401 Unauthorized (clave no válida)
- Use la clave secreta (
sk_...) para las llamadas desde el backend, no la clave pública (pk_...). - Compruebe el modo: una clave
sk_test_solo sirve para los objetos de sandbox. - Asegúrese de no haber copiado espacios ni saltos de línea con la clave.
Para saber más
- Soporte:
support@so-amtar.com