Competitor Tracker & Co. Docs

Agent signup

How an AI agent starts competitor tracking for a person who has no account yet: name a few companies, read their history straight away and hand back a link the person uses to take the organization over.

An AI agent can put a few companies on file for somebody who has no account yet. It names the companies. We file the ones we already track and hand back a link the person uses to take the organization over. Nothing is charged and nothing lands in anyone's inbox.

That is the whole shape. The rest of this page is the fine print, and there is one real limit in it worth reading before you build.

Who this is for

An assistant acting for a person who has never heard of us. A conversation that reached "you should be watching what these three are doing" can act on it rather than recommend it, and our lead detective C. T. Lucky opens the case while the person is still reading the reply.

The endpoint is public. There is no credential to present, because the person it is for does not have one yet. If your caller already has an account, use Competitors instead — this path is for the moment before the account.

Start a signup

POST /v1/agent-signups

Send the companies in the order they matter to the person you are acting for, plus your own id for the conversation the ask came from. Up to 20 names per call.

Call this once per person. If more companies come up later, add them to the same organization rather than calling this again — a second call here opens a second organization with a second link, and the person ends up with two things to claim.

# No key, no account. The claim link comes back in the response.
curl -X POST "https://api.competitortracker.io/v1/agent-signups" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": ["Notion", "linear.app", "Asana", "Gamma"],
    "sessionId": "conv_01J2ZQ8F4T"
  }'
{
  "status": "created",
  "organization": {
    "id": "019e9422-7784-7026-90cb-2f30dfe0b4f4",
    "name": "Notion, Linear, Asana",
    "expiresAt": "2026-09-07T10:12:44.000Z"
  },
  "subscribed": [
    {
      "requested": "Notion",
      "id": "019e9422-7784-7026-90cb-2f30dfe0b4f4",
      "slug": "notion",
      "name": "Notion",
      "url": "https://notion.so",
      "affiliated": false
    }
  ],
  "shortfalls": [
    { "ask": "Asana", "reason": "over_cap" },
    { "ask": "Gamma", "reason": "unpublished" }
  ],
  "claim": {
    "url": "https://app.competitortracker.io/claim/6f1b2c3d4e5f60718293a4b5c6d7e8f9",
    "expiresAt": "2026-09-07T10:12:44.000Z"
  },
  "access": {
    "token": "<short-lived read key>",
    "expiresAt": "2026-09-07T10:12:44.000Z",
    "scopes": [
      "competitors:read",
      "changes:read",
      "snapshots:read",
      "pages:read",
      "agent-signup:topup"
    ]
  }
}

subscribed and shortfalls are one answer in two halves: what went on file, and what did not. Both keep the order you asked in, and every company you named is in exactly one of them. requested echoes the string you sent, so you never have to reconcile our name for a company against the one the person used.

claim.url is the link. It comes back here and travels nowhere else. We have no address for this person and would not write to them if we did, so passing it on is your job.

sessionId ties the link back to the conversation it came from. We never treat it as a credential.

We file companies we already track

This is the limit, and it is a hard one. An agent cannot hand us an address we have never seen and have us go and fetch it. Every entry in companies is matched against the companies we already publish, by name or by domain. Anything we do not hold comes back in shortfalls with reason set to unpublished.

That is not a failure and it should not be repeated as one. The person can add whatever we missed once the organization is theirs, by address, exactly the way anybody else adds a company. Tell them what landed, then tell them what they can add themselves.

You can read the published set before you call. It is the same set track lists serves, industry by industry.

On a catalog we cannot read

If our catalog is unavailable for a moment we answer 503 with status set to catalog_unavailable and create nothing. We will not call a company unpublished on the strength of a page we failed to read. Try again shortly.

Three slots

Three companies get filed. The cap is on the total, not on one call, so it counts everything already on file. On a first call the first three you list are the ones. The rest come back as shortfalls with reason set to over_cap — we do have the company, it is waiting behind the claim.

Nothing past the cap is dropped. The whole ask rides on the claim link, so the page the person lands on shows what they asked for rather than the part that fitted. Order is the entire priority rule, so put the ones that matter first.

Adding more later

POST /v1/agent-signups/companies

You do not have to know every company up front. This adds more to an organization you already opened, so the three slots can be filled over several calls.

Authenticate with access.token from the call that opened it. That key is what says which organization to add to — there is no field for naming one, and there could not be: a name anybody can write is not a credential. The key lasts as long as the organization does.

curl -X POST "https://api.competitortracker.io/v1/agent-signups/companies" \
  -H "Authorization: Bearer $AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "companies": ["Asana", "Gamma"] }'
{
  "status": "updated",
  "organization": {
    "id": "019e9422-7784-7026-90cb-2f30dfe0b4f4",
    "name": "Notion, Linear",
    "expiresAt": "2026-09-07T10:12:44.000Z"
  },
  "added": [
    {
      "requested": "Asana",
      "id": "019e9422-7784-7026-90cb-2f30dfe0b4f5",
      "slug": "asana",
      "name": "Asana",
      "url": "https://asana.com",
      "affiliated": false
    }
  ],
  "alreadyTracking": [],
  "shortfalls": [{ "ask": "Gamma", "reason": "unpublished" }],
  "trackedCount": 3,
  "slotsRemaining": 0,
  "claim": { "expiresAt": "2026-09-07T10:12:44.000Z" }
}

Everything the first call promised holds here. Companies are matched by name or by domain against the ones we already track, and no address you send is ever fetched. Nothing is charged. trackedCount and slotsRemaining say whether it is worth asking again.

