Documentation

Available Endpoints

This page lists all endpoints that support API key authentication. Most endpoints use your property API key; everything under /api/v1/ — report generation, AIVPS, PQS, and coverage — uses your organization API key. For interactive documentation and testing, visit the API Reference.

The API Reference shows all endpoints, but only the ones listed below support external API key access. Other endpoints are for internal application use only.

AI Placement Value Score (AIVPS)

Programmatically score one or more publisher domains against the AI Placement Value Score (AIVPS) layers — organic authority, AI citation accessibility, and AI training influence. Use this to rank placement opportunities, monitor AI-era authority of partner and competitor domains, and feed SEO/PR workflows that already track traditional authority metrics.

These endpoints use an organization-level API key, not your property API key. The two are not interchangeable. Generate a key under Organization Settings → API Key. A single organization key authorizes AIVPS scoring across every domain your organization queries.

Score Publishers

Compute the full AIVPS for one or more publisher domains. If the domain is unknown to Spyglasses, a publisher record is auto-created and enriched asynchronously in the background — the first call may return "confidence": "estimated" while enrichment finishes.

POST /api/v1/ai-impact

Headers:

  • x-api-key: Your organization's AIVPS API key (required)
  • Content-Type: application/json

Body (primary use case — score a batch against a brand):

{
  "domains": ["wirecutter.com", "nytimes.com", "techcrunch.com"],
  "brandContext": {
    "propertyId": "YOUR_PROPERTY_ID"
  }
}
  • domain (string) or domains (array, 1–25) — supply exactly one. Domains are normalized (protocol/www/trailing slash stripped).
  • brandContext.propertyId (optional) — must reference a Property owned by the authenticated organization. Enables category-relevance scoring and AI training influence uplift. Omit if you don't have brand context.
  • baseline (optional) — enables Bring-Your-Own-Baseline mode. See below.

Response:

{
  "success": true,
  "results": [
    {
      "domain": "wirecutter.com",
      "publisher": {
        "id": "cm...",
        "domain": "wirecutter.com",
        "name": "Wirecutter",
        "logoUrl": "https://...",
        "aiPolicyStatus": "green",
        "organicMetricsFetchedAt": "2026-04-01T12:00:00.000Z"
      },
      "aipvs": {
        "domain": "wirecutter.com",
        "aipvs": 78,
        "tier": 1,
        "tierLabel": "Premium",
        "aiImpactMultiplier": 0.82,
        "confidence": "exact",
        "confidenceReason": "...",
        "layers": { "oab": { "..." }, "aca": { "..." }, "ati": { "..." } },
        "weightVersion": "2026-Q1",
        "scoredAt": "2026-04-12T18:30:00.000Z"
      },
      "needsEnrichment": false,
      "created": false
    }
  ],
  "errors": []
}

If any individual domain fails (for example, a missing baseline value in BYOB mode), that domain appears in errors[] and the rest of the batch still succeeds.


Score Publishers with Bring-Your-Own-Baseline (BYOB)

If your team already tracks organic authority with Ahrefs DR, Moz DA, SEMrush AS, Majestic TF, or a custom score, you can supply that baseline and have Spyglasses apply only the AI layers on top. The response's aipvsByob.compositeScore is computed as normalizedBaseline × aiImpactMultiplier.

POST /api/v1/ai-impact

Body:

{
  "domains": ["wirecutter.com", "techcrunch.com"],
  "brandContext": { "propertyId": "YOUR_PROPERTY_ID" },
  "baseline": {
    "source": "ahrefs_dr",
    "values": {
      "wirecutter.com": 88,
      "techcrunch.com": 91
    }
  }
}
  • baseline.source — one of ahrefs_dr, moz_da, semrush_as, majestic_tf, dataforseo, custom.
  • baseline.values — map of domain → baseline value. Must contain an entry for every domain you pass in.
  • baseline.customMin / customMax — required when source is custom; values are linearly rescaled to 0–100.

Response (adds an aipvsByob block per result; standard aipvs is still included for reference):

{
  "success": true,
  "results": [
    {
      "domain": "wirecutter.com",
      "publisher": { "..." },
      "aipvs": { "..." },
      "aipvsByob": {
        "baselineSource": "ahrefs_dr",
        "externalBaseline": 88,
        "compositeScore": 72,
        "tier": 2,
        "tierLabel": "Strong",
        "aiImpactMultiplier": 0.82,
        "layers": { "aca": { "..." }, "ati": { "..." } }
      },
      "needsEnrichment": false,
      "created": false
    }
  ],
  "errors": []
}

Get AI Impact Multiplier

Returns just the raw AI impact multiplier (0.40–1.00) plus the ACA and ATI layers for one or more domains. Use this when you want to combine Spyglasses' AI layers with a baseline you compute yourself at call time — the most portable form of the AIVPS signal.

