INOPAY
Retour aux développeurs

Webhooks HMAC SHA-256

Inopay émet des webhooks signés HMAC SHA-256 pour vous notifier des événements clés. Tous les webhooks sont retentés avec backoff exponentiel jusqu'à 24 h, puis envoyés en dead letter queue.

Événements disponibles

Liste des événements émis par Inopay. Vous choisissez les abonnements lors de l'enregistrement de l'endpoint via POST /v1/webhooks/endpoints.

ÉvénementDescription
order.createdOrdre validé côté Inopay (avant transmission SGI).
order.routedOrdre transmis à la SGI partenaire.
order.executedOrdre exécuté entièrement ou partiellement en bourse.
order.failedÉchec d'exécution (rejet SGI, fonds insuffisants, instrument suspendu).
kyc.attestedNouvelle attestation KYC émise et signée.
kyc.revokedAttestation KYC révoquée (ajout à la CRL).
audit.snapshotSnapshot Merkle quotidien publié et ancré.
webhook.testÉvénement de test déclenché manuellement depuis le dashboard.

Format du payload

Tous les payloads suivent la même enveloppe : id, type, created_at, data, livemode. Exemple pour 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"
  }
}

Signature HMAC SHA-256

Header X-Inopay-Signature: t=<timestamp>,v1=<hex>. Concaténez timestamp + '.' + raw_body, calculez le HMAC SHA-256 avec votre secret webhook, puis comparez en temps constant.

Node.js
// 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);
}
Python
# 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", ""))
Go
// 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"]))
}

Politique de retry

3 retries automatiques : immédiat, +30 s, +5 min. Au-delà : nouvelle tentative à +30 min, +2 h, +6 h, +24 h. Après 24 h sans 2xx : envoi en dead letter queue accessible via le dashboard.

Idempotence côté receiver

Inopay peut renvoyer le même événement plusieurs fois (réseau, retry). Stockez l'id du payload côté receiver et vérifiez avant de re-traiter. Le header X-Inopay-Idempotency-Key est également présent.

Mode test

Déclenchez manuellement un événement test depuis le dashboard ou via la 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"