Webhooks
Webhooks push events to your server so you don't have to poll. Register an HTTPS
endpoint and MirrorMingo POSTs a signed JSON payload when something happens.
Events
| Event | Fires when |
|---|---|
mock_attempt.submitted | A learner submits a mock attempt |
mock_attempt.scored | Scoring for an attempt completes |
exam.content_updated | An exam's papers or specification are refreshed |
Register an endpoint
From Account → Developer → Webhooks, or:
POST /v1/developer/webhooks
{
"url": "https://yourapp.com/hooks/mirrormingo",
"events": ["mock_attempt.scored"],
"description": "Update student dashboard on scoring"
}
The response includes a signing secret (whsec_...) shown only once. Store it.
Payload
{
"event": "mock_attempt.scored",
"created": 1717689600,
"data": { "attempt_id": "att_...", "exam_key": "jamb", "score": 78 }
}
Verifying signatures
Every delivery carries a MirrorMingo-Signature header:
MirrorMingo-Signature: t=1717689600,v1=4f6c...e2
Recompute HMAC_SHA256(secret, "{t}.{raw_body}") and compare in constant time.
Reject timestamps older than ~5 minutes to prevent replay.
Node
import crypto from "node:crypto";
function verify(secret: string, rawBody: string, header: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const ts = Number(parts.t);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Verify against the raw request body, before any JSON parsing reserializes it.
Python
import hashlib, hmac, time
def verify(secret: str, raw_body: str, header: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts = int(parts.get("t", "0"))
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Delivery & retries
- Respond
2xxwithin 10 seconds to acknowledge. Anything else is a failure. - Failed deliveries are retried up to 5 times with exponential backoff.
- Delivery is at-least-once — make your handler idempotent (dedupe on
data.attempt_id+event).