Callbacks
After a deposit or withdraw reaches a final state we POST a JSON status update to the Callback URL configured for your site. We may send more than one callback for a transaction (for example a correction after a system issue), so process each (transactionId, status) pair exactly once.
Verification
Two signatures are included. Verify at least the header signature; it covers the whole body including status.
| Where | Value |
|---|---|
| X-IraQPay-Timestamp | Unix seconds when the callback was sent. |
| X-IraQPay-Signature | hex( HMAC-SHA256( appSecret, timestamp + "." + rawBody ) ). Reject if older than 5 minutes. |
| hash (body) | base64( HMAC-SHA256( appSecret, transactionId + bankId + amount ) ) where amount is the JSON number exactly as rendered (25000, 25000.5). DeusaPay-compatible. |
import { createHmac, timingSafeEqual } from "node:crypto";
app.post("/payments/callback/iraqpay", express.text({ type: "*/*" }), (req, res) => {
const ts = req.header("x-iraqpay-timestamp"), sig = req.header("x-iraqpay-signature");
const expected = createHmac("sha256", APP_SECRET).update(`${ts}.${req.body}`).digest("hex");
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300 || !timingSafeEqual(Buffer.from(sig ?? ""), Buffer.from(expected))) return res.status(401).end();
const cb = JSON.parse(req.body);
// idempotency: skip if (cb.transactionId, cb.status) already processed
if (cb.type === "deposit" && cb.status === "successful") creditUser(cb.userId, cb.amount, cb.processId);
if (cb.type === "withdrawal" && cb.status === "unsuccessful") refundUser(cb.userId, cb.amount, cb.processId);
res.json({ ok: true });
});
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_IRAQPAY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_IRAQPAY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $APP_SECRET);
if (abs(time() - (int)$ts) > 300 || !hash_equals($expected, $sig)) { http_response_code(401); exit; }
$cb = json_decode($raw, true);
// process once per (transactionId, status) …
echo json_encode(['ok' => true]);
Deposit callback payload
{
"hash": "zzunnCrv6Sb38TU/dPYIl+9TKd8gT6iqrcxv+V32AFs=",
"transactionId": "cmv2lqvyn000clzqua7arrfxj",
"bankId": "cmv2lm9yb00017pn0fvwxdb7z", "bank": "FIB",
"amount": 25000,
"type": "deposit",
"bankAccountName": "Ali Hassan Kareem", "bankAccountIban": "07701234567", "identifierKind": "PHONE",
"status": "successful", "statusReason": null,
"name": "Ahmed Al-Jubouri", "userName": "ahmed77", "userId": "u-42", "processId": "ORDER-1001",
"convertedName": "ahmedaljubouri", "requestedAmount": 25000
}
| Name | Description |
|---|---|
| amount | Final approved amount — this is the value you should credit. May differ from requestedAmount. |
| status | successful | unsuccessful. (pending is never sent as a callback.) |
| statusReason | Why it failed (expired, operator note…) when unsuccessful. |
Withdraw callback payload
{
"hash": "…", "transactionId": "cmv2mdjam0003xvpi9u57bev7",
"bankId": "cmv2lm9yb00017pn0fvwxdb7z", "bank": "FIB",
"amount": 20000, "type": "withdrawal",
"accountName": "Ahmed Al-Jubouri", "iban": "07709998877", "identifierKind": "PHONE",
"status": "successful", "statusReason": null,
"name": "Ahmed Al-Jubouri", "userName": "ahmed77", "userId": "u-42", "processId": "WD-2001", "convertedName": "ahmedaljubouri"
}
When a withdraw callback says
unsuccessful you are required to return the funds to your user — the payout did not happen.Delivery & retries
- Respond with HTTP
2xxwithin 15 seconds. Any body is fine ({"ok":true}recommended). - Non-2xx or timeout → we retry after 1, 5, 30, 120 and 720 minutes. Failed deliveries are visible to operators and can be re-sent manually.
- Callbacks come from IraQPay's cloud egress; IPs are not fixed — rely on the signature, not on IP filtering.