Competitors
Manage the competitors Competitor Tracker & Co. tails: subscribe by URL, list and fetch subjects, update tracked categories or unsubscribe, via API or MCP.
Competitors are the central resource. Each one is a third-party site we tail on your behalf. This page is the human-friendly tour from our lead detective C. T. Lucky. For parameter-level detail, see the API reference in the sidebar.
Open a case
POST /v1/competitors
Ties your organization to a competitor URL. The URL is normalized (trailing slashes, scheme, casing) before storage. A second subscribe call against an equivalent URL returns 409 Conflict.
Before we open the case we check that the domain resolves to a real server. A typo or a dead domain gets turned away with 422 CompetitorDomainUnreachable. No sense tailing an address that answers to no one. A domain we can't reach for a moment still goes through — we'd rather let a real site in and catch a dead one on the first read.
Required:
url— a reachable URL. Normalization happens server-side.
Optional:
displayName— overrides the auto-detected competitor name in your account view.trackedCategories— non-empty array of category ids. Omit to default to every active category in the catalog. An unknown id returns422 InvalidCategoryId.
The name and description fields on the response are auto-filled from the competitor's HTML <title> and <meta name="description">, and refresh on every read so a rebrand is picked up over time. Both arrive as null on a fresh subscription and stay that way until the first read lands; displayName is yours to set immediately and is the field that wins when both are present in the account view. The read also captures the competitor's favicon and hands it back as faviconUrl.
# One case per subject. The pack does the rest.
curl -X POST "$CT_API/competitors" \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"displayName": "Example Inc",
"trackedCategories": ["pricing_changes"]
}'Returns 201 Created with the new subscription.
Companies and sub-products
Some subjects are a single product. Others are one company running several products under a single roof. We let you tail the one you care about without losing the bigger picture.
It comes down to the URL you open the case with:
- A root URL like
https://example.comtracks the whole company. - A deep link like
https://example.com/marketingtracks that one sub-product. Behind it we quietly stand up the company it belongs to, so company-wide moves still reach you. A storefront has a head office. - An ordinary single-product site stays on its own, parent to no one.
Two fields on every competitor tell you where it sits:
competitorKind—companyorsub_product.parentCompetitorId— the company a sub-product belongs to, ornullwhen it isn't one.
Fetch a company with Get one and it carries a children array: its sub-products, each with an id, url, normalizedUrl, name and competitorKind, ordered by their URL. The list endpoint leaves children out to keep the page light, so fetch the company when you want them.
A sub-product's changes include its own page diffs plus the company-wide moves we judge relevant to it. Each routed change carries a relevance note, so you can tell a company-wide shift from one that started on the sub-product's own page.
List your subjects
GET /v1/competitors
A page of subscriptions for the current organization, with cursor pagination.
Query parameters:
limit— 1–200, default 50.cursor— opaque cursor returned innextCursorfrom a prior call.labelId— narrow the page to subjects carrying any of these labels. Repeatable (labelId=…&labelId=…) or comma-separated; a subject matches if it carries any one of them.search— free-text match against a subject's name, display name and URL. Case-insensitive; matches anywhere in the text.
curl "$CT_API/competitors?limit=20" \
-H "Authorization: Bearer $CT_TOKEN"Run a name past us before you open a file:
curl "$CT_API/competitors?search=stripe" \
-H "Authorization: Bearer $CT_TOKEN"search and labelId stack — pass both to narrow to labelled subjects whose name or URL also matches.
Response:
{
"items": [
{
"id": "01HF...",
"url": "https://example.com/",
"normalizedUrl": "https://example.com",
"name": "Example Inc — Modern accounting",
"description": "Accounting software for growing teams.",
"displayName": "Example Inc",
"faviconUrl": "https://assets.example-cdn.com/...",
"competitorKind": "company",
"parentCompetitorId": null,
"trackedCategories": ["pricing_changes"],
"labels": [{ "id": "01HG...", "name": "Watchlist", "color": "yellow" }],
"addedAt": "2026-05-04T10:23:00.000Z",
"paused": false,
"pauseReason": null,
"nextChargeAt": "2026-06-03T10:23:00.000Z",
"crawlHealth": null,
"healthDetail": null,
"updatedAt": "2026-05-04T10:23:00.000Z"
}
],
"nextCursor": null
}Field notes:
nameanddescriptionare extracted from the competitor's own metadata and refreshed on each read —nulluntil the first read lands.faviconUrlis a stable link to the competitor's favicon that doesn't expire, so it's safe to cache and reuse. It'snulluntil the first read captures one, and the image updates in place when a later read picks up a new icon.competitorKindandparentCompetitorIdplace the subject in its company. See Companies and sub-products. The list omits a company'schildren; fetch the company to read them.labelsare the per-organization tags you've assigned to this subscription; see Labels.pausedistruewhen the pack stops reading the page because the organization's coin balance can't cover the next cycle.pauseReasoncarries the slug.nextChargeAtis when the next coin-deduction lands for this subscription.nullfor own-app subscriptions, paused rows or rows that haven't been read once yet.crawlHealthflags a subject we can no longer read —permanent_dead,permanent_blocked,uncrawlableornullwhen healthy. See When a tail goes cold.healthDetailis an internal note for support and is oftennull; key your logic oncrawlHealth.
When a tail goes cold
Not every subject stays reachable. A domain lapses, or a site starts refusing automated reads. We sort those into crawlHealth so you always know why a subscription went quiet:
permanent_dead— the address no longer resolves to any server. We stop tailing it. It won't come back on its own.permanent_blocked— the site refuses automated reads. Same outcome: we stop tailing.uncrawlable— the page is unreadable for now. We keep knocking on the weekly cadence and clear the flag the moment a read lands.null— healthy, the usual state.
A permanent_dead or permanent_blocked subject recovers only when you act: drop the case and open a fresh one against the corrected URL.
If a subject turns out dead on its first read and never filed a thing, we refund the coin for that window. The refund lands once, on the same ledger as every other credit. A subject that filed at least one report before going dark keeps its charge.
Get one
GET /v1/competitors/{id}
Returns a single subscription. 404 Not Found if it doesn't belong to the current organization or if you've already dropped it.
curl "$CT_API/competitors/01HF..." \
-H "Authorization: Bearer $CT_TOKEN"The single read carries a billing block the list doesn't, so you can read the meter on one subject without doing the arithmetic yourself:
costPerCycle— coins this subject takes each cycle.0for your own app, which we tail free.isOwnApp—truewhen the subject is your own application. We don't bill you to watch yourself.coinsPaidUntil— when the paid cycle runs out, ornullwhen nothing is on the clock: own-app, never charged or the first read still pending.
Read the case file
GET /v1/competitors/{id}/timeline
A subject has a history, and we keep it. This endpoint hands back that history as an ordered list of events, newest first, under an events array. Each event is a plain record: a kind, the occurredAt time it happened, and a small params bag with the detail for that kind.
It reads like a case file, every beat in the order it happened. The kinds you'll see:
competitor_added— you opened the case.archive_baseline— we recovered an older baseline from the public web archive, so the story doesn't start the day you signed up.tracking_started— the tail began.change_detected— we caught a change.params.changeCountsays how many.tracking_error— we lost the scent for a moment. We pick it back up on the next pass.pages_discovered— we turned up more pages worth watching.params.countsays how many.health_changed— the subject's reachability shifted.params.healthcarries the new state. See When a tail goes cold.tracking_paused— the tail went quiet, usually because the balance ran low.billing_charged— a coin came off the ledger.params.coinssays how much.
curl "$CT_API/competitors/01HF.../timeline" \
-H "Authorization: Bearer $CT_TOKEN"Returns 200 OK with the full ordered list. 404 Not Found if the subject doesn't belong to your organization or you've already dropped it. The newest beat sits on top.
Update
PATCH /v1/competitors/{id}
Partial updates to the per-organization configuration. Both fields are optional but at least one must be present.
displayName—string, ornullto clear the override.trackedCategories— non-empty array of category names. Replaces the existing set entirely.
curl -X PATCH "$CT_API/competitors/01HF..." \
-H "Authorization: Bearer $CT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"trackedCategories":["pricing_changes","product_changes"]}'Returns 200 OK with the updated subscription.
Drop the case
DELETE /v1/competitors/{id}
Ends your organization's subscription to the competitor. The competitor itself remains tracked by the pack — only your organization's relationship to it ends. Returns 204 No Content.
curl -X DELETE "$CT_API/competitors/01HF..." \
-H "Authorization: Bearer $CT_TOKEN"After drop, the same id returns 404 from GET and PATCH. A new POST against the same URL opens a fresh case with a new id.
Tracked categories
The set below is current as of writing. The catalog grows over time. Treat the API as the authoritative list.
| Name | Default priority | Triggers on |
|---|---|---|
pricing_changes | high | Pricing page diffs (plans, line items, regional variations). |
product_changes | high | Feature additions, removals and renames in product surfaces. |
messaging_changes | low | Hero copy, value props, taglines. |
corporate_changes | medium | About, leadership and company-news pages. |
geo_changes | low | Region-specific landing pages and locale switches. |
Default priority is the priority we attach to a fresh change in that category. An individual change can be upgraded to high when its description suggests a pricing-relevant impact.
Scopes and permissions
What a Competitor Tracker & Co. credential can do: the scope catalog, the three organization roles and how scope and role combine to authorize each request.
Detected changes
Read the changes Competitor Tracker & Co. detects on competitors you track: pricing, product and messaging, filtered per competitor, per snapshot or org-wide.