Troubleshooting
My webhook does not arrive
Diagnosis
- Dashboard → Webhooks → Logs: is the request listed?
- No → the event was not triggered. Check that the transaction reached
success/failed/cancelled/expired. Force a pull withPOST /v1/payments/{id}/verify. - Yes, with a non-2xx
http_status→ your server refused it. See “My endpoint returns 4xx/5xx”. - Yes, with
error_message: "timeout"→ your server took longer than 15 seconds. See “Timeout”.
- No → the event was not triggered. Check that the transaction reached
My endpoint returns 4xx/5xx
| http_status | Typical cause |
|---|---|
| 400 | Body parsing: your server reads the payload before verifying the signature |
| 401 / 403 | An auth middleware blocks external requests — exempt /webhooks/soamtar |
| 404 | Wrong route — check the exact URL in the dashboard |
| 422 | Framework validation — accept the payload as is |
| 500 | Application error — check your logs |
| 502 / 503 | Reverse proxy down or app not started |
Fix: exclude your webhook route from CSRF protection (e.g. withoutMiddleware([VerifyCsrfToken::class]) in Laravel) and from your auth middleware.
Timeout
Your handler must answer 2xx in less than 15 seconds. For long processing:
// 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
}My server receives the webhook 5 times
That is a retry, which is expected. Causes:
- You answer 2xx but after more than 15 s → SOAMTAR already recorded a timeout
- You answer non-2xx
- You answer 2xx but SOAMTAR never receives the response (network)
Fix: de-duplicate on your side on the pair (event, data.id).
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);Invalid webhook signature
Symptom
Your HMAC check fails although you use the right secret.
Cause 1: body modified before verification
Express, Symfony and Laravel parse JSON automatically. You must verify the signature on the raw body, before any parsing.
// 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())
})Cause 2: HMAC encoding
Compare lowercase hex on both sides (PHP hash_hmac returns lowercase; check Node). Use hash_equals / crypto.timingSafeEqual, not ===.
Cause 3: expired timestamp
|now - t| > 300s is rejected. Check your server clock (NTP). If you are replaying old webhooks by hand, raise the tolerance or use the verify pull instead.
Idempotency-Key conflict
Symptom
409 idempotency_conflict when you resend the request.
Diagnosis
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 bodyFix:
- To retry: same key, exactly the same body.
- For a new request: a new key.
Best practice
Generate the key when the customer taps "Pay" and persist it (local storage / database) until the final outcome. Do not regenerate it on each HTTP retry.
Transaction stuck in pending
Diagnosis
curl -X POST https://app.so-amtar.com/v1/payments/{id}/verify \
-H "Authorization: Bearer sk_test_xxx"The verify pull queries Orange Money and synchronises the status. If Orange Money confirms, the transaction moves on and a webhook is sent to you.
If nothing changes
- The customer has not confirmed the Orange Money prompt with their PIN (the prompt stays open for about 5 minutes): ask them to check their phone, or start a new payment.
- Orange Money is having an incident: check with support.
- The transaction expires automatically at
expires_at(2 hours by default). - Still stuck: email
support@so-amtar.comwith the SOAMTAR reference (TXN-...).
Refunding an Orange Money payment
Use POST /v1/refunds with the payment's transaction_id (see Refunds). If the call fails:
| Error code | Cause | Fix |
|---|---|---|
scope_insufficient (403) | The API key lacks can_refund | Enable the scope on the key in the dashboard |
refunds_disabled | Refunds are not enabled for your account | Contact support |
transaction_not_refundable | The payment is not success (or already refunded) | Only refund successful payments |
refund_environment_mismatch | Test key on a live payment, or the reverse | Use the key of the payment's environment |
refund_payer_phone_missing | The payment has no payer phone to credit | Refund the customer outside SOAMTAR |
refund_exceeds_refundable / transaction_already_fully_refunded | Amount above what remains refundable | Lower amount or stop |
A refund in processing is waiting for Orange Money to confirm the credit; the final status arrives as refund.success or refund.failed (check failure_reason). You can also poll GET /v1/refunds/{id}.
The displayed balance does not match my settlements
| Component | Included in the displayed balance? |
|---|---|
success not yet settled | Yes |
pending | No |
success already settled | No |
| Refund or payout issued | Yes (deducted) |
| Open chargeback | Reserved on a separate balance |
The "available" balance differs from the "ledger" balance; the dashboard shows both.
A live key cannot find my object
You are using a live key on an object created in the sandbox (or the reverse). Use the key of the matching mode: ids are strictly scoped by mode.
API connection problems (SSL, timeout…)
SSL error: SSL certificate problem: unable to get local issuer certificate
Usually on Windows servers or old PHP/cURL installs without an up-to-date CA bundle.
Fix:
- Download the official
cacert.pemfrom curl.se/ca/cacert.pem. - Point
php.inito it:inicurl.cainfo = "C:/path/to/cacert.pem" openssl.cafile = "C:/path/to/cacert.pem" - Restart your web server.
Error: Connection Timeout
If your server cannot reach app.so-amtar.com:
- Firewall: your server must be allowed outbound requests on port 443 (HTTPS).
- DNS: check that
app.so-amtar.comresolves from your server. - Proxy: if your server uses a proxy, configure the SDK accordingly (or the
HTTP_PROXYenvironment variables).
Error: 401 Unauthorized (invalid key)
- Use the secret key (
sk_...) for backend calls, not the public key (pk_...). - Check the mode: an
sk_test_key only works for sandbox objects. - Make sure no space or line break was copied with the key.
Going further
- Support:
support@so-amtar.com