Skip to content

Keyword.track

Reads, each day, where your pages rank for every term you track and which AI answers cite you, one row per page, term and day.

Action class
None
Approval
Runs on its own
Emits
Keyword.tracked
Price
3¢ per 100 reads
POST https://api.sb/{your-domain}/keywords/track

Inputs

NameTypeDefaultMeaning
keywordsKeyword[]every saved KeywordThe terms to track, as Keyword.create saved them. Unset: every Keyword the startup saved.
venues('google-search' | 'bing' | 'chatgpt' | 'perplexity' | 'gemini' | 'claude')[]['google-search']Where to read. A search engine returns a position; an answer engine returns whether the answer cites your site.
localestringyour home MarketThe market and language each read is taken in, such as en-US.
every'1d' | '1w''1d'How often each term is read. Answer engines read weekly whatever this says, since their answers move slower than results pages.
depthnumber100How many results deep a search read looks: a page past it reads position: null.
sincestring'30d'How much history the response returns: 7d, 30d, 90d. Reading history is free.
maxCostnumber5.00The most tracking may spend in a month, in USD. Over it, the next day’s reads are skipped and the call says so.

Outputs

FieldTypeWhat null means
tracking{ keywords: number; venues: VenueId[]; every: string; nextReadAt: string }Never null: what is tracked from now on, and when the next read runs.
historyRankHistory[]Never null: one row per page, term, Venue and day over since; empty on the first call, before any read.
history[].pagestring | nullNone of your pages appeared in the first depth results, or the answer cited none of them.
history[].positionnumber | null, in rank, 1 is the topNot in the first depth results, or an answer engine, which has no positions.
history[].feature'organic' | 'featured-snippet' | 'ai-overview' | 'local-pack' | 'answer' | nullYour page did not appear.
history[].citedboolean | nullA search engine row: only an answer engine cites.
history[].changenumber | null, in positions since the read beforeThe first read of this page for this term, or an answer engine row.
history[]ObservedEach row also carries observedAt, method, venue and locale.
costCost, in USDNever null: what today’s reads spent. In try mode usd is 0 and maxUsd is a month at this setting.

Effects

Action class

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

Modes
  • try: Runs one read of each term now and returns its rows, unsaved; nothing is scheduled and nothing is billed.
  • send: Saves the setting and returns the stored history at once; each scheduled read emits Keyword.tracked with its rows.
  • do: Saves the setting, runs today’s read, and returns the history with today’s rows in it.
Idempotency

Only with a key: the same Idempotency-Key and input return the first Action, and the same key with other input returns 409. Calling it again with other keywords or venues changes what is tracked from the next read; history is never rewritten.

Saved or shared

The tracking setting and every RankHistory row, kept for as long as the startup exists. Results pages are shared readings, as on Keyword.search; which of your pages ranked is yours alone.

Emits

Keyword.tracked with { keyword, rows }

Performed by

Content lead

Price

About 3¢ per 100 reads. At most 40¢ per 100 reads. Free: history reads, a term no page of yours targets yet, until it is saved.

On each Venue

It acts on your own records and reaches no Venue, so every account answers the same way.

What try does

Runs every check the call meets and returns the record as it would be; nothing is stored, sent or posted.

Refuses when

CheckErrorFixThen call
A data call whose price for what you asked is above its maxCost, checked before anything is spentmax-costLower limit, or raise maxCost to the price the refusal’s detail quotes.Organization.find
A search engine, answer engine or network the call does not read, such as ChatGPT sent to Keyword.measurevenue-unsupportedPick one the call’s venue input lists; AI answers are Keyword.ask, posts are Keyword.search.Keyword.ask
The max-cost payload
JSON
{
  "type": "https://startupbuilder.dev/errors/max-cost",
  "title": "The call would spend more than its cap",
  "status": 422,
  "detail": "A data call whose price for what you asked is above its `maxCost`, checked before anything is spent.",
  "fix": "Lower `limit`, or raise `maxCost` to the price the refusal’s detail quotes.",
  "next": {
    "verb": "Organization.find",
    "input": {
      "question": "US property managers with 20 to 400 units who still track rent in spreadsheets",
      "limit": 25,
      "maxCost": 0.05
    }
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}

When it waits

In send mode it returns the stored history at once, before the next read runs, and Keyword.tracked delivers the result.

TypeScript
await $.Watch.create({
  selector: 'Keyword.tracked',
  url: 'https://leaseline.app/hooks/sb'
})

Example

The second call, a week on: the history alone, free, for the one term the readout flagged.

You, to your agent
How did the rent ledger template page move on Google this week?
Response, in try mode
{
  "api": "https://api.sb/leaseline.app",
  "tracking": {
    "keywords": 1,
    "venues": [
      "google-search"
    ],
    "every": "1d",
    "nextReadAt": "2026-11-03T06:00:00Z"
  },
  "history": [
    {
      "keyword": "https://api.sb/leaseline.app/keywords/keyword_Rl1tE5mP3x",
      "term": "rent ledger template",
      "page": "https://leaseline.app/rent-ledger-template",
      "venue": "google-search",
      "locale": "en-US",
      "day": "2026-10-27",
      "position": 14,
      "feature": "organic",
      "cited": null,
      "change": 3,
      "observedAt": "2026-10-27T06:10:00Z",
      "method": "sampled"
    },
    {
      "keyword": "https://api.sb/leaseline.app/keywords/keyword_Rl1tE5mP3x",
      "term": "rent ledger template",
      "page": "https://leaseline.app/rent-ledger-template",
      "venue": "google-search",
      "locale": "en-US",
      "day": "2026-11-02",
      "position": 9,
      "feature": "organic",
      "cited": null,
      "change": 5,
      "observedAt": "2026-11-02T06:10:00Z",
      "method": "sampled"
    }
  ],
  "cost": {
    "usd": 0,
    "maxUsd": 0,
    "units": 0,
    "misses": 0
  },
  "action": {
    "operation": "Keyword.track",
    "mode": "try",
    "status": "done"
  }
}

To retry, send the same Idempotency-Key: the retry returns the first answer for $0. A new key reads again.

Test values

A sk_test_ key answers these the same way every time, at $0 (every test value).

Test valueAlways returnsWhat it is for
keywords: ['keyword_test_track']{"history":[{"term":"rent ledger template","position":14,"feature":"organic"}]}Always position 14 on Google, so the step that fires on page one can be tested with a known miss.

Used by

None.