Competitor Tracker & Co. Docs

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 returns 422 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.com tracks the whole company.
  • A deep link like https://example.com/marketing tracks 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:

  • competitorKindcompany or sub_product.
  • parentCompetitorId — the company a sub-product belongs to, or null when 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 in nextCursor from 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:

  • name and description are extracted from the competitor's own metadata and refreshed on each read — null until the first read lands.
  • faviconUrl is a stable link to the competitor's favicon that doesn't expire, so it's safe to cache and reuse. It's null until the first read captures one, and the image updates in place when a later read picks up a new icon.
  • competitorKind and parentCompetitorId place the subject in its company. See Companies and sub-products. The list omits a company's children; fetch the company to read them.
  • labels are the per-organization tags you've assigned to this subscription; see Labels.
  • paused is true when the pack stops reading the page because the organization's coin balance can't cover the next cycle. pauseReason carries the slug.
  • nextChargeAt is when the next coin-deduction lands for this subscription. null for own-app subscriptions, paused rows or rows that haven't been read once yet.
  • crawlHealth flags a subject we can no longer read — permanent_dead, permanent_blocked, uncrawlable or null when healthy. See When a tail goes cold. healthDetail is an internal note for support and is often null; key your logic on crawlHealth.

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. 0 for your own app, which we tail free.
  • isOwnApptrue when the subject is your own application. We don't bill you to watch yourself.
  • coinsPaidUntil — when the paid cycle runs out, or null when 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.changeCount says 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.count says how many.
  • health_changed — the subject's reachability shifted. params.health carries 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.coins says 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.

  • displayNamestring, or null to 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.

NameDefault priorityTriggers on
pricing_changeshighPricing page diffs (plans, line items, regional variations).
product_changeshighFeature additions, removals and renames in product surfaces.
messaging_changeslowHero copy, value props, taglines.
corporate_changesmediumAbout, leadership and company-news pages.
geo_changeslowRegion-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.

On this page