Webhooks
Every meaningful change on your cases is POSTed to your registered HTTPS endpoint as a signed event, with automatic retries. This page is the complete receiver contract.
The delivery
POST <your-callback-url>
Content-Type: application/json
X-Quikkred-Event: payment.collected
X-Quikkred-Delivery: evt_9f2c… ← dedupe key
X-Quikkred-Timestamp: 1753260000000
X-Quikkred-Signature: hex(HMAC-SHA256("POST\n<your-path>\n<ts>\n<raw-body>", hmacSecret))
Body envelope: see the event reference.
The five receiver rules
- Verify the signature over the exact raw bytes before trusting anything
— same scheme as request signing, with
your callback path as
PATH. Constant-time compare. - Respond
2xxfast (<10s). Queue heavy processing. Non-2xx or timeout → retry ladder0s → 30s → 2m → 10m → 1h → 6h → 24h, then parked (replayable 30 days) and your technical contact alerted. - Dedupe on
eventId— delivery is at-least-once; retries and replays are normal. - Order by
sequence, not arrival — apply only if greater than the last applied for that loan; on a gap,GET /cases/{loan}for truth. - Use absolute values from payloads (
totalCollected,outstandingAfter) — never accumulate deltas.
If your endpoint goes down
After ~20 consecutive failures we suspend delivery (attempts aren't wasted), alert your technical contact, and probe every 15 minutes. On recovery the backlog drains automatically in per-case sequence order. Nothing is lost.
Reference receiver (Node/Express)
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = process.env.QK_SECRET;
const PATH = "/webhooks/quikkred";
app.use(PATH, express.raw({ type: "application/json", limit: "1mb" })); // RAW body!
function verify(req) {
const ts = req.header("x-quikkred-timestamp");
const sig = req.header("x-quikkred-signature");
if (!ts || !sig) return false;
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;
const expected = crypto.createHmac("sha256", SECRET)
.update(`POST\n${PATH}\n${ts}\n${req.body.toString("utf8")}`).digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post(PATH, async (req, res) => {
if (!verify(req)) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
const isNew = await db.insertIfAbsent("events", event.eventId, event); // rule 3
if (!isNew) return res.json({ duplicate: true });
res.json({ received: true }); // rule 2 — ack now
queue.push(async () => { // rules 4 & 5
const last = await db.lastSequence(event.sourceLoanNumber);
if (event.sequence <= last) return;
if (event.sequence > last + 1) return refetchCase(event.sourceLoanNumber);
await applyEvent(event);
await db.setLastSequence(event.sourceLoanNumber, event.sequence);
});
});
Python/Flask (verification core)
import hashlib, hmac, time
from flask import Flask, request
app = Flask(__name__)
SECRET = b"..."; PATH = "/webhooks/quikkred"
@app.post(PATH)
def hook():
ts = request.headers.get("X-Quikkred-Timestamp", "")
sig = request.headers.get("X-Quikkred-Signature", "")
if abs(time.time() * 1000 - float(ts or 0)) > 300_000:
return "", 401
canonical = f"POST\n{PATH}\n{ts}\n".encode() + request.get_data()
expected = hmac.new(SECRET, canonical, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return "", 401
# dedupe on eventId, ack fast, apply by sequence — as in the Node example
return {"received": True}
Test any deployment instantly with POST /api/v1/partner/webhooks/test — a
signed ping fires at your endpoint and the delivery result returns
synchronously.