# Magister API

Source: https://magistermarketing.com/docs/api. Every page of this section, in order, as Markdown.

## Overview and quickstart

AI visibility, AEO audits, brand profiles and site maps for any domain over REST, billed in prepaid API credits. Get a key and make a first request.

The **Magister API** gives your own code the data and analysis Magister runs for its customers: AI visibility checks for any domain (on up to 11 engines of your choosing, each answer scored with the score's parts), keyword opportunities, full marketing audits, AEO audits, a brand profile and a site map from a domain. Every endpoint takes a domain or a URL and runs once, on its own: there is no brand to set up. An AI visibility check returns every answer whole, with its sources; where an endpoint is built on a data provider's keyword data, its numbers come back as bands (each endpoint below says what it is built on). It is plain HTTPS and JSON at `https://api.magistermarketing.com`, authenticated with an API key and paid for in prepaid **API credits** ($0.02 each), apart from any Magister plan.

**Open to every organization** — create an API account at [/developers/signup](https://magistermarketing.com/developers/signup) (an organization with no brand, which lands on **Settings → Developers**), or use the organization you already have: **Settings → Developers** shows your keys, credits, usage, requests and webhooks. Questions, or a volume you want priced: [team@magistermarketing.com](mailto:team@magistermarketing.com).

**1. Create a key** — an owner or admin opens **Settings → Developers → API keys → Create key**. Copy it right away: the full key (`mm_live_...`) is shown once. Your organization's API account opens with 300 free credits. See [Authentication](https://magistermarketing.com/docs/api/authentication).

**2. Make a request** — check your balance (free), start an AI visibility check, and poll the job until it finishes. With nothing but a domain, a check asks the 3 default engines 3 suggested questions each and costs about 22 credits; `max_credits` refuses it up front if it could cost more than you allow.

**Choose the engines, the answers and the search** — a check asks the real apps buyers use and the makers' own models. ChatGPT, Gemini and Google AI Mode are read as a person sees them, Claude, Perplexity, OpenAI and Grok are asked through their makers' APIs with web search, and Gemini (API), DeepSeek, Mistral and Meta Muse answer from what the model knows. Every answer comes back in full with its sources, beside Magister's analysis of it: whether the domain is named, where, beside whom, the score and the sites it cites. Three request fields decide what is asked:

- `engines`: which engines, by name (`"chatgpt"`) or with their own settings (`{"name": "openai", "answers": 5, "web_search": false}`). Omitted: the default engines, marked in the table below.
- `answers_per_engine`: how many of the questions each engine answers (the first N). 3 by default, or every question in your own `prompts`.
- `web_search`: `false` asks the models without search (cheaper, no sources, and what the model itself knows of a brand); `true` asks only the engines that search. Omitted: each engine's default.

| Engine | What answers | Web search | In the default check | Typical credits per answer | Holds up to |
| --- | --- | --- | --- | --- | --- |
| `chatgpt` | The ChatGPT app, read through DataForSEO | Always | Yes | 1.2 | 2.5 |
| `gemini` | The Gemini app, read through DataForSEO | Always | Yes | 1.2 | 2.5 |
| `claude` | Anthropic's API (Claude Haiku) with web search | On by default; `web_search: false` turns it off | Yes | 4.7 | 13 |
| `perplexity` | Perplexity's Sonar API | Always. It is a search engine: it has no answer without search | Only by name | 1.8 | 5 |
| `openai` | OpenAI's API (not the ChatGPT app) with web search | On by default; `web_search: false` turns it off | Only by name | 3.4 | 10 |
| `google_ai_mode` | Google AI Mode, read through DataForSEO | Always | Only by name | 1.2 | 2.5 |
| `xai` | xAI's API with xAI's own search (not the Grok app) | On by default; `web_search: false` turns it off | Only by name | 24.8 | 50 |
| `gemini_api` | Google's Gemini model through an API (not the Gemini app) | Never. Google's terms bar reselling answers grounded in Google Search | Only by name | 1.3 | 3 |
| `deepseek` | DeepSeek's model | Never. Its search runs only on DeepSeek's own API, which has no zero-retention endpoint | Only by name | 1.1 | 2.5 |
| `mistral` | Mistral's model | Never. Mistral's terms bar storing or passing on the sources its search returns | Only by name | 2.9 | 6.5 |
| `meta` | Meta's Muse Spark model (not the Meta AI app) | Never. Its search runs only on Meta's own API, which has no zero-retention endpoint | Only by name | 1.4 | 3 |

An engine outside the default check is asked only when you name it in `engines`, and **every engine you add raises the check's price** by its own credits per answer. The default check (ChatGPT, Gemini, Claude) costs about 22 credits; with every engine added it costs about 135. Grok's search pages are billed as tokens, so one of its answers costs several times any other engine's.

| Add this engine | Adds, for 3 answers (credits) | The default check with it (credits) |
| --- | --- | --- |
| Perplexity (`perplexity`) | about 5 | about 27 |
| OpenAI (`openai`) | about 10 | about 32 |
| Google AI Mode (`google_ai_mode`) | about 4 | about 26 |
| Grok (`xai`) | about 74 | about 96 |
| Gemini (API) (`gemini_api`) | about 4 | about 26 |
| DeepSeek (`deepseek`) | about 3 | about 25 |
| Mistral (`mistral`) | about 9 | about 31 |
| Meta Muse (`meta`) | about 4 | about 26 |

An engine named for a search setting it cannot do answers 422, and `GET /v1/ai-visibility/engines` returns this table with which engines your account can use. An answer without search stays out of a check's citation numbers, and `by_grounding` in the result gives the searching and the non-searching engines' numbers apart.

```bash
export MAGISTER_API_KEY=mm_live_...

# Your API credit balance (free)
curl https://api.magistermarketing.com/v1/credits \
  -H "Authorization: Bearer $MAGISTER_API_KEY"

# Start an AI visibility check: a job, answered 202 with its job_id
curl https://api.magistermarketing.com/v1/ai-visibility/check \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com", "max_credits": 75}'

# Poll it until "status" is no longer "running"
curl https://api.magistermarketing.com/v1/jobs/JOB_ID \
  -H "Authorization: Bearer $MAGISTER_API_KEY"
```

**The same in JavaScript** (Node 18 or later; keep the key on a server):

```js
const API = 'https://api.magistermarketing.com';
const headers = {
  Authorization: `Bearer ${process.env.MAGISTER_API_KEY}`,
  'Content-Type': 'application/json',
};

async function call(path, init = {}) {
  const response = await fetch(API + path, { ...init, headers });
  const body = await response.json();
  if (!response.ok) throw new Error(`${body.error}: ${body.message}`);
  return body;
}

let job = await call('/v1/ai-visibility/check', {
  method: 'POST',
  body: JSON.stringify({ domain: 'example.com', max_credits: 75 }),
});
while (job.status === 'running') {
  await new Promise((done) => setTimeout(done, (job.poll_after_seconds ?? 15) * 1000));
  job = await call(`/v1/jobs/${job.job_id}`);
}
console.log(job.status, job.credits_charged, job.result);
```

**The same in Python** (with `requests`):

```python
import os
import time

import requests

API = "https://api.magistermarketing.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MAGISTER_API_KEY']}"}


def call(method, path, **kwargs):
    response = requests.request(method, API + path, headers=HEADERS, timeout=30, **kwargs)
    body = response.json()
    if not response.ok:
        raise RuntimeError(f"{body['error']}: {body['message']}")
    return body


job = call("POST", "/v1/ai-visibility/check", json={"domain": "example.com", "max_credits": 75})
while job["status"] == "running":
    time.sleep(job.get("poll_after_seconds") or 15)
    job = call("GET", f"/v1/jobs/{job['job_id']}")
print(job["status"], job["credits_charged"], job["result"])
```

**3. Read what it cost** — every successful response carries `credits_charged` (what the request used) and the `X-Magister-Credits-Remaining` header (your API credit balance). A request the balance cannot cover is refused with a 402 before anything runs. See [Credits and billing](https://magistermarketing.com/docs/api/credits-and-billing).

**4. Go further** — skip polling with a webhook ([Jobs and webhooks](https://magistermarketing.com/docs/api/jobs-and-webhooks)), handle every error code ([Errors](https://magistermarketing.com/docs/api/errors)), and browse every endpoint ([Endpoints reference](https://magistermarketing.com/docs/api/endpoints)). The machine-readable OpenAPI document is at [https://api.magistermarketing.com/v1/openapi.json](https://api.magistermarketing.com/v1/openapi.json).

**API or MCP?** — the API is for your own code: scripts, back ends, scheduled jobs and products you build. To work with Magister from Claude, ChatGPT, Cursor or another assistant, connect it over MCP instead ([How Connect works](https://magistermarketing.com/docs/connect/connect-overview)).

## Authentication and API keys

Bearer API keys, read-only versus full keys, per-key daily credit caps, rotating a key, and keeping keys out of browsers.

Every request sends an API key as a bearer token:

```http
Authorization: Bearer mm_live_...
```

A missing, unknown or revoked key answers **401** `not_authenticated`, the same for all three so a probe learns nothing about which keys exist.

**Who creates keys** — organization owners and admins, in **Settings → Developers → API keys**. Members see each key's name and usage, never the key. The full key is shown once, when it is created; Magister stores only its hash.

**What a key acts as** — a key acts for its organization as the person who created it, and is billed to that organization's API credits. It is revoked when that person leaves the organization, so create production keys as someone who will stay.

**Full or read only** — a **full** key has the `read` and `write` scopes and can start work that spends credits. A **read-only** key has only `read`: it can read your balance and jobs and run the free endpoints, and it can never spend credits. A call that needs a scope the key lacks answers **403** `not_authorized` with `required_scope`. The [Endpoints reference](https://magistermarketing.com/docs/api/endpoints) gives each endpoint's scope.

**Daily credit cap** — give a key an optional cap when you create it. A paid call is refused when the key's spend today (UTC), plus what its running requests hold, plus the new request's [hold](https://magistermarketing.com/docs/api/credits-and-billing), would pass the cap: it answers **429** `rate_limited` with `reason: key_daily_cap_reached` and `retry_after_seconds` until 00:00 UTC. A request whose hold alone is above the cap can never start on that key, so it answers **402** `limit_reached` with `reason: key_daily_cap_too_low`, `estimated_credits` and `daily_cap_credits`, and waiting does not help: under a 100-credit cap, for example, a site audit (which holds 240) never starts. Set the cap above the largest hold the key needs. The cap is per key; the organization's balance still applies to all of them.

**Limits for the whole organization** — 60 requests a minute and 5 running jobs, however many keys you have. Past either, the API answers **429** with `retry_after_seconds` and a `Retry-After` header.

**Rotating a key** — create the new key, deploy it, confirm its calls in **Requests**, then **Revoke** the old one. A revoked key stops working at once; its past usage stays in Usage and Requests. Revoke immediately if a key leaks. Jobs can be polled only with the key that started them, so let running jobs finish (or note their results) before you revoke. Once a key is revoked, nothing more goes to the `webhook_url` its jobs named, not even for a job still running; your [registered endpoints](https://magistermarketing.com/docs/api/jobs-and-webhooks) still get every event.

**Never in a browser** — a key spends your organization's credits. Keep it in a server-side secret store or environment variable; never put it in web pages, mobile apps or a public repository. Use one key per system so you can revoke one without the others.

## Credits and billing

Prepaid API credits at $0.02 with volume bonuses, holds and max_credits, credits_charged, the 402 refusal, auto-recharge, alerts, and expiry.

API calls spend **API credits**: a prepaid balance that belongs to your organization and is billed apart from any Magister plan. Plan credits stay for chat and MCP; API credits are only for API keys, and the API keeps working when a plan's payment is overdue.

**Price** — $0.02 a credit (50 credits per dollar), with 10% more credits from $100 and 20% more from $500. Buy any whole amount from $10 to $2,000. A new API account starts with 300 free credits.

| You pay | Credits | Bonus | You get |
| --- | --- | --- | --- |
| $20 | 1,000 | none | 1,000 |
| $50 | 2,500 | none | 2,500 |
| $100 | 5,000 | +500 (10%) | 5,500 |
| $500 | 25,000 | +5,000 (20%) | 30,000 |
| $2,000 | 100,000 | +20,000 (20%) | 120,000 |

**Buying** — owners and admins choose **Settings → Developers → API billing → Add credits** and pay by card on Stripe Checkout. The credits land as soon as the payment does, and the receipt is listed on the same page. Purchased credits are good for 12 months; the soonest to expire are spent first.

**What a request costs** — a request is charged what it actually used, in credits, and says so in `credits_charged` on every successful response. A request refused before it starts costs nothing; one that fails partway costs what it used. Free endpoints answer `credits_charged: 0`. A job's charge grows while it runs; its final `credits_charged` is on [the job](https://magistermarketing.com/docs/api/jobs-and-webhooks) once it finishes. The `X-Magister-Credits-Remaining` header on each successful response (and on a 402) is your balance.

**Holds** — before a paid request starts, it holds the most it is likely to cost (about twice its usual cost) against your balance. Your **available** credits are the balance less what running requests hold; a request whose hold is more than what is available is refused with a 402, before any provider is called. The hold is released when the request finishes, and only what it used is charged. A run rarely spends past its hold.

| Endpoint | Typical cost (credits) | Holds up to |
| --- | --- | --- |
| POST /v1/brand/extract | about 2, measured per run | 20 |
| POST /v1/web/map | about 1 per map | 4 |
| POST /v1/competitors/discover | about 3 per lookup | 8 |
| POST /v1/keywords/research | about 12 (median of 17 runs, 30 days); a lookup that finds nothing is not billed | 40 |
| POST /v1/audits/site | about 100, measured per run (the full marketing audit) | 240 |

An AI visibility check holds, for each engine, its number of answers times that engine's ceiling (the engines table), plus 3. The default check (3 questions on the 3 default engines) holds up to 57 and usually costs about 22.

**max_credits** — every job endpoint takes `max_credits`. A job whose hold is above it is refused with **402** `limit_reached`, `reason: max_credits_exceeded` and `estimated_credits`, and costs nothing. Use it as a guard against an unexpectedly large request (many questions or engines, for example).

**Out of credits** — a request the balance cannot cover answers **402**:

```json
{
  "error": "limit_reached",
  "reason": "credits_exhausted",
  "message": "Not enough API credits: this request holds up to 33 and 12.5 are available. Add credits to continue.",
  "retryable": false,
  "available_credits": 12.5,
  "top_up_url": "https://magistermarketing.com/developers/billing"
}
```

`GET /v1/credits` (free, `read`) returns `balance`, `held`, `available` and `price_per_credit_usd`.

**Auto-recharge** — save a card (your first purchase saves one) and turn on **Auto-recharge** on API billing: when the balance falls below your threshold (500 credits unless you change it), Magister charges the card the amount you set ($10 to $2,000), at most your monthly limit. Each declined charge emails your owners and admins (or your billing email) and sends an `auto_recharge.failed` webhook event. A declined card is tried once more a day later; a second decline in a row turns auto-recharge off.

**Disputed payments** — disputing a payment for API credits turns auto-recharge off. The disputed card (or a wallet card, such as Apple Pay, that can't be told apart from it) can no longer be saved for auto-recharge; save a different card, or write to [team@magistermarketing.com](mailto:team@magistermarketing.com) if the dispute was a mistake.

**Low-balance alerts** — Magister emails your owners and admins (or the billing email you set) and sends a `balance.low` webhook event once each time the balance falls below your alert threshold (1,000 credits unless you change it). It alerts again only after the balance has gone back above it.

**Usage and requests** — **Settings → Developers → Usage** shows credits per day and per endpoint (with a CSV export), and **Requests** lists every paid or async request with its status, error code and cost.

## 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](https://magistermarketing.com/docs/api/errors).

### 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:

| Event | When | `data` |
| --- | --- | --- |
| `job.succeeded` | A job finished successfully | The 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.failed` | A job failed or timed out | The job, with the error in `result` |
| `balance.low` | The API balance fell below your alert threshold | `balance`, `threshold` |
| `auto_recharge.failed` | An auto-recharge charge was declined | `amount_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_url`s 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`.

## From a lead to a brand

Audit and check a prospect through the API, then create their brand with POST /v1/projects: the audit becomes its plan and the check its first AI visibility history.

Audit a prospect and check their AI visibility through the API. When they sign, create their brand in your organization with the results already in it, and manage it in Magister from then on.

### 1. Audit and check the prospect

```bash
curl -X POST https://api.magistermarketing.com/v1/audits/site \
  -H "Authorization: Bearer $MAGISTER_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://acme.com"}'

curl -X POST https://api.magistermarketing.com/v1/ai-visibility/check \
  -H "Authorization: Bearer $MAGISTER_API_KEY" -H "Content-Type: application/json" \
  -d '{"domain": "acme.com"}'
```

Each answers with a `job_id`. Keep both; they are billed in API credits like any job.

### 2. Create the brand when they sign

```bash
curl -X POST https://api.magistermarketing.com/v1/projects \
  -H "Authorization: Bearer $MAGISTER_API_KEY" -H "Content-Type: application/json" \
  -d '{"website_url": "https://acme.com", "name": "Acme", "from_jobs": ["<audit job_id>", "<check job_id>"]}'
```

```json
{
  "data": {
    "status": "created",
    "message": "Added Acme. Its marketing plan is being built from the audit you already ran, and its agent is being set up.",
    "project_id": "5b2c1d7e-8a4f-4e2b-9c1d-7e8a4f4e2b9c",
    "name": "Acme",
    "website_url": "https://acme.com/",
    "audit": {
      "job_id": "<audit job_id>",
      "plan_id": "9c1d7e8a-4f4e-4b9c-8d7e-8a4f4e2b9c1d",
      "carried": true
    },
    "ai_visibility": {
      "job_id": "<check job_id>",
      "imported": true,
      "set_id": "1d7e8a4f-4e2b-4c1d-9e8a-4f4e2b9c1d7e",
      "round_id": "7e8a4f4e-2b9c-4d7e-8a4f-4e2b9c1d7e8a",
      "answers": 9
    }
  },
  "credits_charged": 0,
  "request_id": "8f14e45f-ceea-4e7a-9b1c-1f2a3b4c5d6e"
}
```

The brand is set up as if it had been added in Magister, with its plan, its agent and its place in your billing:

- **The audit becomes its plan.** No second audit runs. Magister builds the marketing plan from it in a few minutes, billed on your plan's credits like any new brand's first plan, and the audit's AI visibility round carries over with it.
- **The check becomes its first AI visibility history.** It arrives as a set with the check's questions and engines and one round dated to when the check ran, with every answer, score, mention, position, competitor and source. From then on Magister tracks the set weekly, billed on your plan. An engine that Magister's tracking asks with web search (OpenAI, Claude, Grok) but that your check asked without it is left out, since there is nothing to compare with.

### The rules

- `from_jobs` takes your organization's finished jobs, started by any of its keys: at most one site audit and one AI visibility check, both of the brand's own site.
- A job counts as finished once it reads `succeeded`: poll `GET /v1/jobs/{job_id}` with the key that started it (an audit job is settled when it is read), and a job another of your keys started settles by itself within a few minutes. Until then it answers **409** `not_ready`. A failed job, or a job of another site, answers **422**.
- An audit becomes one brand's plan, and a check one brand's history, once: carried into another brand, either answers **409** `conflict`.
- Sending the same request again never adds a second brand. It answers **200** with the brand it created, with the audit as its plan and the check carried in (once). A brand someone else already added for the site gets nothing carried into it (`carried` and `imported` are `false`, reason `brand_exists`). A brand's id is returned only when the key's creator can open it; otherwise it answers **409** `conflict`.
- The key needs the `write` scope, and its creator must be an owner or admin of the organization, or a member allowed to add brands (**403** `not_authorized` otherwise).

### What it costs

Creating the brand costs no API credits. The brand is billed on your plan like any other.

- A brand your plan already includes is created right away, and so is any brand of a paid plan Magister made complimentary.
- Any other brand answers **402** `payment_required` with its `charge` (`amount_cents`, `interval`) and `add_brand_url`, and nothing is added or charged: an API key never agrees to a price. Add the brand at `add_brand_url` instead; the audit and check stay in your API jobs.
- An organization on Free, or with no active plan, answers **402** `payment_required` with `upgrade_url`.

## Errors

One JSON error shape with error, message and retryable; every code with its HTTP status; when to retry.

Every error the API answers is JSON with the same shape, the one Magister's MCP tools use:

```json
{
  "error": "rate_limited",
  "message": "Rate limit of 60 requests a minute exceeded for this organization.",
  "retryable": true,
  "retry_after_seconds": 12,
  "reason": "rate_limited"
}
```

- `error` is a stable code (below); branch on it, not on `message`, which is for people.
- `retryable` says whether the same request may succeed later. Retry only when it is `true`, and after `retry_after_seconds` when it is given (a 429 also sends `Retry-After`). Otherwise fix the request, your balance or your access first.
- Some errors add details: `reason`, `required_scope`, `available_credits`, `top_up_url`, `estimated_credits`, `request_id`, or `fields` for a body that does not fit the schema.
- A request that fails on Magister's side after it started (500 `upstream_failed` with `request_id`) is not retryable: the work may have run and been charged. Look it up in **Settings → Developers → Requests** before sending it again. A path that does not exist answers 404 `not_found`, and a wrong method 405 `unsupported_operation`.

```json
{
  "error": "validation_error",
  "message": "domain: Field required",
  "retryable": false,
  "fields": [{ "field": "domain", "message": "Field required" }]
}
```

### HTTP statuses

- **401**: Missing, unknown or revoked API key.
- **402**: Not enough API credits (`limit_reached`, `credits_exhausted`, with `top_up_url`), over `max_credits`, or a hold above the key's whole daily cap (`key_daily_cap_too_low`).
- **403**: The key lacks a scope, or the endpoint is not enabled.
- **404**: Unknown job; or the API is not enabled.
- **409**: Not ready yet: the report of a job that has not finished.
- **422**: Invalid input (`validation_error`, with `fields` when the body does not fit the schema).
- **429**: 60 requests a minute or 5 running jobs per organization, or the key's daily cap; see `Retry-After`.
- **500**: The request failed unexpectedly (`upstream_failed`): retryable when it failed before it started; after, `retryable` is false and the error carries its `request_id`.
- **502**: An upstream provider failed. Billed only what it used before failing.
- **503**: The request could not start (`transport_unavailable`).

### Error codes

| Code | HTTP | When | Retry |
| --- | --- | --- | --- |
| `not_authenticated` | 401 | No key, an unknown or revoked key, or a key whose creator left the organization. | no |
| `budget_exhausted` | 402 | Passed through from the tool behind the endpoint. | varies |
| `limit_reached` | 402 | Not enough API credits (`reason: credits_exhausted`, with `available_credits` and `top_up_url`), the hold is above your `max_credits` (`reason: max_credits_exceeded`, with `estimated_credits`), or the hold is above the whole daily cap of the key, so that key can never start it (`reason: key_daily_cap_too_low`, with `estimated_credits` and `daily_cap_credits`). Nothing ran and nothing was charged. | no |
| `payment_required` | 402 | Passed through from the tool behind the endpoint. | varies |
| `entitlement_unavailable` | 403 | The API is not open to your organization yet, the endpoint is not available yet, or an AI engine you asked for is not enabled. | no |
| `forbidden` | 403 | Passed through from the tool behind the endpoint. | varies |
| `not_authorized` | 403 | The key lacks the scope the endpoint needs (`required_scope`). | no |
| `not_found` | 404 | The API is switched off, or no such job or set (in this project). | no |
| `project_not_found` | 404 | No such brand in this organization. | no |
| `approval_required` | 409 | Passed through from the tool behind the endpoint. | varies |
| `browser_unavailable` | 409 | Passed through from the tool behind the endpoint. | varies |
| `conflict` | 409 | Passed through from the tool behind the endpoint. | varies |
| `needs_connection` | 409 | Passed through from the tool behind the endpoint. | varies |
| `not_ready` | 409 | The report of a job that has not succeeded. | no |
| `plan_missing` | 409 | Passed through from the tool behind the endpoint. | varies |
| `asset_invalid` | 422 | Passed through from the tool behind the endpoint. | varies |
| `needs_media` | 422 | Passed through from the tool behind the endpoint. | varies |
| `unsupported_operation` | 422 | Passed through from the tool behind the endpoint. | varies |
| `validation_error` | 422 | The input is invalid. A body that does not fit the schema lists each problem in `fields`. | no |
| `quota_exceeded` | 429 | Passed through from the tool behind the endpoint. | varies |
| `rate_limited` | 429 | `reason: rate_limited` (60 requests a minute per organization), `jobs_limit` (5 running jobs per organization) or `key_daily_cap_reached` (the key's daily credit cap, reset at 00:00 UTC). Wait `retry_after_seconds` (also the `Retry-After` header). | yes |
| `upstream_failed` | 502 | A provider behind the endpoint failed (502), or the request failed unexpectedly (500; not retryable once it had started, with `request_id`). Billed only what it used. | varies |
| `transport_unavailable` | 503 | The request could not start. | varies |

Codes marked "passed through" come from the Magister tool behind an endpoint, exactly as its MCP tool returns them. Where **Retry** says "varies", read `retryable` in the error itself.

## Endpoints reference

Every endpoint with its scope, cost, parameters and response, generated from the API's OpenAPI document.

Every endpoint below is generated from the API's OpenAPI document, which is also served at [https://api.magistermarketing.com/v1/openapi.json](https://api.magistermarketing.com/v1/openapi.json) for code generators and tools like Postman. Base URL: `https://api.magistermarketing.com`. Send `Authorization: Bearer mm_live_...` with every call and JSON bodies with `Content-Type: application/json`. An endpoint whose description says "Not enabled until its upstream's resale terms are cleared" answers 403 `entitlement_unavailable` until it opens.

### GET /v1/credits

**GET /v1/credits**: API credit balance.
- Credits: free. Upstream: none (the API credit balance). Scope: `read`.
- Answers 200 with `DataResponse` (below).

### POST /v1/keywords/research

**POST /v1/keywords/research**: Keyword research.
- Credits: about 12 (median of 17 runs, 30 days); a lookup that finds nothing is not billed. Upstream: DataForSEO's data, returned as Magister's opportunities (score, intent, topic, action; demand, difficulty and rank as bands), never DataForSEO's numbers. Scope: `write`. Enabled by Magister for the API; answers 403 `entitlement_unavailable` while it is off.
- `domain` (string, optional, at most 253 characters): A site whose rankings and search summary to read, such as example.com.
- `seed` (string, optional, at most 200 characters): A seed keyword for ideas.
- `limit` (integer, optional, default `20`, 1 to 100)
- Answers 200 with `DataResponse` (below).

### POST /v1/web/map

**POST /v1/web/map**: Map a website's pages.
- Credits: about 1 per map. Upstream: Magister's own crawler (Firecrawl, self-hosted: the site's sitemaps). Scope: `write`.
- `url` (string, required, at most 2000 characters): The site's address, such as https://example.com.
- `search` (string, optional, at most 200 characters): Keep only the pages whose address contains this word.
- `limit` (integer, optional, default `100`, 1 to 500)
- Answers 200 with `DataResponse` (below).

### POST /v1/web/page-speed

**POST /v1/web/page-speed**: A site's page experience for real visitors.
- Credits: free (no paid provider). Upstream: the Chrome UX Report (Google, CC BY 4.0): real visitors' measurements, returned as Magister's page-experience score, ratings and fixes. Scope: `read`.
- `url` (string, required, at most 2000 characters): The site's address, such as https://example.com.
- Answers 200 with `DataResponse` (below).

### POST /v1/competitors/discover

**POST /v1/competitors/discover**: Find a site's search competitors.
- Credits: about 3 per lookup. Upstream: DataForSEO's domain competitors, returned as Magister's overlap, standing and reach (bands), never DataForSEO's numbers. Scope: `write`. Enabled by Magister for the API; answers 403 `entitlement_unavailable` while it is off.
- `domain` (string, required, at most 253 characters): The site whose search rivals to find, such as example.com.
- `limit` (integer, optional, default `10`, 1 to 50)
- Answers 200 with `DataResponse` (below).

### GET /v1/projects

**GET /v1/projects**: Your organization's brands.
- Credits: free. Upstream: none (your organization's brands in Magister). Scope: `read`.
- Answers 200 with `DataResponse` (below).

### POST /v1/projects

**POST /v1/projects**: Create a brand, with a lead's audit and check in it.
- Credits: free (the brand is billed on your plan). Upstream: none: a brand in your organization, set up as in the app (its audit, plan and agent) and billed on your plan like any brand, never in API credits. Scope: `write`. Answers 201 with the new brand, or 200 with the one your organization already has for this site (nothing is added twice: a retry gets the brand it made, and a brand someone else added gets nothing from `from_jobs`; a brand's id is returned only to a key whose creator may open it, otherwise 409). Each job must read `succeeded` (409 `not_ready` until it does). A brand your plan does not already include answers 402 `payment_required` with its `charge` and `add_brand_url`: an API key never agrees to a price. The key's creator must be an owner or admin, or a member allowed to add brands.
- `website_url` (string, required, at most 2048 characters): The brand's public website, such as https://example.com.
- `name` (string, optional, 1 to 80 characters): Defaults to the name Magister reads from the site.
- `whose` (one of `client`, `own`, optional, default `"client"`): A client's brand (the default) or your own business.
- `hosted` (boolean, optional): Connect plans only: `true` asks for the brand with its own hosted Magister Agent.
- `from_jobs` (array of string, optional, at most 2 items): Finished jobs of this site to carry into the brand, from any of your organization's keys: a site audit (`POST /v1/audits/site`) becomes the brand's plan, with no second audit, and an AI visibility check (`POST /v1/ai-visibility/check`) becomes its first AI visibility history, tracked weekly from then on. At most one of each.
- Answers 201 with `DataResponse` (below).

### GET /v1/projects/{project_id}/ai-visibility/sets

**GET /v1/projects/{project_id}/ai-visibility/sets**: A brand's tracked AI visibility sets.
- Credits: free. Upstream: Magister's tracking of a brand's AI visibility: scores, rates, share of voice and changes, never the answers. Scope: `read`.
- Path `project_id` (string, required)
- Answers 200 with `DataResponse` (below).

### GET /v1/projects/{project_id}/ai-visibility/sets/{set_id}

**GET /v1/projects/{project_id}/ai-visibility/sets/{set_id}**: One tracked AI visibility set.
- Credits: free. Upstream: Magister's tracking of a brand's AI visibility: scores, rates, share of voice and changes, never the answers. Scope: `read`.
- Path `project_id` (string, required)
- Path `set_id` (string, required)
- Answers 200 with `DataResponse` (below).

### POST /v1/audits/site

**POST /v1/audits/site**: Start a full marketing audit of a site.
- Credits: about 100, measured per run (the full marketing audit). Upstream: DataForSEO, Google's Chrome UX Report and LLMs as inputs (no Bright Data, no BuiltWith), returned as Magister's scores, findings and opportunities (keywords as bands), never the vendors' data. Scope: `write`. Enabled by Magister for the API; answers 403 `entitlement_unavailable` while it is off.
- `webhook_url` (string, optional, at most 2000 characters): Public https URL POSTed when the job finishes, signed in `Magister-Signature` with your organization's webhook secret (Settings -> Developers -> Webhooks); retried 5 times over a day.
- `max_credits` (number, optional, above 0, at most 100,000): Refuse the job (402 `limit_reached`, reason `max_credits_exceeded`) when the most it may hold is above this. A refused job costs nothing.
- `url` (string, required, at most 2000 characters): Any public site, such as https://example.com.
- Answers 202 with `JobAccepted` (below).

### POST /v1/audits/aeo

**POST /v1/audits/aeo**: Start an AEO (AI readiness) audit of a page.
- Credits: free (no paid provider). Upstream: none (Magister fetches the page itself). Scope: `read`.
- `webhook_url` (string, optional, at most 2000 characters): Public https URL POSTed when the job finishes, signed in `Magister-Signature` with your organization's webhook secret (Settings -> Developers -> Webhooks); retried 5 times over a day.
- `max_credits` (number, optional, above 0, at most 100,000): Refuse the job (402 `limit_reached`, reason `max_credits_exceeded`) when the most it may hold is above this. A refused job costs nothing.
- `url` (string, required, at most 2000 characters): A public page, such as https://example.com/pricing.
- Answers 202 with `JobAccepted` (below).

### GET /v1/ai-visibility/engines

**GET /v1/ai-visibility/engines**: The engines an AI visibility check can ask.
- Credits: free. Upstream: none (the engines a check can ask, and which your account may use). Scope: `read`.
- Answers 200 with `DataResponse` (below).

### POST /v1/ai-visibility/check

**POST /v1/ai-visibility/check**: Check a domain's AI visibility (no brand needed).
- Credits: about 22 for the default check (3 questions on the 3 default engines), measured per run; each answer costs what its engine does (`GET /v1/ai-visibility/engines`). Perplexity, asked only by name, adds about 5. OpenAI, asked only by name, adds about 10. Google AI Mode, asked only by name, adds about 4. Grok, asked only by name, adds about 74. Gemini (API), asked only by name, adds about 4. DeepSeek, asked only by name, adds about 3. Mistral, asked only by name, adds about 9. Meta Muse, asked only by name, adds about 4. An answer without web search costs a fraction of one with it. Upstream: the ChatGPT and Gemini apps and Google AI Mode read through DataForSEO; Anthropic, OpenAI, Perplexity and xAI through their APIs with web search; Gemini's API, DeepSeek, Mistral and Meta's Muse answering without search. Scope: `write`.
- `webhook_url` (string, optional, at most 2000 characters): Public https URL POSTed when the job finishes, signed in `Magister-Signature` with your organization's webhook secret (Settings -> Developers -> Webhooks); retried 5 times over a day.
- `max_credits` (number, optional, above 0, at most 100,000): Refuse the job (402 `limit_reached`, reason `max_credits_exceeded`) when the most it may hold is above this. A refused job costs nothing.
- `domain` (string, required, at most 253 characters): The site whose brand to check, such as example.com.
- `brand_name` (string, optional, 1 to 100 characters): Defaults to the site's own name.
- `competitors` (array of string, optional, at most 20 items, each at most 100 characters)
- `prompts` (array of string, optional, at most 10 items, each at most 500 characters): Up to 10 questions buyers ask; every engine is asked the same ones. Omitted: unbranded buyer questions are suggested from the site, as many as the most answers asked of any engine (3 by default); a site your organization checked before is asked the questions that check suggested, so repeat checks compare, until they are 30 days old (`prompts_suggested_at` in the result says when they were first suggested). A page's address (`medium.com/@brand`) always gets its own.
- `engines` (array of object, optional, at most 11 items): Which engines to ask, each by name (`"openai"`) or with its own settings (`{"name": "openai", "answers": 5, "web_search": false}`). Omitted: the default engines (`GET /v1/ai-visibility/engines` marks them, with what each is, whether it searches and what it costs). The others are asked only by name.
- `answers_per_engine` (integer, optional, 1 to 10): How many of the questions each engine answers. Defaults to 3, or to every question in `prompts` when you send your own.
- `web_search` (boolean, optional): Whether the engines search the web for their answers. Omitted: each engine's default (the ChatGPT and Gemini apps, Google AI Mode, Claude, OpenAI, Perplexity and Grok search; Gemini's API, DeepSeek, Mistral and Meta answer from what the model knows). `true`: with no `engines`, only the engines that can search are asked. `false`: every engine that can answers without search, which leaves out Perplexity (it is a search engine) and the apps (they are read as a buyer uses them). An answer without search has no sources and costs less.
- Answers 202 with `JobAccepted` (below).

### POST /v1/brand/extract

**POST /v1/brand/extract**: Extract a brand from its domain.
- Credits: about 2, measured per run. Upstream: Magister's own page reader (Firecrawl, self-hosted) and an LLM. Scope: `write`.
- `domain` (string, required, at most 253 characters): Any public site, such as example.com.
- Answers 200 with `DataResponse` (below).

### GET /v1/jobs/{job_id}

**GET /v1/jobs/{job_id}**: Poll a job.
- Credits: free. Upstream: none. Scope: `read`.
- Path `job_id` (string, required)
- Answers 200 with `JobResponse` (below).

### GET /v1/jobs/{job_id}/report

**GET /v1/jobs/{job_id}/report**: A finished job as an HTML report.
- Credits: free. Upstream: none. Scope: `read`.
- Path `job_id` (string, required)
- Answers 200 with `text/html`.

### Response shapes

**DataResponse**
- `data` (object, required)
- `credits_charged` (number, required): Credits this request has cost: what it actually used. A request refused before it starts costs nothing; one that fails partway costs what it used.
- `request_id` (string, required)

**JobAccepted**
- `job_id` (string, required)
- `status` (one of `running`, `succeeded`, `failed`, required)
- `status_url` (string, required)
- `poll_after_seconds` (integer, required)
- `credits_charged` (number, required)
- `request_id` (string, required)

**JobResponse**
- `job_id` (string, required)
- `kind` (one of `site_audit`, `ai_visibility_check`, `aeo_check`, required)
- `status` (one of `running`, `succeeded`, `failed`, required)
- `result` (object, optional)
- `poll_after_seconds` (integer, optional)
- `credits_charged` (number, required)
- `report_url` (string, optional): The finished job as HTML (needs the key).
- `share_url` (string, optional): The same report at a link anyone may open.
- `created_at` (string, optional)
- `finished_at` (string, optional)

**ErrorBody**
- `error` (string, required): The MCP error code, such as `limit_reached`.
- `message` (string, required)
- `retryable` (boolean, required)

## Examples

Real requests and responses for the AI visibility check, keyword research, competitors, page speed and the full audit, with how long each took and what it cost: ready to paste into an LLM or a coding agent.

Each example is a real run against tally.so, trimmed to a few items per list, with long answers cut short here (…); the API returns each answer whole. Your figures differ: answers change from run to run, and credits are measured per run.

### The engines a check can ask (free)

```bash
curl -X GET https://api.magistermarketing.com/v1/ai-visibility/engines \
  -H "Authorization: Bearer $MAGISTER_API_KEY"
```

One engine of the list:

```json
{
  "name": "chatgpt",
  "label": "ChatGPT",
  "web_search": "always",
  "answered_by": "The ChatGPT app, read through DataForSEO",
  "default": true,
  "available": true,
  "typical_credits_per_answer": 1.2,
  "max_credits_per_answer": 2.5
}
```

### An AI visibility check (a job)

```bash
curl -X POST https://api.magistermarketing.com/v1/ai-visibility/check \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"tally.so","engines":["chatgpt","gemini","claude","perplexity","openai"]}'
```

This one names the 5 engines it asked; with only a `domain`, a check asks the 3 default engines. It answers `202` with a `job_id`; poll `GET /v1/jobs/{job_id}` (or pass `webhook_url`). This one took 1 min 5 s. Every answer comes back whole, with its sources.

```bash
curl -X GET https://api.magistermarketing.com/v1/jobs/cc838f9f-4f8c-4c1a-9cb1-e9835e78cbe5 \
  -H "Authorization: Bearer $MAGISTER_API_KEY"
```

```json
{
  "job_id": "cc838f9f-4f8c-4c1a-9cb1-e9835e78cbe5",
  "kind": "ai_visibility_check",
  "status": "succeeded",
  "created_at": "2026-11-11T21:40:21.622813+00:00",
  "finished_at": "2026-11-11T21:41:26.877514+00:00",
  "credits_charged": 24.75,
  "result": {
    "domain": "tally.so",
    "brand_name": "Tally Forms",
    "score": 48,
    "mention_rate": 67,
    "citation_rate": 40,
    "average_position": 2.2,
    "answers": 15,
    "failed_answers": 0,
    "engines": [
      "chatgpt",
      "gemini",
      "claude",
      "perplexity",
      "openai"
    ],
    "by_engine": {
      "chatgpt": {
        "model": null,
        "score": 52,
        "answers": 3,
        "grounding": "live_search",
        "sentiment": {
          "neutral": 1,
          "negative": 0,
          "positive": 2
        },
        "web_search": true,
        "mention_rate": 100,
        "citation_rate": 67,
        "average_position": 2.7
      },
      "claude": {
        "model": "claude-haiku-5-5",
        "score": 54,
        "answers": 3,
        "grounding": "live_search",
        "sentiment": {
          "neutral": 0,
          "negative": 0,
          "positive": 2
        },
        "web_search": true,
        "mention_rate": 67,
        "citation_rate": 33,
        "average_position": 2
      }
    },
    "results": [
      {
        "cited": true,
        "model": null,
        "score": 71,
        "answer": "For a small business, I'd prioritize form builders that offer **free submissions, professional-looking forms, customer lead collection, conditional logic, and integrations** without forcing you to upgrade immediately.\n\nI've compared the strongest options available in November 2026. These are my top recommendations.\n\n# Best free form builders for small businesses\n\n\n\n\n\n(Best overall)\n\n**1\\.…",
        "engine": "chatgpt",
        "prompt": "Best free form builders for small businesses",
        "status": "ready",
        "branded": false,
        "seconds": 31.7,
        "named_as": null,
        "position": 1,
        "citations": [
          {
            "url": "https://tally.so/",
            "title": "Visit Tally",
            "domain": "tally.so",
            "snippet": null,
            "own_site": true
          },
          {
            "url": "https://forms.google.com/",
            "title": "Visit Google Forms",
            "domain": "google.com",
            "snippet": null,
            "own_site": false
          },
          {
            "url": "https://fillout.com/",
            "title": "Visit Fillout",
            "domain": "fillout.com",
            "snippet": null,
            "own_site": false
          }
        ],
        "mentioned": true,
        "sentiment": "positive",
        "web_search": true,
        "prompt_type": "pricing",
        "score_parts": {
          "depth": {
            "max": 15,
            "score": 15
          },
          "framing": {
            "max": 15,
            "score": 15
          },
          "accuracy": {
            "max": 5,
            "score": 5
          },
          "citation": {
            "max": 10,
            "score": 10
          },
          "visibility": {
            "max": 25,
            "score": 6
          },
          "competitive": {
            "max": 10,
            "score": 7
          },
          "recommendation": {
            "max": 20,
            "score": 13
          }
        },
        "brands_named": [
          "Fillout",
          "Stripe",
          "HubSpot Forms",
          "HubSpot",
          "Zoho Forms"
        ],
        "judge_reason": "Tally is the top-ranked, strongly praised choice, with specific features and a generous-free-plan recommendation alongside several other form builders.",
        "competitors_found": [],
        "competitors_named": [
          "Fillout",
          "HubSpot Forms",
          "Zoho Forms"
        ]
      },
      {
        "cited": true,
        "model": "claude-haiku-5-5",
        "score": 97,
        "answer": "Here are the strongest free options for small businesses in 2026. Most of the comparison sites that ranked these are run by form vendors, so check current limits on each pricing page before committing.\n\n1. **Tally (best overall for most small businesses).** One review says Tally is the best free form builder for small businesses because it has no form or response limits and includes payments,…",
        "engine": "claude",
        "prompt": "Best free form builders for small businesses",
        "status": "ready",
        "branded": false,
        "seconds": 19.3,
        "named_as": null,
        "position": 1,
        "citations": [
          {
            "url": "https://paperform.co/form-builders/best-form-builders-for-small-businesses/",
            "title": "Best Form Builders for Small Businesses in 2026",
            "domain": "paperform.co",
            "snippet": null,
            "own_site": false
          },
          {
            "url": "https://tally.so/help/best-free-online-form-builders",
            "title": "8 Best Free Form Builders in 2026 (Free Plans, Not Trials)",
            "domain": "tally.so",
            "snippet": null,
            "own_site": true
          },
          {
            "url": "https://fabric.so/comparison/best-free-form-builder",
            "title": "Best free form builder",
            "domain": "fabric.so",
            "snippet": null,
            "own_site": false
          }
        ],
        "mentioned": true,
        "sentiment": "positive",
        "web_search": true,
        "prompt_type": "pricing",
        "score_parts": {
          "depth": {
            "max": 15,
            "score": 15
          },
          "framing": {
            "max": 15,
            "score": 15
          },
          "accuracy": {
            "max": 5,
            "score": 5
          },
          "citation": {
            "max": 10,
            "score": 10
          },
          "visibility": {
            "max": 25,
            "score": 25
          },
          "competitive": {
            "max": 10,
            "score": 7
          },
          "recommendation": {
            "max": 20,
            "score": 20
          }
        },
        "brands_named": [
          "G2",
          "HubSpot Forms",
          "Wufoo",
          "Cognito Forms",
          "Stripe"
        ],
        "judge_reason": "Tally gets detailed, strongly positive coverage as the best overall option and the explicit final pick, alongside relevant form-builder alternatives.",
        "competitors_found": [],
        "competitors_named": [
          "HubSpot Forms",
          "Wufoo",
          "Cognito Forms",
          "Fillout",
          "forms.app"
        ]
      }
    ],
    "questions": [
      {
        "type": "pricing",
        "score": 85,
        "prompt": "Best free form builders for small businesses",
        "answers": 5,
        "branded": false,
        "cited_in": 5,
        "brands_named": [
          "Fillout",
          "Stripe",
          "HubSpot Forms",
          "Zoho Forms",
          "Google Workspace"
        ],
        "mentioned_in": 5,
        "best_position": 1,
        "searched_answers": 5,
        "competitors_named": [
          "Fillout",
          "HubSpot Forms",
          "Zoho Forms",
          "Cognito Forms",
          "Wufoo"
        ]
      }
    ],
    "competitors": [
      {
        "name": "Fillout",
        "mentions": 7,
        "answer_share": 47
      },
      {
        "name": "Microsoft Forms",
        "mentions": 3,
        "answer_share": 20
      },
      {
        "name": "Zoho Forms",
        "mentions": 3,
        "answer_share": 20
      }
    ],
    "citation_opportunities": [
      {
        "domain": "hubspot.com",
        "answers": 3,
        "examples": [
          {
            "url": "https://blog.hubspot.com/marketing/form-builder-tools?region=united-states",
            "title": "HubSpot's form builder guide"
          }
        ]
      }
    ]
  }
}
```

### Keyword opportunities (answers at once)

```bash
curl -X POST https://api.magistermarketing.com/v1/keywords/research \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"tally.so","seed":"online form builder","limit":15}'
```

```json
{
  "data": {
    "domain": "tally.so",
    "seed": "online form builder",
    "broadened_seed": null,
    "partial": false,
    "failed_sections": [],
    "site": {
      "ranking_keywords": "100-1k",
      "paid_keywords": "under 10",
      "monthly_search_visits": "1k-10k"
    },
    "keywords": [
      {
        "keyword": "form online builder",
        "source": "suggestion",
        "opportunity": 78,
        "intent": "commercial",
        "demand": "10k-100k",
        "difficulty": "easy",
        "position": null,
        "cluster": "online form builder",
        "action": "Make a page that compares the options and makes your case."
      },
      {
        "keyword": "free online html form builder",
        "source": "suggestion",
        "opportunity": 58,
        "intent": "commercial",
        "demand": "1k-10k",
        "difficulty": "easy",
        "position": null,
        "cluster": "html",
        "action": "Make a page that compares the options and makes your case."
      }
    ],
    "clusters": [
      {
        "cluster": "online form builder",
        "keywords": 11
      },
      {
        "cluster": "website",
        "keywords": 5
      }
    ]
  },
  "credits_charged": 9.92,
  "request_id": "560c6fae-a58b-4a50-b9eb-059fd6531004"
}
```

### Search competitors (answers at once)

```bash
curl -X POST https://api.magistermarketing.com/v1/competitors/discover \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"tally.so","limit":6}'
```

```json
{
  "data": {
    "domain": "tally.so",
    "competitors": [
      {
        "domain": "jotform.com",
        "overlap": 58,
        "standing": "ranks_higher",
        "search_reach": "100k+"
      },
      {
        "domain": "typeform.com",
        "overlap": 45,
        "standing": "about_even",
        "search_reach": "10k-100k"
      },
      {
        "domain": "forms.app",
        "overlap": 37,
        "standing": "ranks_lower",
        "search_reach": "10k-100k"
      }
    ],
    "site": {
      "search_reach": "1k-10k"
    },
    "note": null
  },
  "credits_charged": 3.2,
  "request_id": "1a18e09d-3822-41fd-9215-e7cf988cff42"
}
```

### Page speed for real visitors (free, answers at once)

```bash
curl -X POST https://api.magistermarketing.com/v1/web/page-speed \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://tally.so"}'
```

```json
{
  "data": {
    "origin": "https://tally.so",
    "score": 99,
    "rating": "needs_improvement",
    "devices": {
      "phone": {
        "score": 98,
        "rating": "needs_improvement",
        "metrics": {
          "largest_contentful_paint": {
            "p75": 2020,
            "rating": "good"
          },
          "interaction_to_next_paint": {
            "p75": 229,
            "rating": "needs_improvement"
          },
          "cumulative_layout_shift": {
            "p75": 0,
            "rating": "good"
          }
        }
      },
      "desktop": {
        "score": 100,
        "rating": "good",
        "metrics": {
          "largest_contentful_paint": {
            "p75": 1263,
            "rating": "good"
          },
          "interaction_to_next_paint": {
            "p75": 77,
            "rating": "good"
          },
          "cumulative_layout_shift": {
            "p75": 0.01,
            "rating": "good"
          }
        }
      }
    },
    "fixes": [
      {
        "metric": "interaction_to_next_paint",
        "rating": "needs_improvement",
        "fix": "The page is slow to respond to taps and clicks: break up long JavaScript tasks, defer third-party scripts and load chat or tracking widgets after the page is usable."
      }
    ],
    "field_data": true,
    "note": null,
    "source": "Chrome UX Report (CrUX), Google, CC BY 4.0"
  },
  "credits_charged": 0,
  "request_id": "673502b3-11ee-41a1-a4e3-4b6f9e4e18f8"
}
```

The rating is Google's pass mark: every metric must be good, so a score of 99 with one metric over its bar reads `needs_improvement`.

### The full marketing audit (a job)

```bash
curl -X POST https://api.magistermarketing.com/v1/audits/site \
  -H "Authorization: Bearer $MAGISTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://tally.so"}'
```

This one took 1 min 17 s:

```json
{
  "job_id": "6a7defd2-d26b-4ab0-b0f6-a804e41a9856",
  "kind": "site_audit",
  "status": "succeeded",
  "created_at": "2026-11-11T04:08:51.896908+00:00",
  "finished_at": "2026-11-11T04:10:09.145871+00:00",
  "credits_charged": 103.74,
  "result": {
    "website_url": "https://tally.so",
    "status": "complete",
    "health": {
      "current": 64,
      "subscores": {
        "ai_search": {
          "current": 87
        },
        "google_search": {
          "current": 20
        },
        "website_and_content": {
          "current": 86
        }
      },
      "tabs": {
        "google_search": {
          "score_current": 20,
          "score_basis": "Keyword research: the site is on Google's first two pages for 4 of 10 keywords looked up.",
          "keywords": [
            {
              "demand": "1k-10k",
              "keyword": "free form builder",
              "position": null
            },
            {
              "demand": "1k-10k",
              "keyword": "free online form builder",
              "position": null
            },
            {
              "demand": "100-1k",
              "keyword": "no-code form builder",
              "position": null
            }
          ],
          "findings": [
            {
              "title": "No top-100 visibility for the listed searches",
              "detail": "Tally does not appear in the top 100 for any of the five listed searches, including “online form creator” and “online form maker,” each with 10k-100k monthly searches.",
              "severity": 5
            }
          ],
          "opportunities": [
            {
              "title": "Make a page for online form makers",
              "detail": "Create a page that directly answers searches like “online form creator” and “online form maker,” with examples of what people can build and a clear invitation to try Tally."
            }
          ]
        },
        "website_and_content": {
          "score_current": 86,
          "checklist": [
            {
              "note": "Found: Get started",
              "label": "Clear call to action",
              "status": "pass"
            },
            {
              "note": "Found: Organization, ContactPoint, WebSite, SoftwareApplication",
              "label": "Schema markup",
              "status": "pass"
            }
          ]
        }
      }
    }
  }
}
```
