故障排查
收不到 Webhook
诊断
- 控制台 → Webhooks → 日志:列表中有这条请求吗?
- 没有 → 事件未触发。请确认交易已到达
success/failed/cancelled/expired。可通过POST/v1/payments/{id}/verify强制查询。 - 有,但
http_status不是 2xx → 您的服务器拒绝了请求。参见 “我的端点返回 4xx/5xx”。 - 有,且
error_message: "timeout"→ 您的服务器响应超过 15 秒。参见 “超时”。
- 没有 → 事件未触发。请确认交易已到达
我的端点返回 4xx/5xx
| http_status | 常见原因 |
|---|---|
| 400 | 请求体解析:您的服务器在验证签名前读取了 payload |
| 401 / 403 | 认证中间件拦截了外部请求:请将 /webhooks/soamtar 排除 |
| 404 | 路由错误:请在控制台中核对准确的 URL |
| 422 | 框架校验:请按原样接受 payload |
| 500 | 应用错误:请查看您的日志 |
| 502 / 503 | 反向代理宕机或应用未启动 |
解决方法:将您的 Webhook 路由从 CSRF 保护(例如 Laravel 中的 withoutMiddleware([VerifyCsrfToken::class]))和认证中间件中排除。
超时
您的处理程序必须在 15 秒内返回 2xx。如果处理耗时较长:
// 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
}我的服务器收到同一个 Webhook 5 次
这是重试,属于正常现象。原因:
- 您返回了 2xx,但耗时超过 15 秒 → SOAMTAR 已记录超时
- 您返回了非 2xx 状态码
- 您返回了 2xx,但 SOAMTAR 未收到响应(网络问题)
解决方法:在您这一端按 (event, data.id) 组合去重。
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);Webhook 签名无效
症状
尽管使用了正确的密钥,您的 HMAC 验证仍然失败。
原因 1:验证前请求体已被修改
Express、Symfony 和 Laravel 会自动解析 JSON。您必须在任何解析之前,对原始请求体验证签名。
// 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())
})原因 2:HMAC 编码
两端都应比较小写十六进制(PHP 的 hash_hmac 返回小写;请在 Node 中确认)。使用 hash_equals / crypto.timingSafeEqual,而不是 ===。
原因 3:时间戳过期
|now - t| > 300s 会被拒绝。请检查服务器时钟(NTP)。不要为了处理旧的投递而放宽容差:请在 Dashboard → Webhook logs → Resend 中重新发送(每次尝试都会用新的时间戳重新签名),或改用 verify 查询。
Idempotency-Key 冲突
症状
重新发送请求时返回 409 idempotency_conflict。
诊断
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 body解决方法:
- 重试:使用相同的密钥和完全相同的请求体。
- 新请求:使用新的密钥。
最佳实践
在客户点击“支付”时生成密钥,并持久保存(本地存储或数据库),直到得到最终结果。不要在每次 HTTP 重试时重新生成。
交易卡在 pending
诊断
curl -X POST https://app.so-amtar.com/v1/payments/{id}/verify \
-H "Authorization: Bearer sk_test_xxx"verify 查询会向 Orange Money 询问并同步状态。如果 Orange Money 确认,交易会继续推进,并向您发送 Webhook。
如果没有任何变化
- 客户尚未用 PIN 确认 Orange Money 请求(该请求约保持 5 分钟):请客户检查手机,或发起新的付款。
- Orange Money 出现故障:请咨询客服。
- 交易会在
expires_at自动过期(默认 2 小时)。 - 仍然卡住:请携带 SOAMTAR 参考号(
TXN-...)发送邮件至support@so-amtar.com。
为 Orange Money 付款退款
使用 POST/v1/refunds(密钥需具备 can_refund scope):
- 传入付款的
transaction_id;不传amount则退还剩余全部金额,传入则为部分退款。 - 款项会退回到付款人的 Orange Money 号码并从您的余额中扣除。退款在 Orange 确认前保持
processing状态;结果通过refund.success/refund.failed通知。 - 如果收到
refund_payer_phone_missing,说明该付款没有付款人号码:请联系支持团队。
参见退款(英文)。
显示的余额与结算不一致
| 组成部分 | 是否计入显示余额? |
|---|---|
尚未结算的 success | 是 |
pending | 否 |
已结算的 success | 否 |
| 已发出的退款或出款 | 是(扣减) |
| 未结的拒付 | 冻结在单独的余额中 |
“可用”余额不同于“账面”余额;控制台会同时显示两者。
live 密钥找不到我的对象
您在用 live 密钥访问沙盒中创建的对象(或反之)。请使用对应模式的密钥:id 严格按模式区分。
API 连接问题(SSL、超时等)
SSL 错误:SSL certificate problem: unable to get local issuer certificate
通常出现在 Windows 服务器或缺少最新 CA 证书包的旧版 PHP/cURL 安装中。
解决方法:
- 从 curl.se/ca/cacert.pem 下载官方
cacert.pem。 - 在
php.ini中指向它:inicurl.cainfo = "C:/chemin/vers/votre/cacert.pem" openssl.cafile = "C:/chemin/vers/votre/cacert.pem" - 重启 Web 服务器。
错误:Connection Timeout
如果您的服务器无法访问 app.so-amtar.com:
- 防火墙:您的服务器必须允许在 443 端口(HTTPS) 发起出站请求。
- DNS:确认服务器能解析
app.so-amtar.com。 - 代理:如果服务器使用代理,请相应配置 SDK(或
HTTP_PROXY环境变量)。
错误:401 Unauthorized(密钥无效)
- 后端调用请使用私钥(
sk_...),而不是公钥(pk_...)。 - 检查模式:
sk_test_密钥只能访问沙盒对象。 - 确认复制密钥时没有带上空格或换行。
了解更多
- 客服:
support@so-amtar.com