Automating Coverage Tracking
If your team already tracks placements somewhere else — a dedicated PR platform, a social-listening tool, a shared sheet, a Slack channel — you shouldn't have to paste them into Spyglasses a second time. The coverage API lets an automation append placements to a coverage group the moment they're won, so AI citation tracking starts on day one rather than at the end of the quarter.
Two shapes of workflow are common:
- Real-time. A trigger fires per placement — a new Reddit thread or comment surfaced by social listening, a new row in your PR tracker, a Slack message in
#coverage-wins. Each fires onePOSTwith a single URL. - Batch. A nightly or weekly export from your PR platform posts a batch of URLs in one call.
Both use the same endpoint. Both are safe to run repeatedly.
Before You Start
You need three things:
- An organization API key. Open Organization Settings → API Key and generate one. This is not your property API key — see Authentication for the difference.
- A property ID. The client whose coverage you're tracking.
- A coverage group ID. Create the group in the app first (Coverage Groups on the property), then look up its ID with the API:
Copy the id. That's the only value your automation needs to hold besides the key.
Property groups are created in the app, not through the API. That's deliberate: a property group is a reporting unit your team names and attaches to project goals, and an automation that could invent them tends to produce a long tail of near-duplicate groups nobody meant to create.
Organization-level scoring groups — a flat list of placements where each row names its own brand — can be created through the API. See Scoring Placements for Any Brand below.
Sending a Placement
One placement, one call:
Or a batch, up to 1000 URLs per call:
Duplicates Are Free
You do not need to deduplicate before calling. URLs are matched on a normalized form, so www, trailing slashes, UTM parameters, and #fragments all collapse to the same item. A URL the group already has comes back in duplicates and costs nothing.
This matters more than it sounds. Social-listening triggers fire more than once for the same story; PR platform exports overlap at the boundaries; a retried webhook resends the same payload. All of it is safe.
YouTube links get the same treatment one level deeper: a youtu.be short link, a /watch?v= link, and a /shorts/ link for the same video are recognized as one placement, because AI assistants cite the same video under any of those forms.
Handling the Response
added > 0— new placements are being classified and scored in the background.duplicates > 0— already tracked. Not an error.invalid— lines that couldn't be parsed as anhttp(s)URL, each with areason. Worth logging; a run where everything lands ininvalidusually means the field mapping in your automation is pointing at the wrong column.
What Happens Next
Adding a URL kicks off three things in the background:
- Classification — the publisher is resolved and the page is classified (page type, content format, listicle detection).
- Placement scoring — a Placement is created and scored, producing a PQS. A URL that already has a placement for this property is linked instead, at no cost.
- Goal backfill — every active project tracking this group re-checks its goal hits, so a placement that was already being cited before you added it is credited retroactively.
Scoring typically completes within a minute of ingestion. Read it back:
Each item carries a placement block whose status moves pending → running → completed, with pqsScore populated on completion.
Controlling Cost
Scoring a placement fetches the page and runs classifiers over it — the expensive part of ingestion. If you're bulk-loading a large historical archive and per-placement quality scores aren't the point, pass score: false:
Classification and goal backfill still run, so citation attribution is unaffected. You can score later with Score placements on the group in the app, which is idempotent and only scores what's missing.
For steady-state real-time ingestion, leave scoring on. The volume is one placement at a time, and the PQS is most of why the placement is worth tracking.
Scoring Placements for Any Brand
Everything above assumes the placements belong to a property you track. If you measure coverage for brands that aren't your clients — a benchmark set, a new business pitch, a media-value model that needs a score per placement — you don't need a property for each one. Post rows of url and brand and a cross-brand group is created for you:
How Brands Resolve
Each distinct brand value is resolved once, however many rows name it:
- A bare domain (
acme.com) is read as a BRAND. - A URL with a path (
https://acme.com/products/widget) is read as a PRODUCT, anchored on that page. Product scoring judges the placement against the product, not the whole company. - A brand you already track — a client, or a prospect you generated a report for — is reused, so its curated company name, aliases and categories are the ones the scorer uses. Matching is on the domain.
- Anything else becomes a hidden scoring-only brand record. It carries the company name and aliases the scorer needs and nothing else: it doesn't appear in your client list, it runs no daily prompts, and it costs no subscription seat. Adding the same domain later as a real property promotes the record in place, so the scores you already collected stay attached.
The company name matters more than it looks. The scorer locates the brand mention on the page before it scores anything, so a wrong name reports the placement as no_brand_mention. Names are looked up automatically from the domain; the in-app import wizard lets you correct one before the import runs.
Rows whose brand can't be parsed at all come back in invalidRows with a reason, and rows whose URL can't be parsed come back in invalid. Neither fails the batch.
Poll for the Scores
The create call answers 202 as soon as the rows are stored — scoring runs in the background, a page fetch and several classifiers per placement. Poll the group at pollIntervalSeconds until every item's placement.status is completed or failed:
Each item carries the brand it was scored against, and — with includeAipvs=true — the publisher's AI Placement Value Score computed in that brand's own context:
includeAipvs is a network-bound score, capped at 100 distinct brand-and-publisher pairs per page, so leave it off on a fast polling loop and ask for it once the placements have finished.
Adding to an existing cross-brand group uses the same rows shape:
The body shape has to match the group. rows on a property group, or url/urls on a cross-brand group, is refused with 400 — a placement can't be scored without knowing which brand it is for, and guessing would quietly produce a group of no_brand_mention rows.
List your cross-brand groups by omitting propertyId:
What It Costs
Placement scoring is currently free, and a monthly cap can be configured per account. A request that would exceed the cap is refused with 429 and a PLACEMENT_SCORING_CAP_REACHED code, and nothing is created — no group, no brand records, no partial import. With no cap configured, the check never refuses.
Citations are the one thing a cross-brand group doesn't have. They come from a property's own tracked prompts and reports, so the citations endpoint answers 400 for a cross-brand group rather than returning an empty set. Track the brand as a property when you want to know which of its placements AI assistants actually cite.
Closing the Loop
The point of getting placements into Spyglasses is finding out which ones AI assistants actually cite. Poll the citations endpoint on a schedule — daily is plenty — and filter to the window since your last run:
The response includes per-citation hits with the prompt that triggered them, the platform that answered, and the surrounding answer text — enough to post a message like "Your TechCrunch piece was cited by Perplexity answering 'best industrial heat providers'" straight into Slack.
Platform Notes
Zapier
Use the Webhooks by Zapier → POST action.
- URL —
https://www.spyglasses.io/api/v1/coverage/groups/YOUR_GROUP_ID/items - Payload Type —
json - Data — key
url, value mapped from your trigger's link field - Headers —
x-api-keyset to your organization API key,Content-Typeset toapplication/json
Store the key in the Zap's header field rather than in the URL. Zapier logs request URLs in the task history.
n8n
Use an HTTP Request node.
- Method —
POST - URL —
https://www.spyglasses.io/api/v1/coverage/groups/YOUR_GROUP_ID/items - Authentication — Generic Credential Type → Header Auth, name
x-api-key, value your organization API key - Body Content Type — JSON
- Body —
{ "url": "{{ $json.link }}" }
For batch flows, collect items with an Aggregate node first and send a single call with urls — one request of 200 URLs is far better than 200 requests.
Make
Use the HTTP → Make a request module.
- URL:
https://www.spyglasses.io/api/v1/coverage/groups/YOUR_GROUP_ID/items - Method:
POST - Headers:
x-api-keyset to your organization API key - Body type: Raw
- Content type: JSON (application/json)
- Request content:
{ "url": "{{1.link}}" }, with the link mapped from your trigger module - Parse response: Yes, so later modules can read
added,duplicatesandinvalid
For batch flows, collect the links with an Array aggregator first and send a single call with urls. One request of 200 URLs is far better than 200 requests.
To pull metrics or citations on a schedule, set the scenario's schedule (daily is plenty) and use the same module with GET.
Anything Else
Any tool that can send an authenticated HTTP POST works. There is nothing Zapier-, n8n- or Make-specific about the endpoint.
Errors
| Status | Meaning |
|---|---|
400 | Malformed body — most often both url and urls, or neither |
401 | Missing or unrecognized x-api-key. Check you used the organization key, not a property key |
404 | The group doesn't exist, or it belongs to a different organization |
429 | The monthly placement-scoring cap for this account has been reached. Nothing was created |
500 | Something failed on our side — safe to retry, since duplicates are free |
Limits
- 1000 URLs per
urlsbatch, and 1000 rows perrowsbatch. - 100 new brands created by a single cross-brand request. Brands you already track don't count.
- 500 items per page when reading a group back (
limit). - 100 brand-and-publisher pairs scored per page with
includeAipvs=true. - 1000 citations per page on the citations endpoint (
limit).
If you're planning ingestion at a volume that makes these awkward, get in touch — we'd rather size it with you than have you discover a ceiling in production.