Resolução de problemas
O meu webhook não chega
Diagnóstico
- Painel → Webhooks → Registos: o pedido aparece na lista?
- Não → o evento não foi gerado. Confirme que a transação chegou a
success/failed/cancelled/expired. Force uma consulta comPOST/v1/payments/{id}/verify. - Sim, com um
http_statusdiferente de 2xx → o seu servidor recusou-o. Veja «O meu endpoint responde 4xx/5xx». - Sim, com
error_message: "timeout"→ o seu servidor demorou mais de 15 segundos. Veja «Tempo de espera esgotado».
- Não → o evento não foi gerado. Confirme que a transação chegou a
O meu endpoint responde 4xx/5xx
| http_status | Causa habitual |
|---|---|
| 400 | Leitura do corpo: o seu servidor lê o payload antes de verificar a assinatura |
| 401 / 403 | Um middleware de autenticação bloqueia os pedidos externos: exclua /webhooks/soamtar |
| 404 | Rota errada: confirme o URL exato no painel |
| 422 | Validação do framework: aceite o payload tal como está |
| 500 | Erro da aplicação: consulte os seus registos |
| 502 / 503 | Proxy inverso em baixo ou aplicação não iniciada |
Solução: exclua a sua rota de webhook da proteção CSRF (por ex. withoutMiddleware([VerifyCsrfToken::class]) em Laravel) e do seu middleware de autenticação.
Tempo de espera esgotado
O seu handler deve responder 2xx em menos de 15 segundos. Para um processamento longo:
// 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
}O meu servidor recebe o webhook 5 vezes
É uma repetição, o que é esperado. Causas:
- Responde 2xx mas após mais de 15 s → a SOAMTAR já registou um tempo esgotado
- Responde com um código diferente de 2xx
- Responde 2xx mas a SOAMTAR nunca recebe a resposta (rede)
Solução: elimine duplicados do seu lado segundo o par (event, data.id).
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);Assinatura de webhook inválida
Sintoma
A sua verificação HMAC falha apesar de usar o segredo correto.
Causa 1: corpo alterado antes da verificação
Express, Symfony e Laravel interpretam o JSON automaticamente. Tem de verificar a assinatura sobre o corpo em bruto, antes de qualquer interpretação.
// 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: codificação do HMAC
Compare hexadecimal em minúsculas dos dois lados (o hash_hmac do PHP devolve minúsculas; verifique no Node). Use hash_equals / crypto.timingSafeEqual, não ===.
Causa 3: carimbo temporal expirado
|now - t| > 300s é recusado. Verifique o relógio do seu servidor (NTP). Não alargue a tolerância para processar entregas antigas: reenvie-as a partir de Dashboard → Webhook logs → Resend (cada tentativa é assinada de novo com um carimbo temporal recente) ou use a consulta verify.
Conflito de Idempotency-Key
Sintoma
409 idempotency_conflict ao reenviar o pedido.
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 bodySolução:
- Para repetir: mesma chave e exatamente o mesmo corpo.
- Para um novo pedido: uma nova chave.
Boa prática
Gere a chave quando o cliente toca em «Pagar» e guarde-a (armazenamento local ou base de dados) até ao resultado final. Não a gere de novo a cada repetição HTTP.
Transação bloqueada em pending
Diagnóstico
curl -X POST https://app.so-amtar.com/v1/payments/{id}/verify \
-H "Authorization: Bearer sk_test_xxx"A consulta verify pergunta ao Orange Money e sincroniza o estado. Se o Orange Money confirmar, a transação avança e é-lhe enviado um webhook.
Se nada mudar
- O cliente não confirmou o pedido do Orange Money com o seu PIN (o pedido fica aberto cerca de 5 minutos): peça-lhe que verifique o telemóvel ou inicie um novo pagamento.
- O Orange Money tem um incidente: consulte o suporte.
- A transação expira automaticamente em
expires_at(2 horas por predefinição). - Se continuar bloqueada: escreva para
support@so-amtar.comcom a referência SOAMTAR (TXN-...).
Reembolsar um pagamento Orange Money
Use POST/v1/refunds (chave com o scope can_refund):
- Envie o
transaction_iddo pagamento; omitaamountpara reembolsar o montante restante ou indique-o para um reembolso parcial. - O montante é creditado no número Orange Money do pagador e debitado do seu saldo. O reembolso fica em
processingaté à confirmação da Orange; o resultado chega porrefund.success/refund.failed. - Se receber
refund_payer_phone_missing, o pagamento não tem número de pagador: contacte o suporte.
Ver Reembolsos (em inglês).
O saldo mostrado não corresponde às minhas liquidações
| Componente | Incluído no saldo mostrado? |
|---|---|
success ainda não liquidado | Sim |
pending | Não |
success já liquidado | Não |
| Reembolso ou pagamento de saída emitido | Sim (deduzido) |
| Estorno em aberto | Reservado num saldo à parte |
O saldo «disponível» é diferente do saldo «contabilístico»; o painel mostra ambos.
Uma chave live não encontra o meu objeto
Está a usar uma chave live num objeto criado no sandbox (ou o inverso). Use a chave do modo correspondente: os identificadores dependem estritamente do modo.
Problemas de ligação à API (SSL, tempo esgotado…)
Erro SSL: SSL certificate problem: unable to get local issuer certificate
Surge normalmente em servidores Windows ou instalações antigas de PHP/cURL sem um pacote de certificados CA atualizado.
Solução:
- Descarregue o
cacert.pemoficial em curl.se/ca/cacert.pem. - Aponte o
php.inipara ele:inicurl.cainfo = "C:/chemin/vers/votre/cacert.pem" openssl.cafile = "C:/chemin/vers/votre/cacert.pem" - Reinicie o seu servidor web.
Erro: Connection Timeout
Se o seu servidor não consegue chegar a app.so-amtar.com:
- Firewall: o seu servidor tem de poder fazer pedidos de saída na porta 443 (HTTPS).
- DNS: confirme que
app.so-amtar.comé resolvido a partir do seu servidor. - Proxy: se o seu servidor usa um proxy, configure o SDK em conformidade (ou as variáveis de ambiente
HTTP_PROXY).
Erro: 401 Unauthorized (chave inválida)
- Use a chave secreta (
sk_...) nas chamadas a partir do backend, não a chave pública (pk_...). - Confirme o modo: uma chave
sk_test_só funciona com objetos do sandbox. - Garanta que não copiou espaços nem quebras de linha com a chave.
Saber mais
- Suporte:
support@so-amtar.com