Embeds & webhooks (Pro)

Both halves of the ERP/IMS story: the embed puts the test player inside your site, the webhook pushes results back to your server. No polling, no iframes-of-dashboards.

Embedding a test

<script src="https://siddhixsys.com/embed-test.js" data-test="TESTCODE" async></script>
  • Replace TESTCODE with the code from the test's share panel.
  • The player renders where the tag sits and auto-resizes its height.
  • Optional data-min-height="600".
  • Students sign in inside the frame exactly as they would on the direct link — personal links (?c= codes) also work embedded.
  • Requires the organizer to have an active Pro plan; otherwise the frame shows an "open in full window" card.
  • Heavy-proctored tests always open in their own window (fullscreen and camera cannot be enforced inside an iframe — we refuse rather than pretend).

Webhooks

Add endpoints in Agents & API → Webhooks. We POST JSON:

  • attempt.submitted — a student finished (or time expired). attempt.state is evaluated (final score) or submitted (written answers pending).
  • result.published — you published results; the payload carries every published row: {name, email, phone, score, max_score}.
  • ping — the "Send test ping" button.
{
  "event": "attempt.submitted",
  "created_at": "2026-08-04T12:00:00.000Z",
  "test": { "id": 12, "code": "aB3xY9zQ", "title": "Weekly test 4" },
  "taker": { "name": "Asha", "email": "asha@example.com", "phone": null, "source": "roster" },
  "attempt": { "id": 88, "state": "evaluated", "score": 42, "max_score": 60,
               "submitted_at": "2026-08-04T11:59:31.000Z", "auto_submitted": false }
}

Verifying the signature

Every delivery carries X-Amatya-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, ${t}.${rawBody}):

const crypto = require('node:crypto');
function verify(header, rawBody, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expect = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; // 5-minute window
  return fresh && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expect));
}

Delivery rules

  • Respond 2xx within 10 seconds; anything else counts as a failure.
  • Retries: 6 attempts with growing backoff (1m → 5m → 30m → 2h → 12h).
  • After 12 consecutively failed deliveries the webhook pauses itself — resume it from the panel once your endpoint is fixed.
  • Endpoints must be public https — private/internal addresses are rejected at creation AND re-checked at every delivery.
  • Redirects are not followed.

The same webhook fires for every test you run — filter on test.id or test.code in your handler.