POST /api/v1/ai-impact/multiplier

Headers:

  • x-api-key: Your organization's AIVPS API key (required)
  • Content-Type: application/json

Body:

{
  "domains": ["wirecutter.com", "techcrunch.com"],
  "brandContext": { "propertyId": "YOUR_PROPERTY_ID" }
}

Response:

{
  "success": true,
  "results": [
    {
      "domain": "wirecutter.com",
      "aiImpactMultiplier": 0.82,
      "layers": {
        "aca": { "composite": 0.91, "platforms": { "..." } },
        "ati": { "composite": 0.74, "dti": 0.8, "uplift": 1.1, "trainingAccessComposite": 0.84, "platforms": { "..." } }
      },
      "confidence": "exact",
      "needsEnrichment": false
    }
  ],
  "errors": []
}

AI Placement Quality Score (PQS)

Programmatically score placements against the AI Placement Quality Score factors — document position, chunk containment, placement type, editorial control, link attribute, and sentiment multiplier. When a publisher domain is supplied, the response also includes the publisher's AIPVS and a combined Total Placement Value (AIPVS × PQS / 100).

The PQS API supports both scoring modes:

  • Prospective (synchronous) — score a hypothetical placement from user-supplied inputs. Returns the full score in the POST response.
  • Retrospective (async, polling) — score an existing placement at a live URL. The POST creates a placement and enqueues the background job; the client polls a GET endpoint for the result.

All PQS endpoints use an organization-level API key, the same key used for the AIVPS endpoints above. Generate one under Organization Settings → API Key.

Score a Prospective Placement

Compute the PQS for a hypothetical placement given its scoring inputs. Use this to rank placement offers, run "what-if" scenarios before negotiating, or integrate PQS into your own media planning tools.

POST /api/v1/ai-impact/placement

Headers:

  • x-api-key: Your organization's API key (required)
  • Content-Type: application/json

Body:

{
  "placementType": "roundup_top_third",
  "editorialControl": "collaborative",
  "linkAttribute": "dofollow",
  "assumedPosition": 0.12,
  "publisherDomain": "wirecutter.com",
  "brandContext": {
    "propertyId": "YOUR_PROPERTY_ID"
  }
}
  • placementType (required) — one of dedicated_article, roundup_top, roundup_top_third, roundup_middle_third, roundup_bottom_third, passing_mention, sidebar_widget_biobox, quote_only
  • editorialControl (required) — one of full_authorship, collaborative, quoted, mentioned, unpredictable
  • linkAttribute (required) — one of dofollow, nofollow, sponsored, ugc, no_link
  • assumedPosition (optional, default 0.15) — the fraction of the document (0.0–1.0) where you expect the brand mention to land. 0.05 = first 10%, 0.5 = middle, 0.95 = footer.
  • publisherDomain (optional) — when supplied, the publisher's AIPVS is computed and combined with the PQS for a Total Placement Value.
  • brandContext.propertyId (optional) — must reference a Property owned by the authenticated organization. Narrows AIPVS category relevance and training influence uplift.

Response:

{
  "success": true,
  "pqs": {
    "pqsScore": 79,
    "rawWeightedScore": 79,
    "subScores": {
      "position": 0.85,
      "chunk": 0.7,
      "type": 0.75,
      "editorial": 0.85,
      "link": 1.0
    },
    "sentimentMultiplier": 1.0,
    "sentimentCategory": "neutral",
    "effectiveWeights": {
      "position": 0.3,
      "chunk": 0.2,
      "type": 0.2,
      "editorial": 0.15,
      "link": 0.15
    },
    "defaultWeights": {
      "position": 0.3,
      "chunk": 0.2,
      "type": 0.2,
      "editorial": 0.15,
      "link": 0.15
    },
    "inputs": {
      "placementType": "roundup_top_third",
      "editorialControl": "collaborative",
      "linkAttribute": "dofollow",
      "assumedPosition": 0.12
    }
  },
  "aipvs": {
    "publisher": {
      "id": "cm...",
      "domain": "wirecutter.com",
      "name": "Wirecutter",
      "logoUrl": "https://...",
      "aiPolicyStatus": "green",
      "organicMetricsFetchedAt": "2026-04-01T12:00:00.000Z"
    },
    "aipvs": {
      "domain": "wirecutter.com",
      "aipvs": 78,
      "tier": 1,
      "tierLabel": "Premium",
      "aiImpactMultiplier": 0.82,
      "confidence": "exact",
      "layers": { "oab": { "..." }, "aca": { "..." }, "ati": { "..." } }
    },
    "needsEnrichment": false
  },
  "totalPlacementValue": 62
}

