Docs
REST API
Programmatic, read-only access to your reports and AI-visibility data with team-scoped API keys.
What the REST API is
The CrunchJunkie REST API gives you programmatic, read-only access to the same data the app and the MCP connector expose — your clients, reports and their live data, and AI-visibility metrics — over plain HTTPS with JSON responses. It's built for servers, cron jobs, data warehouses and no-code tools like Zapier or Make: anywhere you want your CrunchJunkie numbers without a person clicking through the dashboard.
Two things are true by design. It's read-only: every v1 endpoint fetches, nothing writes, so a leaked key can never change or delete anything or spend budget. And it's team-scoped: a key belongs to exactly one workspace and only ever returns that workspace's data. The API is available on the Pro, Agency and Scale plans. (The MCP connector is on every paid plan.)
Creating an API key
Open Settings → REST API. Only a workspace owner or admin can create or revoke keys — a key can read the whole workspace, so it isn't a member-level action.
Give the key a name (e.g. "Zapier production") and click Create. The full key — it looks like cj_live_ab12cd34.xxxx… — is shown exactly once. Copy it and store it somewhere safe (a secrets manager, your tool's credential field); we only ever keep a hash, so we can't show it to you again. If you lose it, revoke it and mint a new one.
Revoking is instant: click Revoke on a key and any integration using it stops working on its next request. Keys don't expire on their own unless you set an expiry. A key belongs to the workspace, but when a member leaves, the keys they created in it are revoked with them: when you remove them under Settings → Team (the removal asks first and says how many), and when they delete their own account (its confirmation says how many). The keys stay listed here as revoked.
Making a request
The base URL is https://app.crunchjunkie.io/api/v1. Send your key as a bearer token in the Authorization header:
curl -H "Authorization: Bearer cj_live_ab12cd34.xxxx…" https://app.crunchjunkie.io/api/v1/clients
Every response is JSON. Success looks like { "data": … , "meta": { … } }; an error looks like { "error": { "code": "…", "message": "…" } } with the matching HTTP status (401 unauthenticated, 403 wrong scope or plan, 404 not found, 429 rate-limited, 500 server error).
Endpoints
v1 is read-only and mirrors the MCP read tools, so the numbers match the app exactly:
• GET /api/v1/clients — your clients, their connected providers and report counts.
• GET /api/v1/reports?clientId=… — your reports (ids for the calls below). Reports in Deleted are left out; add includeDeleted=true to list them too, marked deleted: true.
• GET /api/v1/reports/{id} — one report's definition (widgets, date config).
• GET /api/v1/reports/{id}/data?datePreset=…&comparePreset=… — the report's live assembled data (KPIs, charts, tables). A preset resolves in the workspace's time zone, as in the app, and the response carries the resolved dateRange. Add brandFilter=branded or brandFilter=nonbranded to read its Search Console widgets, and every other widget that counts search queries or keywords, for that group only.
• GET /api/v1/ai-visibility/projects?clientId=… — your AI-visibility projects.
• GET /api/v1/ai-visibility/projects/{id}/metrics?dateFrom=…&dateTo=…&model=…&market=…&source=…&limit=…&offset=… — AI-visibility metrics over whole UTC days (by default the last 30, at most 180): each brand pooled over every answer (answers naming it ÷ answers, with the answers as n and the ± margin), the client per engine, and the snapshot rows behind them, each with its engine label and source. Sentiment is on 0–100 with 50 neutral, and position 1 means named first; both are null, with avgPositionMentions / sentimentMentions and a reason, when fewer than 10 mentions carry them; meta.note states the pooling rule. A call returns at most 5,000 snapshot rows (limit, 1 to 5,000; 500 when omitted); page with offset until you hold meta.total rows.
• GET /api/v1/ai-visibility/projects/{id}/prompts?days=…&activeOnly=…&q=… — the tracked prompts, each with its visibility pooled over its answers in the window and the answers as n. activeOnly=true (the default) lists the prompts scans ask; activeOnly=false adds paused prompts, marked active: false, whose stored answers are kept. The API never changes a prompt. A prompt deleted in the app or through the MCP connector is gone with every answer stored for it, so this list no longer includes it, for past periods either.
• GET /api/v1/ai-visibility/projects/{id}/answers?scanJobId=…&days=…&model=…&promptId=…&market=…&brandMentioned=…&source=…&includeText=…&limit=…&offset=… — the AI answers behind those figures, one per prompt, engine and run, newest first: the answer's id, the scan it came from (scanJobId), when it ran, the engine with the model the provider reported, the market, the prompt with its category and tags, and for your brand and every tracked competitor whether the answer named it, at which position and with what sentiment (0–100, 50 is neutral), plus the URLs it cited and the ones it only retrieved where the engine says which, and a 280-character excerpt. includeText=true adds the full text (up to 30,000 characters; a page then holds at most 25 answers, otherwise 100, 50 by default). With scanJobId you get every answer that one scan stored, whatever its date; an answer marked superseded: true was asked again later the same day on the same engine and no longer counts. An answer can become superseded after you read it, if a re-scan runs later that day, so re-read a day's scans before you pool it. Without scanJobId you get the answers the figures count over the window (30 days by default, 1 to 180; outside that a 400). Pool rates yourself as answers naming the brand ÷ answers, never as an average of per-prompt or per-engine rates, or your numbers will not match the app. To sync after every scan, read by scanJobId: it is cheap, and a finished scan's list does not move. A window list is newest first, so a scan finishing while you page shifts its offsets; key your rows by answerId. market=de-DE reads one market, exactly as listed in the locale of each answer (the same value /metrics takes as market); a market with no answers in the window returns an empty list and meta.note names the markets that have some. Page with meta.nextOffset; a limit outside its range is a 400.
• GET /api/v1/ai-visibility/projects/{id}/scans?status=…&limit=…&offset=… — a project's scans, newest first: id, status (queued, running, done, stopped, error), what started it, when it started and ended, the engines and markets it covered, prompts, runs per prompt, planned checks and the answers it stored. Use it to catch up on a scan.completed event your endpoint missed.
In /reports/{id}/data a KPI metric's prev is the comparison period's value. It is null when there is nothing to compare with: no comparison period, no earlier data for that metric, a comparison period the source could not be read for, or a figure that only exists as it stands today (catalog counts, review totals, follower totals). It is a number, 0 included, when the comparison period was measured: 0 is a real previous 0, from which there is no percentage change. Every KPI widget carries prevNull: true; meta.note says the same. A KPI metric's value is null when there is no figure: the read failed, or the value cannot be computed (for example cost per conversion with 0 conversions). It never stands for 0.
A Search Console or Rank Tracking chart in /reports/{id}/data can carry googleUpdates: the Google ranking updates that ran during its dates, from Google's Search Status Dashboard (name, link, begin and end as US Pacific days, end null while still rolling out). They show timing only, not a cause, and meta.googleUpdates repeats that whenever a chart carries them.
A Google Ads donut by device or network of a rate, a share or an average (CTR, CPC, impression share, cost per conversion, ROAS) has no slices in /reports/{id}/data and the MCP get_report_data: its emptyReason is "gads_not_summable", since such a figure per device is no part of a total. A Google Ads map of such a metric carries notSummable: true, and its countries' figures are each country's own rate, never shares to add up. meta.gadsNotSummable says so for such a donut and meta.geoNotSummable for a map of any source (a Google Analytics map of Users or a rate too), and the widget's aiNote names the metric. A donut of any other source works the same way since 3.187: a rate, an average, a ratio, reach or unique users (CTR, ROAS, bounce rate, average position, users, visitors) has no slices and the emptyReason "not_summable", and meta.donutNotSummable says so. A donut of a count or an amount keeps its 9 biggest slices and one "Other" with the rest, so its slices add up to every row the source returned (Google Analytics returns up to 10,000 rows, Matomo and Piwik PRO up to 1,000; rows past that are in no slice).
Table column names (cols) stay English ids; display cells are written in the report's language, as the report prints them — since 14 October 2026 that includes the Type and Source cells of the Cited URLs table and the Issue cells of the Source health table (English before), in /reports/{id}/data and the MCP get_report_data alike.
An AI-visibility source or citation widget in /reports/{id}/data (and in the MCP get_report_data) that left out answers whose URLs come from model memory, not a search, says how many: memoryExcluded on a table, donut or chart, fromMemory on each affected KPI metric. Those answers are in neither the numerator nor the denominator; the citation and content-gap endpoints report the same count as answersFromMemoryExcluded. Any AI-visibility table with a comparison can also carry withinMargin, the [row, column] cells whose change is not at least twice its margin of error: in a Brand table a cell's comparison, in a Prompt table its Δ Visibility cell, in a tag table its Δ columns and the change under a value. The report prints them grey with "~", so read such a change as unchanged — read this field rather than the "~" in the text.
The machine-readable OpenAPI 3.1 spec is published at https://app.crunchjunkie.io/api/v1/openapi.json — import it into Postman, Insomnia or your codegen of choice.
Stability and versioning
v1 is a contract you can build on. Within v1 we only ever add: a new endpoint, a new optional parameter, a new field in a response or a webhook payload, or a new value in a list such as engine ids, scan statuses or answerEnd. We never remove or rename a field, or change its type or meaning. So write your integration to ignore fields it doesn't know and to treat engine ids, statuses and similar values as open sets. A change that would break this comes as a new version (v2) next to v1. We announce it by email at least six months ahead to the owners of every workspace that uses an API key or a webhook endpoint, and v1 keeps working for at least six months after v2 is available. Every addition is in the changelog, and the OpenAPI file at /api/v1/openapi.json always describes what the API returns today.
Rate limits, pagination and accuracy
Each key gets a token-bucket rate limit; if you exceed it you get a 429 with a Retry-After header telling you how many seconds to wait. Large collections return the most-recent rows with a meta.total and a meta.truncated flag, so a capped page is never passed off as the whole set — narrow by date or model to reach older data.
One accuracy note that matters if you compute your own aggregates: AI-visibility metrics follow the metric contract — pool a rate by summing the counts and the runs and dividing once (Σ visibilityCount ÷ Σ totalRuns), never by averaging per-row percentages, and keep each market (locale) as its own population. The app does this for you; do the same downstream and your numbers will match ours.
Getting started Reporting Report templates Filtering, dimensions & comparisons Sharing & delivering reports White-label & branding Metrics glossary AI visibility Your own AI API keys Perception AI Shopping Visibility GEO Audit AI crawlers & crawlability AI traffic from GA4 Rank Tracker SEO audit Keyword research Links Monitoring Signals & Automations Integrations Data Studio Connect to Claude & ChatGPT (MCP) Connect Claude Connect Claude Code Connect ChatGPT Connect Cursor Connect VS Code Webhooks Single sign-on (SSO) Account & billing Data, privacy & security The Crunch assistant Refer a friend FAQ