Real-time notifications for authorization events. Signed with HMAC-SHA256 + timestamped so you can reject replays. Retried with exponential backoff. Idempotent by delivery ID.
When something happens on your tenant (an authorization is approved, captured, etc.), we enqueue a delivery to every subscription that’s active and listening for that event type. A background worker picks them up and POSTs to your URL with:
POST https://your-app.example.com/webhooks/rmo
Content-Type: application/json
X-RMO-Signature: sha256=<hex digest>
X-RMO-Timestamp: <unix seconds>
X-RMO-Event: authorization.approved
X-RMO-Delivery: <17-char delivery RecordId>
Idempotency-Key: <event-type>:<source idempotency-key>
User-Agent: rmo-network-webhooks/1.0
{
"id": "7421589",
"recordId": "A1b2C3d4E5f6G7h8I",
"amount": 42.50,
"currency": "USD",
"approvalCode": "A7B2C3",
"card": "<card record id>",
"merchant": "<merchant record id>",
"channel": "POS"
}
authorization.captured notification.
The signature is HMAC-SHA256 over timestamp + "." + body, using your subscription’s signing secret (the whsec_… returned at create / rotate time) as the HMAC key.
sk_* bearer authenticates your requests to RMO. The whsec_* subscription secret authenticates RMO’s deliveries to you. They’re different keys.
import hmac, hashlib, time, os from flask import Flask, request, abort SUBSCRIPTION_SECRET = os.environ["RMO_WEBHOOK_SECRET"].encode() MAX_DRIFT_SECONDS = 300 app = Flask(__name__) @app.post("/webhooks/rmo") def rmo_webhook(): ts = request.headers.get("X-RMO-Timestamp", "") sig = request.headers.get("X-RMO-Signature", "") body = request.get_data() # 1. Reject if missing if not ts or not sig.startswith("sha256="): abort(400) # 2. Reject if drift > 5 minutes (anti-replay) if abs(time.time() - int(ts)) > MAX_DRIFT_SECONDS: abort(400) # 3. Recompute and compare in constant time payload = ts.encode() + b"." + body expected = hmac.new(SUBSCRIPTION_SECRET, payload, hashlib.sha256).hexdigest() provided = sig.split("=", 1)[1] if not hmac.compare_digest(expected, provided): abort(401) # 4. Idempotency: skip if X-RMO-Delivery already seen delivery_id = request.headers["X-RMO-Delivery"] if already_processed(delivery_id): return "", 200 # 5. Process event = request.headers["X-RMO-Event"] data = request.get_json() handle_event(event, data) mark_processed(delivery_id) return "", 200
const express = require("express"); const crypto = require("crypto"); const app = express(); const SECRET = process.env.RMO_WEBHOOK_SECRET; const MAX_DRIFT = 300; // IMPORTANT: capture raw body for signing app.use("/webhooks/rmo", express.raw({ type: "application/json" })); app.post("/webhooks/rmo", (req, res) => { const ts = req.get("X-RMO-Timestamp") || ""; const sig = req.get("X-RMO-Signature") || ""; if (!ts || !sig.startsWith("sha256=")) return res.sendStatus(400); if (Math.abs(Date.now()/1000 - parseInt(ts)) > MAX_DRIFT) { return res.sendStatus(400); } const payload = Buffer.concat([Buffer.from(ts + "."), req.body]); const expected = crypto.createHmac("sha256", SECRET).update(payload).digest("hex"); const provided = sig.slice(7); const ok = expected.length === provided.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided)); if (!ok) return res.sendStatus(401); const deliveryId = req.get("X-RMO-Delivery"); if (alreadyProcessed(deliveryId)) return res.sendStatus(200); handleEvent(req.get("X-RMO-Event"), JSON.parse(req.body)); markProcessed(deliveryId); res.sendStatus(200); });
Subscribe to one or more of:
| Event | Fires when |
|---|---|
| authorization.approved | A new card auth or payment-code redemption is approved. |
| authorization.declined | An auth is declined for any reason — limit, expired card, wrong PIN, etc. |
| authorization.captured | A capture (partial or full) is applied to an approved auth. |
| authorization.reversed | A reverse / refund (partial or full) is applied. |
If your endpoint returns a non-2xx or times out (10s connect+read), we retry on this schedule:
| Attempt | Delay |
|---|---|
| 1 (initial) | immediate |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +12 hours |
| 7 (last) | +24 hours |
After 7 attempts the delivery is marked Webhook Retry Exhausted. After 50 consecutive failures on a subscription, the subscription itself is auto-paused — you’ll need to investigate and reactivate it via PATCH /v1/webhook-subscriptions/<id> {"status":"active"}.
200 within a few seconds; do the heavy work asynchronously (queue + worker). Long handlers cause retries.X-RMO-Delivery./v1/authorizations with sandbox creds and observing your endpoint.GET /v1/webhook-subscriptions/<id>/deliveries shows the last N attempts — status, response code, error string. Indispensable when debugging.