Notes:

  • pqs.subScores.chunk is always 0.7 for prospective placements. Actual chunk containment can only be computed from real content, so the API treats prospective placements as "partial" containment by default. Retrospective scoring (via the web UI) computes the real value.
  • pqs.sentimentMultiplier is always 1.0 for prospective placements. Sentiment analysis requires real content around the mention.
  • aipvs and totalPlacementValue are null when publisherDomain is omitted or the AIPVS lookup fails.
  • pqs.effectiveWeights matches defaultWeights in prospective mode. Retrospective mode (web UI only) redistributes the editorial weight proportionally across the other four signals.

Example — run a lift preview: Fire several requests with the same publisher and editorial control but different placement positions / link attributes to quantify how much better terms would be worth. This is exactly what the "What if you negotiated better terms?" panel in the web UI does.


Score a Retrospective Placement (asynchronous)

Score an existing placement at a live URL. Spyglasses fetches the page, locates the brand mention (using the property's name and aliases), auto-detects the link attribute, classifies placement type and sentiment with an AI model, and computes the PQS. The pipeline typically takes 15–30 seconds and runs as a background job, so this endpoint returns a placement ID immediately — you poll for the result.

POST /api/v1/ai-impact/placement/retrospective

Headers:

  • x-api-key: Your organization's API key (required)
  • Content-Type: application/json

Body:

{
  "url": "https://www.prnewswire.com/news-releases/example-article",
  "propertyId": "YOUR_PROPERTY_ID"
}
  • url (required) — the live URL to score. Must be publicly accessible.
  • propertyId (required) — the Property whose brand name and aliases should be used to locate the mention in the content. Must belong to the authenticated organization.
  • title (optional) — a human-readable label for the placement. Auto-extracted from the page's <title> / og:title / h1 if omitted.
  • placementType / editorialControl / linkAttribute (optional) — all three are auto-detected from the page during scoring. Only supply them if you want to override the auto-detection.

Response (HTTP 202):

{
  "success": true,
  "id": "cm_placement_abc123",
  "status": "pending",
  "mode": "retrospective",
  "url": "https://www.prnewswire.com/news-releases/example-article",
  "createdAt": "2026-04-15T14:30:00.000Z",
  "pollUrl": "/api/v1/ai-impact/placement/cm_placement_abc123",
  "pollIntervalSeconds": 3,
  "estimatedCompletionSeconds": 30
}

The returned id is the placement's unique identifier. Use it with the polling endpoint below.


Poll a Placement's Status and Result

Fetch the current state of a placement created via the retrospective endpoint (or any placement owned by the authenticated organization). Poll at roughly 3-second intervals until status is completed or failed.

GET /api/v1/ai-impact/placement/:id

Headers:

  • x-api-key: Your organization's API key (required)

Response while scoring is still in progress (HTTP 200):

{
  "success": true,
  "id": "cm_placement_abc123",
  "status": "running",
  "mode": "retrospective",
  "url": "https://www.prnewswire.com/news-releases/example-article",
  "createdAt": "2026-04-15T14:30:00.000Z",
  "startedAt": "2026-04-15T14:30:01.000Z",
  "pollUrl": "/api/v1/ai-impact/placement/cm_placement_abc123",
  "pollIntervalSeconds": 3
}
  • status is either pending (queued, not yet running) or running (actively being scored).
  • Keep polling until status changes to completed or failed.

Response when scoring has completed (HTTP 200):

{
  "success": true,
  "id": "cm_placement_abc123",
  "status": "completed",
  "mode": "retrospective",
  "url": "https://www.prnewswire.com/news-releases/example-article",
  "title": "Example Company partners with Acme for industrial decarbonization",
  "createdAt": "2026-04-15T14:30:00.000Z",
  "completedAt": "2026-04-15T14:30:24.000Z",
  "pqs": {
    "pqsScore": 72,
    "rawWeightedScore": 72,
    "subScores": {
      "position": 0.85,
      "chunk": 1.0,
      "type": 0.75,
      "editorial": 0.5,
      "link": 0.6
    },
    "sentimentMultiplier": 1.0,
    "sentimentCategory": "neutral",
    "sentimentPolarity": 0.52,
    "effectiveWeights": {
      "position": 0.353,
      "chunk": 0.235,
      "type": 0.235,
      "editorial": 0,
      "link": 0.176
    },
    "defaultWeights": { "position": 0.3, "chunk": 0.2, "type": 0.2, "editorial": 0.15, "link": 0.15 },
    "inputs": {
      "placementType": "roundup_top_third",
      "editorialControl": "mentioned",
      "linkAttribute": "nofollow",
      "positionPercent": 0.18
    },
    "brandMention": {
      "matchedAlias": "Example Company",
      "tokenIndex": 287,
      "totalTokens": 1620
    },
    "keyMessagePullThrough": {
      "matches": [
        {
          "keyMessageId": "cm_km_001",
          "message": "Zero-carbon industrial heat",
          "sentenceText": "Example Company provides zero-carbon industrial heat and power...",
          "sentenceCharStart": 1240,
          "sentenceCharEnd": 1380,
          "score": 0.51,
          "matched": true
        }
      ],
      "threshold": 0.4
    }
  },
  "aipvs": {
    "publisher": { "id": "cm_pub_xyz", "domain": "prnewswire.com", "name": "PR Newswire", "..." },
    "aipvs": { "domain": "prnewswire.com", "aipvs": 68, "tier": 2, "tierLabel": "Strong", "..." },
    "needsEnrichment": false
  },
  "totalPlacementValue": 49
}

Notes:

  • In retrospective mode, the editorial weight is redistributed proportionally across the other four signals (effectiveWeights.editorial = 0) because the editorial decision has already been made. The defaultWeights field shows the original distribution for reference.
  • pqs.inputs reflects the auto-detected values after scoring, not necessarily what you passed in the create request. If the scorer detected a different link attribute or placement type, those take precedence.
  • pqs.brandMention shows which alias matched and where it was located in the tokenized content.
  • keyMessagePullThrough.matches is always one entry per configured key message (even below-threshold "closest match" entries); the matched flag indicates whether each crossed the threshold.
  • totalPlacementValue is null when the publisher couldn't be looked up or hasn't been enriched yet.

Response when scoring has failed (HTTP 200):

{
  "success": false,
  "id": "cm_placement_abc123",
  "status": "failed",
  "errorMessage": "No brand mention found in content. Check the URL or add brand aliases to the Property.",
  "mode": "retrospective",
  "url": "https://www.prnewswire.com/news-releases/example-article",
  "createdAt": "2026-04-15T14:30:00.000Z",
  "completedAt": "2026-04-15T14:30:12.000Z"
}

Common failure modes:

  • "No brand mention found in content" — the scorer couldn't locate any occurrence of the Property's name or aliases in the page. Check the URL is correct and add the exact form of the brand name used in the article to the Property's aliases.
  • "The URL returned 404 — check that it's correct and publicly accessible." — the page no longer exists or isn't publicly available.
  • "The URL returned 403 — the site is blocking our crawler." — the site is blocking the Spyglasses fetcher via WAF or bot protection. Try a different page on the same site.
  • "Could not extract readable content from the page." — the page is JavaScript-rendered, paywalled, or has an unusual layout that prevents content extraction.

Polling example (JavaScript):

async function scoreRetrospectivePlacement(url, propertyId, apiKey) {
  // 1. Create the placement and get back an ID
  const createRes = await fetch("/api/v1/ai-impact/placement/retrospective", {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": apiKey },
    body: JSON.stringify({ url, propertyId }),
  });
  const { id, pollUrl, pollIntervalSeconds } = await createRes.json();
 
  // 2. Poll until the placement is completed or failed
  while (true) {
    await new Promise((r) => setTimeout(r, pollIntervalSeconds * 1000));
    const poll = await fetch(pollUrl, { headers: { "x-api-key": apiKey } });
    const result = await poll.json();
    if (result.status === "completed") return result;
    if (result.status === "failed") throw new Error(result.errorMessage);
    // Otherwise keep polling (status === "pending" or "running")
  }
}

AI Visibility Reports

Generate an AI Visibility Report — how a brand appears across ChatGPT, Gemini, Claude, Perplexity, and Google AI Overviews — for one property, or for a list of prospects at once. Use this to run a report from a CRM trigger, a pitch workflow, or a nightly job, instead of clicking through the app.

Reports are generated in the background and take a few minutes. Every endpoint here returns immediately and hands you an id to poll.

These endpoints use an organization-level API key, not your property API key. Generate one under Organization Settings → API Key. One organization key works across every property in the organization.

Credits, and why the API is stricter than the app

Every report costs credits. Before anything starts, the full cost must already be sitting in your organization's credit balance:

  • The API never bills overage and never triggers an automatic top-up. An integration that runs unattended must not be able to run up a bill.
  • If the balance is short, the call returns 402 and nothing is created or charged.
  • In the app, a subscriber can acknowledge overage and continue. There is no equivalent on the API — buy credits in the app and retry.
  • Organizations on contractual (enterprise) billing are not balance-checked at all. Their usage is recorded and invoiced under their agreement rather than drawn from a credit balance, so these endpoints do not return 402 for them.

A 402 looks like this:

{
  "error": "INSUFFICIENT_CREDITS",
  "requiredCredits": 10,
  "availableBalance": 4,
  "message": "This report costs 10 credits and 4 are available. The API does not bill overage — add credits and retry."
}

Generate a Report

Start a report for one property you already track.

POST /api/v1/ai-reports

Headers:

  • x-api-key: Your organization's API key (required)
  • Content-Type: application/json
curl -X POST "https://www.spyglasses.io/api/v1/ai-reports" \
  -H "x-api-key: sg_YOUR_ORGANIZATION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "propertyId": "cm9ovvnnc0001wjubh0en0cpe" }'

Body:

{
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "notifyEmail": "pr@example.com"
}
  • propertyId (required) — the property to report on. Must belong to the authenticated organization.
  • locationCity / locationRegion / locationCountry (optional) — location context for this run. Each falls back to the property's own default when omitted.
  • notifyEmail (optional) — who to email when the report finishes.

Response (HTTP 202):

{
  "id": "V1StGXR8Z5jdHi6BmyT",
  "status": "pending",
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "publicToken": "example-com-3f9a",
  "publicUrl": "https://www.spyglasses.io/reports/example-com-3f9a",
  "createdAt": "2026-09-09T14:30:00.000Z",
  "completedAt": null
}

publicUrl is the shareable report link. It works as soon as generation finishes — no login required — so you can put it straight into a CRM field or a pitch email.

If a report is already in flight (HTTP 200): when the property already has a report that is running, or finished but not yet purchased, that report is returned instead and nothing new is started or billed. The body is the same shape; the status code is 200 rather than 202. This is the same rule the app applies, and it means a retrying integration cannot double-bill.

Errors:

StatuserrorMeaning
400validation detailMissing or malformed propertyId
401API key is required / Invalid API keyNo x-api-key header, or it matches no organization
402INSUFFICIENT_CREDITSThe full cost is not available. No overage on the API
404Property not foundNo such property in your organization. A property owned by someone else is answered 404, not 403, so the endpoint cannot be used to probe which ids exist
500Failed to create AI reportServer error; safe to retry

Poll a Report

Fetch a report's current status. Generation takes a few minutes — poll every 30 seconds or so rather than continuously.

GET /api/v1/ai-reports/:id

Headers:

  • x-api-key: Your organization's API key (required)
curl "https://www.spyglasses.io/api/v1/ai-reports/V1StGXR8Z5jdHi6BmyT" \
  -H "x-api-key: sg_YOUR_ORGANIZATION_API_KEY"

Response (HTTP 200):

{
  "id": "V1StGXR8Z5jdHi6BmyT",
  "status": "complete",
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "publicToken": "example-com-3f9a",
  "publicUrl": "https://www.spyglasses.io/reports/example-com-3f9a",
  "createdAt": "2026-09-09T14:30:00.000Z",
  "completedAt": "2026-09-09T14:36:12.000Z"
}
  • pending / running — still generating; keep polling.
  • complete / partial — readable at publicUrl.
  • failed — it will not finish.

Errors:

StatuserrorMeaning
401API key is required / Invalid API keyNo x-api-key header, or it matches no organization
404AI report not foundNo such report in your organization. A report owned by someone else is answered 404, the same as one that does not exist

Generate Reports for a List of Prospects

Start one report per row. Each row creates a prospect property in your organization and runs its report. This is the API form of the app's bulk import, and carries the same limits: a single batch accepts at most 20 rows, and rows whose domain is unparseable or already tracked in the organization are dropped.

Bulk generation is available on active Agency plans, the same gate as the in-app bulk import. Other plans get 403.

POST /api/v1/ai-report-batches

Headers:

  • x-api-key: Your organization's API key (required)
  • Content-Type: application/json
curl -X POST "https://www.spyglasses.io/api/v1/ai-report-batches" \
  -H "x-api-key: sg_YOUR_ORGANIZATION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      { "value": "acme.com" },
      { "value": "initech.com", "locationCity": "Austin", "locationRegion": "Texas", "locationCountry": "US" }
    ]
  }'

