Skip to main content
LLM Metrix
REST API · MCP · v1

API documentation

Read your projects and scan history, trigger scans, manage prompts and webhooks, attribute AI referrals from your own site, and upload server access logs over REST or the MCP server. Every request is authenticated with a personal access token you create in the dashboard.

Base URL
https://llmmetrix.com

Authentication

Every endpoint takes a bearer token in the Authorization header. Tokens are prefixed llmx_; anything without that prefix is rejected before a database lookup happens.

Request
curl https://llmmetrix.com/api/v1/projects \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"

Creating a key

  • Read keys are created in the dashboard under Account → API keys. Beacon (collector) keys are minted from the AI Traffic page; see Installing the beacon.
  • The raw token is shown exactly once, at creation. It is not retrievable afterwards and support cannot recover it. Copy it into your secret store immediately. If you lose it, revoke the key and create another.
  • Only the SHA-256 hash of the token is stored. Authentication hashes the presented token and looks up that hash, so a database dump contains no usable credentials. Alongside it we keep the first 12 characters as a display prefix (e.g. llmx_ab12cd3) so you can tell your keys apart in the UI, plus a last_used_at timestamp updated on each authenticated call.
  • Keys are long-lived by default but can carry an optional expiry: pass expires_at (an ISO datetime in the future) at creation and the key authenticates as nothing once past it. Revoke any key at any time from the same screen; revocation takes effect on the next request.
  • API + MCP access requires a paid plan (Business or Agency). Keys can also be scoped by capability: pass scopes: ["read"] for a monitoring credential that cannot trigger scans, or ["write"] for a pipeline credential that cannot list. The default is both. Rate limits scale with your plan (see Rate limits).

The two key kinds, and why the split exists

Every key carries a kind. The two are not interchangeable, and the separation is the single most important property of this API's security model.

API key kinds
KindCan doCannot doWhere it lives
apiRead: every GET /api/v1 endpoint. Write: POST /api/v1/scans (trigger a scan), retry jobs, manage tracked prompts and the webhook URL, POST /api/ingest/logs. Also drives the MCP server.Rejected by POST /api/collect.Your server, your secret manager, your CI. Never in a browser.
collectorAppend AI-referral events via POST /api/collect, for the one project it is bound to.Rejected by every /api/v1 route (read AND write) and by the MCP server. Treated exactly like an unknown key.Your public page source. It is publishable by construction.

Why this matters

A first-party analytics tag has to carry a credential in the page source, which means anyone who views source has it. The usual outcome is that a single API credential ends up both published and capable of reading tenant data. We split the credential instead:

  • The shared authentication helper for the read API rejects a collector key before it returns a session. The key resolves to the same “invalid key” result as a random string. So the credential in your page source cannot be turned around and used to read your scans, projects or competitors.
  • Conversely the beacon rejects a full api key. You cannot accidentally paste a read key into your site template and have it work. It returns 401, which is the failure mode you want.
  • A collector key must be bound to exactly one project at creation; an unscoped or multi-project collector key is refused with 400. The beacon then derives its target project from the key's own binding and never from the request body, so a scraped key cannot be pointed at another project or another tenant.
  • Both write endpoints re-check, on every request, that the key's owner is still a member of the project's workspace. Removing someone from a workspace kills their keys' access immediately, rather than at the next key rotation.

Project scoping

