Skip to main content

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

EventFires when
mock_attempt.submittedA learner submits a mock attempt
mock_attempt.scoredScoring for an attempt completes
exam.content_updatedAn 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 2xx within 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).