Body:

{
  "rows": [
    { "value": "acme.com" },
    { "value": "https://initech.com/products/widget" }
  ],
  "notifyEmail": "newbiz@example.com"
}
  • rows (required, at least 1) — one entry per prospect. value is a bare domain (tracked as a brand) or a product URL with a path (tracked as that product).
  • rows[].locationCity / locationRegion / locationCountry (optional) — location context for that row's report.
  • notifyEmail (optional) — who to email when the batch finishes.

The projected cost is rows × the report credit cost, and the whole of it must be available before anything starts. A short balance returns 402 with a projectedCredits field alongside the usual requiredCredits and availableBalance, and no property or report is created.

Response (HTTP 202):

{
  "batchId": "cm_batch_9f2",
  "total": 2,
  "rejected": [
    {
      "index": 2,
      "value": "not a domain",
      "subjectType": "BRAND",
      "domain": "not a domain",
      "location": {},
      "status": "bad_domain",
      "message": "Could not read a valid domain from this value."
    }
  ],
  "projectedCredits": 20,
  "status": "running"
}

total is the number of rows accepted. rejected lists every row that was dropped, with its position in the array you sent, why it was dropped (bad_domain, or duplicate when the organization already tracks that domain), and a message you can show a user. rejected is [] when everything was accepted.