A key can be scoped to a list of projects at creation (up to 100 project UUIDs; every one is validated against the creator's own workspace memberships, so a key can never name a project its creator cannot reach). An unscoped key sees every project in every workspace its owner belongs to.

  • GET /api/v1/projects intersects your memberships with the key's scope.
  • GET /api/v1/scans returns 403 project not in key scope when a scoped key asks for a project outside its scope.
  • POST /api/ingest/logs returns 403 project not in key scope for an out-of-scope project, a project that does not exist, or a project whose workspace you have since left.
  • The write endpoints inherit the same boundary: triggering a scan, listing or retrying jobs, and managing tracked prompts all resolve the target project through the same membership ∩ scope allowlist. An out-of-scope target reads as 404 (trigger, retry, prompts) or an empty list (jobs, prompts), never as another tenant's data.

Rate limits

Rate limits by direction
SurfaceLimitBucketResponse
GET /api/v1/projects
GET /api/v1/scans
GET /api/v1/jobs
GET /api/v1/jobs/[jobId]
GET /api/v1/prompts
GET /api/v1/webhooks
MCP tools
120 requests / minute on Business · 600 / minute on AgencyPer key: every read endpoint AND the MCP tools share one counter.429 with a Retry-After header (seconds).
POST /api/v1/scans
POST /api/v1/jobs/[jobId]/retry
POST /api/v1/prompts
PATCH /api/v1/prompts/[id]
DELETE /api/v1/prompts/[id]
PUT/DELETE /api/v1/webhooks
60 requests / minute on Business · 300 / minute on AgencyPer key: all write endpoints share one counter, separate from reads.429 with a Retry-After header (seconds).
POST /api/ingest/logs10 uploads / minutePer key.429 {"error":"Rate limit exceeded (10 uploads/minute)"} with a Retry-After header (seconds).
POST /api/collect240 requests / minutePer project: one misbehaving tag install cannot exhaust the shared write path.429 {"error":"Rate limit exceeded"} with a Retry-After header (seconds).

Windows are sliding, one minute wide, and every 429 carries a Retry-After header in seconds; the callers here are scripts with no human to read a JSON message and back off. Two layers sit underneath the write limits and are documented on the endpoints they gate: triggering a scan runs the full dashboard gate ladder (cooldown, budget gates, per-user rate and concurrency caps), and retrying a job is throttled at 5 restarts / 10 minutes per user.

Errors

Errors are JSON with an error field. A 500 additionally carries a short ref correlation code that is also written to our server logs. Quote it in a support ticket and we can find the exact failure.

Status codes
StatusMeaning
200Success. On /api/ingest/logs, 200 specifically means the upload was deduplicated onto an existing job.
202Accepted: a scan was queued ( POST /api/v1/scans returns the jobId to poll), a job was restarted, or /api/ingest/logs queued a new parse job.
400Malformed JSON, invalid parameters, or a body that failed schema validation.
401Missing, malformed, unknown, or wrong-kind key.
402A plan, budget or payment gate: prompt caps reached (trigger a scan, create/activate a prompt), a suspended workspace, or a capability your plan does not include (log ingest needs Agency). The beacon never 402s. A plan or origin rejection there answers 200 {"ok":true,"stored":false} indistinguishably, because the collector key is public by design.
403Authenticated, but not permitted: project outside the key's scope, or an unverified domain. A beacon Origin that does not belong to the project's site is not 403. The collector answers 200 {ok:true,stored:false} so a public key cannot be probed by status code.
404Not found, including jobs, scans or prompts the key cannot see, which are deliberately indistinguishable from missing ones. Also the collector key's bound project no longer exists.
409Conflict: a job is not restartable, or its row changed state mid-flight.
413Body over the size cap (8 KB for the beacon, 4 MB for log ingest).
429Rate limited. See above.
500Server error. Body is {"error":"…","ref":"a1b2c3d4"}.
503A dependency is unavailable or not configured (the collector's database, or log storage).
GET

/api/v1/projects

Lists the projects visible to the key: every project in every workspace the key's owner belongs to, intersected with the key's project scope. Newest first.

Takes no query parameters and is not paginated: the full list is returned in one response, ordered by created_at descending.

Request
curl https://llmmetrix.com/api/v1/projects \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"
200 Response
{
  "data": [
    {
      "id": "8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90",
      "domain": "acme.com",
      "brand_name": "Acme",
      "industry": "B2B SaaS",
      "competitors": ["globex.com", "initech.com"],
      "engines": ["chatgpt", "perplexity", "gemini", "claude"],
      "created_at": "2026-04-02T09:14:22.881Z",
      "workspace_id": "1d2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b"
    }
  ]
}

If the key's owner belongs to no workspace, the response is {"data": []} rather than an error.

GET

/api/v1/scans

Returns completed visibility scans with their per-engine aggregates and per-answer detail, newest first.

Query parameters for GET /api/v1/scans
ParameterTypeDefaultNotes
project_idUUIDnoneOptional. Restricts results to one project. Omit it to get scans across every project the key can reach.
limitinteger20Clamped to 1–100. Values outside the range are clamped, not rejected; a non-numeric value falls back to 20.
offsetinteger00-based page offset. Use with limit to page through history.

What this endpoint deliberately excludes

  • Only status: "completed" scans are returned; in-flight and failed scans never appear.
  • Only scans of your own brand in the project's default market. Competitor-subject benchmark scans and additional-region scans are stored on the same project but are filtered out here, because they carry no distinguishing marker in this response shape. Mixing them in would silently corrupt any dashboard that trends or sums these scores.
  • Engines whose every call failed are omitted from engine_results, and individual answers whose engine call errored are omitted from prompt_results. An infrastructure failure reads as absent data, not as zero visibility.

Pagination is limit + offset; page through history a window at a time. The endpoint reads the most recent window of history by default.

Request
curl "https://llmmetrix.com/api/v1/scans?project_id=8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90&limit=5" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"
200 Response (truncated)
{
  "data": [
    {
      "id": "b6c1a2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
      "project_id": "8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90",
      "status": "completed",
      "score": 62,
      "summary": "Acme is cited in 6 of 10 answers, most often behind Globex.",
      "created_at": "2026-07-29T07:03:11.402Z",
      "engine_results": [
        {
          "engine": "chatgpt",
          "label": "ChatGPT",
          "score": 71,
          "mentionRate": 0.8,
          "avgRank": 2.1,
          "sentiment": "positive",
          "prompts": 10
        }
      ],
      "prompt_results": [
        {
          "engine": "chatgpt",
          "label": "ChatGPT",
          "prompt": "best invoicing software for agencies",
          "mentioned": true,
          "rank": 2,
          "sentiment": "positive",
          "competitorsMentioned": ["Globex"],
          "citations": [
            {
              "url": "https://acme.com/pricing",
              "domain": "acme.com",
              "title": "Acme Pricing",
              "position": 1
            }
          ],
          "answerExcerpt": "For agencies, Acme and Globex are the two…",
          "answerText": "For agencies, Acme and Globex are the two most…",
          "claimsAboutBrand": ["Acme offers unlimited seats"],
          "accuracyRisk": "low"
        }
      ]
    }
  ]
}
Response field notes
FieldNotes
score0–100 visibility score for the scan. Nullable.
engine_results[].mentionRateFraction (0–1) of that engine's delivered answers mentioning the brand.
engine_results[].avgRankAverage position within the answer, or null when the brand was never ranked.
engine_results[].promptsNumber of answers actually delivered by that engine (errored calls are not counted, and are not billed).
sentimentOne of "positive", "neutral", "negative".
accuracyRiskOne of "none", "low", "medium", "high": the hallucination risk of the claims the answer makes about your brand.
citations[].urlThe real cited URL on search-grounded engines; null on ungrounded ones, where only the domain could be extracted from the answer prose.
answerExcerptFirst 280 characters of the answer. answerText carries the full text.
POST

/api/v1/scans

Triggers a real visibility scan and returns the jobId to poll. 202 means queued, not completed.

Body: {"projectId":"…","competitor?":"…","region?":"…"}. The scan runs through the same gate ladder as the dashboard (cooldown, weekly/daily/monthly budget gates, payment suspension, per-user rate and concurrency caps), so a script cannot spend where the UI would refuse. Poll GET /api/v1/jobs/[jobId] until it reaches a terminal status, then read the answers via GET /api/v1/scans.

Gate notes

  • A 402 carries the gate that refused: { error, plan, used, limit } for prompt caps, or account_suspended for a payment-suspended workspace.
  • A 429 is the cooldown (60 s between scans of the same subject+market) or an in-flight scan; the Retry-After header is a poll interval, not a deadline.
  • 403 domain_blocked when the project's domain is on the acceptable-use blocklist.
Request
curl -X POST "https://llmmetrix.com/api/v1/scans" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90"}'
202 Response
{
  "jobId": "9f2f4e6d-5c4b-4a3b-9e8f-1a2b3c4d5e6f"
}
GET

/api/v1/jobs

Lists background jobs visible to the key, newest first.

Visibility is the same three-tier union as the dashboard's activity page: jobs of projects in scope, jobs of workspaces the owner belongs to, and the owner's own user-tier jobs (exports, notifications). Optional filters: status (one of pending, processing, completed, failed, dead, canceled), type (e.g. scan), project_id, limit (default 50, max 100) and offset.

Request
curl "https://llmmetrix.com/api/v1/jobs?status=failed&limit=10" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"
GET

/api/v1/jobs/[jobId]

Returns one job: status, attempts, error and the scan coverage snapshot.

The poll endpoint for a triggered scan. The attempts/max_attempts pair answers the retryable-vs-give-up question. A job the key cannot see is 404, indistinguishable from a missing one. coverage carries the per-engine coverage snapshot for scan jobs (which engines returned nothing), never the full result payload.

Request
curl "https://llmmetrix.com/api/v1/jobs/9f2f4e6d-5c4b-4a3b-9e8f-1a2b3c4d5e6f" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"
200 Response
{
  "data": {
    "id": "9f2f4e6d-5c4b-4a3b-9e8f-1a2b3c4d5e6f",
    "type": "scan",
    "status": "completed",
    "error": null,
    "attempts": 1,
    "max_attempts": 5,
    "project_id": "8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90",
    "created_at": "2026-08-11T07:00:00.000Z",
    "updated_at": "2026-08-11T07:00:42.000Z",
    "started_at": "2026-08-11T07:00:00.000Z",
    "finished_at": "2026-08-11T07:00:42.000Z",
    "coverage": { "engines": ["chatgpt", "perplexity"], "failedEngines": [] }
  }
}
POST

/api/v1/jobs/[jobId]/retry

Re-drives a failed, dead or stale-processing job through the same restart ladder as the dashboard.

Restarts the same job row, so a re-run can never double-charge (the credit ledger is guarded on the job id). Gate notes: 409 when the job is not restartable or its row moved mid-flight; 402 for a suspended workspace or AI budget refusal; 403 for viewers. On top of the 60 writes/minute bucket, restarts are throttled at 5 / 10 minutes per user.

Request
curl -X POST "https://llmmetrix.com/api/v1/jobs/9f2f4e6d-5c4b-4a3b-9e8f-1a2b3c4d5e6f/retry" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx"
202 Response
{
  "jobId": "9f2f4e6d-5c4b-4a3b-9e8f-1a2b3c4d5e6f",
  "restarted": true,
  "status": "pending"
}
GET

/api/v1/prompts?project_id=…

Lists a project's tracked prompts (active first, then newest).

project_id is required. An out-of-scope project reads as an empty list, never as another tenant's prompts. When a project has active tracked prompts, scans run exactly those instead of auto-generated discovery prompts.

POST

/api/v1/prompts

Adds a tracked prompt, enforcing the per-plan active-prompt cap.

Body: {"projectId":"…","text":"…"}, text 1–500 chars. At the cap the response is 402 { error, plan, limit }; deactivate one prompt or upgrade. The count is enforced under an advisory lock, so concurrent adds cannot slip past it.

PATCH

/api/v1/prompts/[id]

Activates or deactivates a tracked prompt.

Body: {"active": true | false}. Turning a prompt on is capped by the same per-plan active limit as creation; turning one off never is.

DELETE

/api/v1/prompts/[id]

Deletes a tracked prompt.

Same role gate as the dashboard's delete: the key owner must hold owner, admin or member in the prompt's workspace. Returns { ok: true }.

Webhooks

A webhook URL that receives scan.completed deliveries on behalf of the key's owner, the same per-user setting as the dashboard's notification settings, delivered through the same queue and SSRF-guarded HTTP client. URLs must be https:// (http is rejected with a 400). { webhook_url: null } clears it.

GET

/api/v1/webhooks

Returns the configured webhook URL (or null).

PUT

/api/v1/webhooks

Sets the webhook URL.

Request
curl -X PUT "https://llmmetrix.com/api/v1/webhooks" \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://hooks.example.com/scan-completed"}'
DELETE

/api/v1/webhooks

Clears the webhook URL.

MCP server (Model Context Protocol)

The public API speaks MCP over Streamable HTTP at https://llmmetrix.com/api/mcp. Point Claude Desktop, Cursor or any MCP client at it with the same Authorization: Bearer llmx_… header. Reads: list_projects, list_scans, get_scan (full answers for one scan), get_job, list_prompts, get_webhook. Writes: trigger_scan (returns a job_id to poll with get_job), create_prompt, set_prompt_active, delete_prompt, set_webhook, clear_webhook, retry_job.

Every tool is the identical REST request over a different transport: reads share one read bucket, writes one write bucket (per key, at your plan's rate tier; see Rate limits above), and per-tool access falls out of the same membership ∩ scope allowlist the REST routes use: a project-scoped key can only ever read or write projects in its own scope, the write actions run the same per-plan caps and payment gates as the dashboard, and a collector key is rejected like any unknown key. Serving is STATELESS (MCP revision 2026-07-28): every request stands alone (no handshake, no session id, no affinity), while 2025-era clients are still served through a stateless fallback. The openapi.json contract below documents the REST surface the tools mirror.

API + MCP access requires a paid plan (Business or Agency) and rides your key's capability scopes: a key minted with scopes: ["read"] sees only the six read tools, one with ["write"] only the seven write tools: an absent scope is an absent tool in tools/list, not a refused call. Keys can also carry an optional expires_at; an expired key authenticates as nothing.

Connecting from Claude Desktop

claude_desktop_config.json
{
  "mcpServers": {
    "llmmetrix": {
      "type": "http",
      "url": "https://llmmetrix.com/api/mcp",
      "headers": { "Authorization": "Bearer llmx_xxxxxxxxxxxx" }
    }
  }
}

OpenAPI contract

The machine-readable contract lives at https://llmmetrix.com/openapi.json: OpenAPI 3.1, generated from the request schemas the API actually validates with (so the two cannot drift), with the response shapes this page documents. Use it to generate typed clients for your language of choice.

POST

/api/collect

The public AI-referral beacon. Called from your own site on page load with a collector key; records visits that arrived from an AI assistant. Runs on the edge, accepts cross-origin requests, and returns no readable tenant data.

POST /api/collect body fields
FieldTypeNotes
keystring ≤ 200The collector key. Sent in the body because navigator.sendBeacon cannot set headers; an Authorization: Bearer header also works and takes precedence.
referrerstring ≤ 2048document.referrer. Defaults to empty.
pagestring ≤ 2048Landing page: a path ("/pricing") or a full URL. Only the path component is stored; query and hash are discarded because they can carry personal data.
utmobjectsource, medium, campaign, term, content. utm_source is the primary classification signal.
sessionIdstring ≤ 64Optional opaque session id.
projectIdUUIDAdvisory only. Validated if present, then ignored; the target project always comes from the key's own binding.

The body is validated strictly: unknown fields are rejected. The raw body is capped at 8 KB before parsing.

Request (server-side sender)
curl -X POST https://llmmetrix.com/api/collect \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx" \
  -H "Content-Type: text/plain" \
  -d '{
    "referrer": "https://chatgpt.com/",
    "page": "/pricing",
    "utm": { "source": "chatgpt", "medium": "ai_referral" }
  }'
200 Response
// Classified as an AI referral and stored:
{ "ok": true, "stored": true, "source": "chatgpt", "matchedBy": "referrer" }

// Acknowledged but NOT an AI-attributed visit (deliberately not stored):
{ "ok": true, "stored": false }
POST /api/collect status codes
StatusBodyCause
400{"error":"Invalid JSON body"} / {"error":"Invalid payload","details":…}Unparseable body, or a field that failed validation.
401{"error":"Missing or invalid write key"}No key, an unknown key, a full api-kind key, a collector key with no bound project, or an owner who is no longer a member of the project's workspace.
200{"ok":true,"stored":false}Acknowledged, nothing stored. Returned for a visit that was not an AI referral, and (deliberately indistinguishably) when AI traffic attribution is not on the workspace's plan or the browser Origin does not fall under the project's domain. A collector key is public by design, so a distinct status for either would let anyone holding one read the workspace's plan tier or probe its bound domain.
404{"error":"project not found"}The key's bound project has been deleted.
413{"error":"Payload too large"}Body over 8 KB.
429{"error":"Rate limit exceeded"}Over 240 requests/minute for this project. Carries Retry-After.
503{"error":"Collector not configured"}The collector's backing store is unavailable on this deployment.

What is stored, and what is not

  • Stored: the matched AI source, the landing path, your utm_* values, the optional session id, and a country code taken from the edge platform's own geo header, never from anything the caller sends.
  • Not stored: cookies (none are set), IP addresses, query strings, hashes, or any visit that did not classify as an AI referral.
  • Classification: utm_source wins whenever it matches a known AI source, because AI apps routinely strip the referrer; the referrer host is the fallback. The response tells you which decided it via matchedBy.

Integrity, stated plainly

Because the collector key is published in your page source, anyone who reads it can append referral rows to your own project, never to anyone else's. The Origin check stops the key being embedded on an unrelated site and beaconed from a browser there, and the per-project rate limit bounds volume. A non-browser client that simply omits Origin is allowed through, because rejecting header-less requests would break the documented server-side sender path and an attacker omits the header just as easily. This residual is inherent to every first-party analytics tag.

Installing the beacon

You do not have to call /api/collect yourself. We ship a small tag that reads the referrer and utm_* parameters on page load and beacons them for you.

  1. Mint a collector key. Open Dashboard → AI Traffic, select the project, and click Generate write key. The key is bound to that one project and is shown once; the snippet on that page comes pre-filled with it.
  2. Paste the snippet into your site before </body>, or into a Google Tag Manager Custom HTML tag firing on All Pages.
    Install snippet
    <script async src="https://llmmetrix.com/tag.js"
            data-project="8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90"
            data-key="llmx_xxxxxxxxxxxx"></script>
  3. Serve it from the domain on the project. The beacon checks the browser's Origin against the project's domain: the apex and any subdomain of it are accepted (acme.com covers shop.acme.com, but not the other way round). A tag installed on an unrelated host is accepted with 200 {"ok":true,"stored":false}, not 403.
  4. Tag your AI-facing links where you can. AI assistants frequently strip the referrer, so a utm_source is the reliable signal. Recognised sources are ChatGPT, Perplexity, Gemini, Claude, Copilot and Meta AI; the AI Traffic page has a link generator that emits correctly-tagged URLs.
  5. Verify. Load a page with ?utm_source=chatgpt and confirm the beacon request returns {"ok":true,"stored":true} in your browser network panel. Referrals appear on the AI Traffic page.
Tag script attributes
AttributeRequiredNotes
data-projectYesProject UUID. The tag will not fire without it, though the server still derives the real target from the key.
data-keyYesThe collector key (llmx_…).
data-endpointNoOverride the collector URL. Defaults to /api/collect on the origin the script itself was loaded from.

The tag sends text/plain so the POST stays a CORS simple request and needs no preflight; it uses navigator.sendBeacon where available and falls back to fetch(…, { keepalive: true }). It sets no cookies and sends no personal data: referrer, path and utm_* only.

POST

/api/ingest/logs

Uploads a chunk of server access log for asynchronous parsing. Crawler hits from AI bots are extracted against our bot taxonomy and rolled up into the project's AI Traffic analytics.

Authenticated with a full api key (a collector key is rejected). The route does no parsing itself: it validates, stores the body, queues a job and returns a job id.

POST /api/ingest/logs body fields
FieldTypeNotes
projectIdUUID, requiredThe project the log belongs to.
format"auto" | "combined" | "json"Defaults to "auto". "combined" is NGINX/Apache combined format; "json" is line-delimited JSON (Vercel/CDN).
contentstring, requiredThe raw log text. 1 to 2,000,000 characters.

Domain verification is required (tier 1+)

The project's domain must be verified to at least tier 1 (the meta_tag method: <meta name="llmmetrix-site-verification" content="…"> on the home page). Otherwise the upload is refused with 403 domain_not_verified.

This is not paperwork. Scanning a domain is observation of public data and is deliberately ungated; you can track a competitor from day one. Uploading access logs is a different claim: those rows become that project's crawler analytics, so accepting logs for a domain nobody has proven they control would be a data-poisoning primitive. Tier 1 is the bar because it demonstrates control of the site while taking five minutes and no DNS ticket. Request a token from POST /api/projects/{id}/verify, or use the Domains page in the dashboard.

Request
curl -X POST https://llmmetrix.com/api/ingest/logs \
  -H "Authorization: Bearer llmx_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg c "$(tail -n 5000 /var/log/nginx/access.log)" \
    '{projectId:"8f14e45f-ceea-467a-9cbe-3e2f5d1b7a90", format:"combined", content:$c}')"
202 / 200 Response
// 202, a new parse job was queued:
{ "jobId": "3f9a1c7e-2b44-4d1a-9f0e-77c5b8a3e112", "deduped": false }

// 200, identical content was already uploaded; collapsed onto the existing job:
{ "jobId": null, "deduped": true }
POST /api/ingest/logs status codes
StatusBodyCause
400{"error":"Invalid JSON body"} or the first validation messageUnparseable body, unknown field, missing projectId, empty or over-length content.
401{"error":"Invalid or missing API key"}No key, an unknown key, or a collector key.
402{"error":"upgrade_required"}Server-log ingestion requires the Agency plan.
403{"error":"project not in key scope"}Project outside the key's scope, non-existent, or in a workspace you have left.
403{"error":"domain_not_verified"}The project's domain is not verified to tier 1.
413{"error":"Payload too large…"}Raw request body over 4 MB. Split the upload.
429{"error":"Rate limit exceeded (10 uploads/minute)"}Over 10 uploads/minute for this key.
503{"error":"storage_unavailable"}The log could not be stored. Retry the whole upload.

Two size limits, not one

content is capped at 2,000,000 characters by the schema, and the whole raw request body is capped at 4 MB: the JSON envelope plus escaping around that content. Rotate through your log in chunks and upload each one.

Uploads are idempotent on content: re-sending an identical chunk for the same project collapses onto the existing job instead of double-counting the traffic, so an interrupted batch script can simply be re-run. Uploaded logs go to private storage that no customer-facing credential can read.

Something missing?

These are the endpoints available to an API key today. If you need something that is not here (project creation, or a larger export), tell us what you are building.

Put the API to work

Create a key in the dashboard, trigger your first scan over REST or MCP, and stream the results into your own tooling.