Magister API

API Documentation

Jobs and webhooks

Long-running work runs as a job: poll it, pass a webhook_url, or register webhook endpoints for job and billing events. Signatures, retries and resends.

Audits and AI visibility checks take from seconds to minutes, so they run as jobs. Starting one answers 202 right away:

json
{
  "job_id": "8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e",
  "status": "running",
  "status_url": "/v1/jobs/8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e",
  "poll_after_seconds": 15,
  "credits_charged": 0,
  "request_id": "8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e"
}

A job is running, then succeeded or failed. It holds its credits while it runs, and counts toward your organization's 5 running jobs. A job that does not finish is closed as failed with upstream_failed: an AI visibility check or an AEO audit (ai_visibility_check, aeo_check) after 45 minutes, a full site audit (site_audit) after 24 hours. There are three ways to learn that it finished.

1. Poll the job

GET /v1/jobs/{job_id} (free, read) with the key that started the job. While it runs, wait poll_after_seconds between polls. When it finishes, the job carries its result, its final credits_charged, and for a success a report_url (the report as HTML; needs the key) and a share_url (the same report at a link anyone can open for 30 days). A failed job's result is the error.

2. Pass a webhook_url

Every job endpoint takes webhook_url: a public https address that receives the job's job.succeeded or job.failed event when it finishes. No setup is needed; it is signed like every delivery below.

3. Register webhook endpoints

In Settings → Developers → Webhooks, owners and admins register up to 10 https endpoints and choose their events. Registered endpoints receive:

EventWhendata
job.succeededA job finished successfullyThe job, as GET /v1/jobs/{job_id} returns it, with its report_url and its share_url (a link anyone can open for 30 days)
job.failedA job failed or timed outThe job, with the error in result
balance.lowThe API balance fell below your alert thresholdbalance, threshold
auto_recharge.failedAn auto-recharge charge was declinedamount_usd, reason, auto_recharge_on, balance

A job's event goes to its own webhook_url (if any) and to every registered endpoint subscribed to it. Once the key that started the job is revoked or expires (a key is revoked when the person who created it leaves the organization), nothing more goes to the job's own webhook_url, not even a resend, because a key is often revoked after it leaks; registered endpoints, which only owners and admins add, still get the event. Every delivery is a POST with a JSON body of event, created_at and data:

json
{
  "event": "job.succeeded",
  "created_at": "2026-01-01T00:01:03+00:00",
  "data": {
    "job_id": "8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e",
    "kind": "ai_visibility_check",
    "status": "succeeded",
    "result": {
      "score": 66
    },
    "created_at": "2026-01-01T00:00:00+00:00",
    "finished_at": "2026-01-01T00:01:02+00:00",
    "credits_charged": 10.12,
    "report_url": "https://api.magistermarketing.com/v1/jobs/8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e/report",
    "share_url": "https://api.magistermarketing.com/v1/reports/8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e.1769817663.0d4468395608c639b19dd7fc242ab86f"
  }
}
json
{
  "event": "balance.low",
  "created_at": "2026-01-01T09:30:00.000Z",
  "data": { "balance": 812.4, "threshold": 1000 }
}
json
{
  "event": "auto_recharge.failed",
  "created_at": "2026-01-01T09:30:00.000Z",
  "data": { "amount_usd": 50, "reason": "Your card was declined.", "auto_recharge_on": true, "balance": 312.5 }
}

Verifying a delivery

Each delivery carries a Magister-Signature header, t=<unix seconds>,v1=<hex>, and a Magister-Event header naming the event (User-Agent Magister-Webhooks/1). v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with your organization's signing secret (Settings → Developers → Webhooks → Reveal; it starts with whsec_). One secret signs every delivery of the organization, its own webhook_urls included. Compute the HMAC over the body exactly as received, compare in constant time, and reject a timestamp more than five minutes old. In Node, as the Webhooks page shows:

js
import { createHmac, timingSafeEqual } from 'node:crypto';

// header: Magister-Signature: t=<unix seconds>,v1=<hex>
// rawBody: the body exactly as received, before any JSON parsing.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(String(header ?? '').split(',').map((part) => part.split('=')));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1);
  return given.length === expected.length && timingSafeEqual(Buffer.from(expected), given);
}

Rotating the secret — Rotate on the Webhooks page changes it at once; deliveries after that are signed with the new secret, so update your servers right away.

Retries and resends

Answer with any 2xx within 10 seconds. Anything else (a 3xx, 4xx or 5xx, a timeout, or an address that no longer resolves to a public host; redirects are not followed) is retried 5 times over about a day: after 1 minute, 10 minutes, 1 hour, 6 hours, 16 hours. Recent deliveries on the Webhooks page shows each one's state and tries, and Resend sends one again. A delivery can arrive more than once (a resend, or a retry after your server answered too slowly), so make your handler idempotent: key on event and data.job_id.