Errors:

StatuserrorMeaning
400NO_VALID_ROWS / IMPORT_LIMIT_EXCEEDEDNothing survived validation, or more rows than a single import allows
401API key is required / Invalid API keyNo x-api-key header, or it matches no organization
402INSUFFICIENT_CREDITSThe full projected cost is not available. No overage on the API
403plan messageBulk generation is not available on this organization's plan
500Failed to start the batchServer error; safe to retry

Poll a Batch

Track a batch's progress and collect each row's report link as it becomes available.

GET /api/v1/ai-report-batches/:batchId

Headers:

  • x-api-key: Your organization's API key (required)
curl "https://www.spyglasses.io/api/v1/ai-report-batches/cm_batch_9f2" \
  -H "x-api-key: sg_YOUR_ORGANIZATION_API_KEY"

Response (HTTP 200):

{
  "batchId": "cm_batch_9f2",
  "status": "running",
  "total": 2,
  "completed": 1,
  "failed": 0,
  "createdAt": "2026-09-09T14:30:00.000Z",
  "updatedAt": "2026-09-09T14:36:20.000Z",
  "reports": [
    {
      "propertyId": "cm_prop_a1",
      "domain": "acme.com",
      "reportId": "V1StGXR8Z5jdHi6BmyT",
      "status": "complete",
      "publicToken": "acme-com-7c2",
      "publicUrl": "https://www.spyglasses.io/reports/acme-com-7c2",
      "createdAt": "2026-09-09T14:30:05.000Z",
      "completedAt": "2026-09-09T14:36:18.000Z"
    },
    {
      "propertyId": "cm_prop_a2",
      "domain": "initech.com",
      "reportId": null,
      "status": null,
      "publicToken": null,
      "publicUrl": null,
      "createdAt": null,
      "completedAt": null
    }
  ]
}