added is what this call put on file. A company that was already there comes back in alreadyTracking and takes no slot, so calling twice with the same name files it once. Anything that does not fit is a shortfall, over_cap when we hold the company and unpublished when we do not.

The claim link does not change and does not need re-sending. It is kept up to date, so the person sees everything on file and everything still outstanding whenever they open it — including a company that was outstanding on the first call and arrived on the second.

Three answers mean the organization is no longer yours to extend. Each is a 200 with status set to refused and a reason:

ReasonWhat happened
already_claimedThe person has taken the organization over. The companies are theirs now.
expiredNobody claimed it within four days and it is on its way out.
not_a_shellThe key belongs to an ordinary account, which adds companies the usual way.

Read the history straight away

access.token is a bearer token for the organization that just opened. It reads — competitors:read, changes:read, snapshots:read and pages:read — plus one narrow thing it can write: agent-signup:topup, which opens the top-up and nothing else. It cannot add a company by address, cannot see a balance and cannot touch the organization itself.

That last part is the reason the top-up has its own route. The ordinary way to add a company takes an address, and a key that could reach it could have us fetch anything at all. This one takes names and matches them against what we already track, which is the same limit the first call works under.

curl "https://api.competitortracker.io/v1/changes" \
  -H "Authorization: Bearer $AGENT_TOKEN"

This is the point of the whole path. We have been tailing these companies for months already, so their record is there the minute the organization opens rather than a week later. Ask what they have been up to.

The key expires with the organization it opened, and it stops working the moment somebody claims — by then the organization it read is gone.

access can come back null. That is rare and it is not an error: the organization exists and the claim link works, so the signup stands. You are the one who goes without.

The claim

What the person sees

GET /v1/claims/{token}

Public, gated by nothing but the token in the link. This is what the claim page renders.

{
  "status": "ready",
  "companies": [
    {
      "competitorId": "019e9422-7784-7026-90cb-2f30dfe0b4f4",
      "name": "Notion",
      "url": "https://notion.so",
      "affiliated": false,
      "costCoins": 1
    }
  ],
  "shortfalls": [{ "ask": "Gamma", "reason": "unpublished" }],
  "cost": { "companies": 3, "coinsPerCompany": 1, "coinsPerCycle": 3 },
  "expiresAt": "2026-09-07T10:12:44.000Z",
  "claimedAt": null
}

status is one of four answers: ready, already_claimed, expired or unknown. Looking spends nothing. Somebody can read this, close the tab and come back tomorrow to the same link.

Taking it on

POST /v1/claims/{token}/accept

The person signs in first — a new one signs up the ordinary way — and the companies move into the organization they own. Coins are spent here and nowhere earlier, at the rate everything else costs: one coin per company per cycle. The cost is on the screen before the button, which is why the preview carries it.

curl -X POST "https://api.competitortracker.io/v1/claims/$TOKEN/accept" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "decline": ["019e9422-7784-7026-90cb-2f30dfe0b4f4"] }'

Name any company in decline to leave it out. An absent list means take all of them. Every company comes back with its own outcome:

OutcomeWhat it means
mergedOn file, charged like any other company.
declinedYou asked us to leave it out.
already_trackingYou were following it before the claim.
insufficient_balanceYour coins ran out before this one.
unavailableWe no longer hold that company.
failedSomething went wrong on our side.

A balance that runs out part-way through is reported rather than rolled back. What fitted is on file and the rest is named, so nobody has to guess which half happened.

The link works once

It is spent on accept, never on the preview. A claim that runs out of coins has still spent the link, so check the balance before confirming and untick a company if the total does not fit.

Once the companies have moved across, the organization the agent opened is removed. It has done its job.

Four days

An unclaimed organization is removed four days after it opens. organization.expiresAt and claim.expiresAt name the same instant, deliberately: a link that outlived the organization behind it would promise a move it could not make.

An expired link is not a dead end. It still answers with the companies that were on it, so what the person lands on is an ordinary signup with those companies already picked. They get the same companies, paid for the usual way.

Nothing is charged, nothing is sent

Before a claim there are no coins in that organization, no weekly report is written for it and no mail goes anywhere. The person you are acting for is not billed, not mailed and not signed up to anything until they say yes on the confirmation screen. What the agent reads in the meantime is work the pack of AI agents had already done.

Companies we are affiliated with

affiliated is true when a company is one of ours. We track it exactly like any other and watch it the same way — we would rather say so than let somebody find out later. The flag is on the signup response and on the claim preview, because a disclosure that waits until after the decision is not much of a disclosure. Pass it on to the person you are acting for.

What can go wrong

StatusWhen
400The body fails validation. One company minimum, 20 maximum.
429Too many signups started from here lately. Nothing was created.
503We could not read our catalog. Nothing was created and nothing is called missing.

The endpoint is public, so it is metered. A 429 is a "wait and retry", not a refusal: nothing was created and the person's ask is intact.

The top-up is metered too, on its own budget rather than the one that opens organizations — filling three slots across three calls costs you nothing extra. It answers the same three statuses, and a 429 there means the same thing: wait, then ask again.

Over MCP

Our MCP server exposes this as start_tracking_competitors, and it is the one tool a client with no credential may call. Everything else in the catalog stays behind the front door, and a credential is what opens it.

sessionId is optional there — we make one up when your client has none. The answer is the response above, verbatim.

The top-up is add_tracked_competitors, and it needs the key the first call returned, since that key is what says which organization to add to. Connect with it as your credential and the tool is there.

See Model Context Protocol for connecting a client, and the tool catalog for the rest of what a signed-in caller gets.

On this page