Retry policy
3 automatic retries: immediate, +30 s, +5 min. Beyond: new attempt at +30 min, +2 h, +6 h, +24 h. After 24 h with no 2xx: forwarded to the dead letter queue accessible from the dashboard.
Inopay emits HMAC SHA-256 signed webhooks to notify you of key events. All webhooks are retried with exponential backoff up to 24 h, then sent to the dead letter queue.
Events emitted by Inopay. You pick the subscriptions when registering an endpoint via POST /v1/webhooks/endpoints.
| Event | Description |
|---|---|
| order.created | Order validated by Inopay (before SGI transmission). |
| order.routed | Order forwarded to the partner SGI. |
| order.executed | Order fully or partially executed on exchange. |
| order.failed | Execution failure (SGI rejection, insufficient funds, suspended instrument). |
| kyc.attested | New KYC attestation issued and signed. |
| kyc.revoked | KYC attestation revoked (added to the CRL). |
| audit.snapshot | Daily Merkle snapshot published and anchored. |
| webhook.test | Test event manually triggered from the dashboard. |
All payloads share the same envelope: id, type, created_at, data, livemode. Sample for order.executed:
POST https://your-app.example.com/webhooks/inopay HTTP/1.1
Content-Type: application/json
X-Inopay-Signature: t=1764082380,v1=4f5a9b2c8e1d3f6a7b9c0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b
X-Inopay-Idempotency-Key: 6c4b0b6e-1c2a-4d09-9b3a-7d1f4e5a6c2d
User-Agent: Inopay-Webhooks/1.0
{
"id": "evt_2A9p3X7q",
"type": "order.executed",
"created_at": "2026-04-25T14:32:14.001Z",
"livemode": true,
"data": {
"order_id": "ord_9Pk2X",
"rcpt_to": "sgi_partner_001",
"instrument": "SNTS.BRVM",
"side": "buy",
"filled_qty": 10,
"average_fill_price_cents": 1248750,
"executed_at": "2026-04-25T14:32:13.880Z",
"exchange_ref": "BRVM-2026-04-25-XK4287"
}
}Header X-Inopay-Signature: t=<timestamp>,v1=<hex>. Concatenate timestamp + '.' + raw_body, compute HMAC SHA-256 with your webhook secret, then compare in constant time.
// Verify Inopay webhook signature (Node.js >= 20)
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyInopaySignature(
rawBody: string,
header: string,
secret: string,
toleranceSeconds = 300,
): boolean {
// Header format: "t=<unix_ts>,v1=<hex>"
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=') as [string, string]),
);
const ts = Number(parts.t);
if (!Number.isFinite(ts)) return false;
if (Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}# Verify Inopay webhook signature (Python 3.10+)
import hmac, hashlib, time
def verify_inopay_signature(
raw_body: bytes,
header: str,
secret: str,
tolerance_seconds: int = 300,
) -> bool:
# Header format: "t=<unix_ts>,v1=<hex>"
parts = dict(p.split("=", 1) for p in header.split(","))
try:
ts = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - ts) > tolerance_seconds:
return False
signed = f"{parts['t']}.{raw_body.decode()}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))// Verify Inopay webhook signature (Go 1.21+)
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
func VerifyInopaySignature(rawBody []byte, header, secret string, tolerance time.Duration) bool {
parts := map[string]string{}
for _, kv := range strings.Split(header, ",") {
if idx := strings.IndexByte(kv, '='); idx > 0 {
parts[kv[:idx]] = kv[idx+1:]
}
}
ts, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil { return false }
if d := time.Since(time.Unix(ts, 0)); d > tolerance || d < -tolerance {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(parts["t"] + "." + string(rawBody)))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(parts["v1"]))
}3 automatic retries: immediate, +30 s, +5 min. Beyond: new attempt at +30 min, +2 h, +6 h, +24 h. After 24 h with no 2xx: forwarded to the dead letter queue accessible from the dashboard.
Inopay may resend the same event multiple times (network, retry). Store the payload id on your side and check before re-processing. The X-Inopay-Idempotency-Key header is also present.
Manually trigger a test event from the dashboard or via the CLI:
# Trigger a test event from the dashboard or CLI
inopay-cli webhook test \
--endpoint=ep_4Xk9R \
--event=order.created \
--livemode=false
# Or from the dashboard:
# Settings → Webhooks → endpoint → "Send test event"