Entries appear in reports[] as the background job works through the rows, so a row with null fields early on will fill in on a later poll. status on the batch stays running until every row has finished or failed.

Errors:

StatuserrorMeaning
401API key is required / Invalid API keyNo x-api-key header, or it matches no organization
404Batch not foundNo such batch in your organization. A batch owned by someone else is answered 404, the same as one that does not exist

Coverage Groups

A coverage group is a named list of earned-media placement URLs for one property — a PR team's coverage for a client. These endpoints let an automation feed placements into a group as they are won, instead of pasting them into the app in batches.

For a step-by-step walkthrough of wiring this to Zapier, n8n, or a social-listening tool, see Automating Coverage Tracking.

These endpoints use an organization-level API key, not your property API key. Generate one under Organization Settings → API Key. One organization key works across every client property in the organization.

List Coverage Groups

Return a property's coverage groups with their item counts. Call this once when setting up an automation to find the groupId you'll feed.

GET /api/v1/coverage/groups?propertyId=YOUR_PROPERTY_ID

Headers:

  • x-api-key: Your organization's API key (required)

Response:

{
  "success": true,
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "groups": [
    {
      "id": "cm_grp_q3",
      "name": "Q3 Coverage",
      "description": null,
      "itemCount": 142,
      "trackedByProjectGoals": 1,
      "createdAt": "2026-07-01T00:00:00.000Z",
      "updatedAt": "2026-07-28T16:04:00.000Z"
    }
  ]
}

The property must belong to the authenticated organization, or the call returns 403.


Add Placement URLs to a Coverage Group

The automation entry point. Append one URL or a batch of up to 1000.

POST /api/v1/coverage/groups/:groupId/items

Headers:

  • x-api-key: Your organization's API key (required)
  • Content-Type: application/json

Body (single URL — the usual shape for a real-time trigger):

{
  "url": "https://techcrunch.com/2026/07/28/example-article"
}

Body (batch — for a nightly export or a backfill):

