IQIraQPayAPI Reference

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.

WhereValue
X-IraQPay-TimestampUnix seconds when the callback was sent.
X-IraQPay-Signaturehex( 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
}
NameDescription
amountFinal approved amount — this is the value you should credit. May differ from requestedAmount.
statussuccessful | unsuccessful. (pending is never sent as a callback.)
statusReasonWhy 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 2xx within 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.