{
  "urls": [
    "https://techcrunch.com/2026/07/28/example-article",
    "https://www.reddit.com/r/example/comments/abc123/thread-title",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "score": true
}
  • url (string) or urls (array, 1–1000) — supply exactly one.
  • score (boolean, default true) — whether to create and score a Placement for each new URL. See "Controlling scoring cost" below.

Response:

{
  "success": true,
  "groupId": "cm_grp_q3",
  "added": 2,
  "duplicates": 1,
  "invalid": [],
  "placementsLinked": 1,
  "scoringQueued": true
}
  • added — item rows created.
  • duplicates — submitted URLs the group already had. Safe and free: re-posting the same URL costs nothing.
  • invalid — rejected lines, each with a value and a reason.
  • placementsLinked — URLs that already had a scored placement for this property and were linked immediately, at no cost.
  • scoringQueued — whether placement scoring was dispatched for the new items.

Notes:

  • URLs are matched on a normalized form, so www, trailing slashes, tracking parameters, and fragments all collapse to the same item. This is what makes the endpoint safe to call from a trigger that may fire more than once for the same story.
  • YouTube links are expanded to their equivalent watch, youtu.be, and shorts forms automatically, because AI platforms cite the same video under any of them.
  • A URL that already has a placement for this property is linked rather than rescored.

Controlling scoring cost

By default each newly added URL gets a Placement created and scored — the page is fetched, the brand mention located, the placement type and sentiment classified, and the PQS computed. That is the same work the app does when you paste URLs in, and it is the expensive step.

Pass score: false to skip it. Classification and project-goal backfill still run, so citations still attribute correctly; you just don't get a PQS until you run Score placements on the group in the app, which is idempotent and only scores what's missing. Use this when bulk-loading a large historical archive where per-placement quality scores aren't the point.


Read a Coverage Group

Return a group with a page of its items, each carrying its publisher, classification, and linked placement. Use this to read scores back into your workflow after ingestion.

GET /api/v1/coverage/groups/:groupId?limit=100&offset=0

Headers:

  • x-api-key: Your organization's API key (required)

Query Parameters:

  • limit (optional, default 100, max 500) — items per page
  • offset (optional, default 0)

Response:

{
  "success": true,
  "id": "cm_grp_q3",
  "name": "Q3 Coverage",
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "totalItems": 142,
  "limit": 100,
  "offset": 0,
  "items": [
    {
      "id": "cm_item_001",
      "url": "https://techcrunch.com/2026/07/28/example-article",
      "urlNormalized": "techcrunch.com/2026/07/28/example-article",
      "domain": "techcrunch.com",
      "title": "Example Company raises Series B",
      "pageType": "blog_article",
      "contentFormat": "news",
      "isListicle": false,
      "classifiedAt": "2026-07-28T16:05:12.000Z",
      "createdAt": "2026-07-28T16:04:00.000Z",
      "publisher": {
        "domain": "techcrunch.com",
        "mediaType": "earned_press",
        "authorityType": "trade_media"
      },
      "placement": {
        "id": "cm_placement_abc123",
        "status": "completed",
        "failureCode": null,
        "pqsScore": 71,
        "placementType": "dedicated_article",
        "title": "Example Company raises Series B",
        "authorName": "Jane Reporter",
        "completedAt": "2026-07-28T16:05:38.000Z"
      }
    }
  ]
}

Items are newest-first. placement is null until the scoring job creates one, then carries status pendingrunningcompleted, with pqsScore populated on completion. Poll this endpoint if your workflow needs the score.


Get Citations Earned by a Coverage Group

The read side of the loop: which of the group's URLs are actually being cited by AI assistants, in which answers, on which platforms. Scoped to the property's own tracked prompts and reports.

GET /api/v1/coverage/groups/:groupId/citations

Headers:

  • x-api-key: Your organization's API key (required)

Query Parameters:

  • platform (optional) — filter to a single AI platform
  • citationType (optional) — one of cited_inline, evaluated_source, search_result, unclassified, all
  • url (optional) — filter to a single placement URL
  • startDate / endDate (optional) — YYYY-MM-DD, inclusive
  • limit (optional, max 1000) / offset (optional)

The response contains a funnel, a cited-vs-uncited comparison across the group's URLs, and per-citation hits with the prompt, platform, and the surrounding answer text.

This is the endpoint to poll for a "notify me when a placement gets cited" automation — filter by startDate to the period since your last run.


Property Information

Get Property Details

Retrieve information about your property using your API key.

GET /api/me

Headers:

  • x-api-key: Your API key (required)

Response:

{
  "propertyId": "cm9ovvnnc0001wjubh0en0cpe",
  "domain": "example.com",
  "companyName": "Example Company",
  "apiKey": "sg_XXX...",
  "verified": true,
  "verifiedAt": "2025-01-01T00:00:00.000Z",
  "blockAiModelTrainers": false,
  "customBlocks": [],
  "customAllows": []
}

AI Visibility Rankings

Get AI Visibility Rankings

Retrieve prioritized grounding search opportunities from your most recent AI report.

GET /api/ai-rankings/:propertyId

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • propertyId: Your property ID (from /api/me)

Historical Metrics

Get Brand Consistency History

Retrieve brand consistency score trends over time.

GET /api/properties/:id/historical-metrics/brand-consistency

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • id: Your property ID

Get Discovery Metrics History

Retrieve discovery query performance trends over time.

GET /api/properties/:id/historical-metrics/discovery

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • id: Your property ID

Get Grounding Search Metrics History

Retrieve grounding search ranking trends over time.

GET /api/properties/:id/historical-metrics/grounding-searches

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • id: Your property ID

Query Parameters:

  • onlyGaps: Set to "true" to filter to gap opportunities only

Get Competitor Share of Voice

Retrieve share of voice data for competitors from your most recent AI report.

GET /api/properties/:id/competitor-sov

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • id: Your property ID

Get AI Visibility Rankings (Alternative)

Alternative endpoint for AI visibility rankings data.

GET /api/properties/:id/ai-visibility-rankings

Headers:

  • x-api-key: Your API key (required)

Parameters:

  • id: Your property ID

AI Traffic Analytics

Retrieve AI traffic timeline data with comparison to previous period.

POST /api/analytics/ai/timeline-with-trends

Headers:

  • x-api-key: Your API key (required)
  • Content-Type: application/json

Body:

{
  "propertyApiKey": "sg_YOUR_API_KEY",
  "timeRange": {
    "start": "2025-01-01T00:00:00Z",
    "end": "2025-01-31T23:59:59Z"
  },
  "granularity": "day"
}

Get AI Conversion Metrics

Retrieve AI conversion metrics showing bot visits vs human visits.

POST /api/analytics/ai/conversion-metrics

Headers:

  • x-api-key: Your API key (required)
  • Content-Type: application/json

Body:

{
  "propertyApiKey": "sg_YOUR_API_KEY",
  "timeRange": {
    "start": "2025-01-01T00:00:00Z",
    "end": "2025-01-31T23:59:59Z"
  },
  "sessionWindowMinutes": 5
}

Get AI Traffic Summary

Retrieve AI traffic summary with breakdowns by category and referrer.

POST /api/analytics/ai/traffic-summary

Get AI Traffic Timeline

Retrieve AI traffic timeline data for charts.

POST /api/analytics/ai/timeline

Get AI Traffic Timeline by Intent

Retrieve AI traffic timeline segmented by page intent.

POST /api/analytics/ai/timeline-by-intent

Get AI Overview Visits

Retrieve visits from Google AI Overviews, Featured Snippets, and People Also Ask.

POST /api/analytics/ai/ai-overview-visits

Get Bot Types

Retrieve list of AI visitor bot types for filtering.

GET /api/analytics/ai/bot-types

Agent Analytics

Get Agent Data by Category

Retrieve analytics data for agents grouped by category.

POST /api/analytics/agent/category

Headers:

  • x-api-key: Your API key (required)
  • Content-Type: application/json

Body:

{
  "propertyId": "YOUR_PROPERTY_ID",
  "timeRange": {
    "start": "2025-01-01T00:00:00Z",
    "end": "2025-01-31T23:59:59Z"
  }
}

Get Agent Data by Subcategory

Retrieve analytics data for agents grouped by subcategory.

POST /api/analytics/agent/subcategory

Get Agent Data by Bot Type

Retrieve analytics data for agents grouped by bot type.

POST /api/analytics/agent/bot-type

Get Agent Data by Location

Retrieve analytics data for agents grouped by location.

POST /api/analytics/agent/location

Get Agent Data by Pages

Retrieve analytics data for agents grouped by page path.

POST /api/analytics/agent/pages

Get Live Agent Data

Retrieve real-time agent data for a property.

GET /api/analytics/agent/live/:propertyId

Get Blocked Requests Statistics

Retrieve statistics about blocked vs allowed requests.

POST /api/analytics/agent/blocked-stats

Recrawl Analytics

Get Site Recrawl Summary

Retrieve site-wide recrawl frequency statistics.

POST /api/analytics/recrawl/summary

Headers:

  • x-api-key: Your API key (required)
  • Content-Type: application/json

Body:

{
  "propertyId": "YOUR_PROPERTY_ID",
  "timeRange": {
    "start": "2025-01-01T00:00:00Z",
    "end": "2025-01-31T23:59:59Z"
  }
}

Get Page Recrawl Frequency

Retrieve recrawl frequency statistics for each page.

POST /api/analytics/recrawl/pages

Get Bot Recrawl Frequency

Retrieve recrawl frequency statistics for each crawler bot.

POST /api/analytics/recrawl/bots

Get Bot Page Recrawl Frequency

Retrieve recrawl frequency for pages crawled by a specific bot.

POST /api/analytics/recrawl/bots/:botTypeId/pages

Internal-Only Endpoints

The API Reference shows additional endpoints that are for internal application use only. These endpoints use session-based authentication and are not supported for external API access.

If you need access to data not available through the endpoints listed above, please contact us to discuss your requirements.

On this page

How-to guides for setting up Spyglasses to track your AI traffic