URL: /code/data/keywords/measure · Id: `page:/code/data/keywords/measure`

# Keyword.measure

> Measure the terms you name in one locale: monthly searches, cost per click, difficulty from 0 to 100, intent and the 12-month trend.

`POST https://api.sb/{your-domain}/keywords/measure`

Measure the terms you name in one locale: monthly searches, cost per click, difficulty from 0 to 100, intent and the 12-month trend.

- **Price:** 18¢ per 100 terms
- **Success rate:** 99.94% of 7,106 calls
- **Freshness:** Fresh for 30 days
- **Emits:** `Keyword.measured`
- **Capability:** [Market data](https://startupbuilder.dev/systems/marketing/market-data)

```text title="Run it · Ask"
How many people search “rent ledger template” a month, what does a click cost, and how hard is page one? Spend up to 1¢.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Keyword.measure({ keywords: ['rent ledger template', 'rent ledger for 50 units'], locale: 'en-US', maxCost: 0.01 })
```

```json title="Run it · Response"
{
  "measurements": [
    {
      "term": "rent ledger template",
      "searches": 1900,
      "cpc": 2.4,
      "competition": 0.09,
      "difficulty": 18,
      "intent": "learn",
      "trend": "rising",
      "history": [
        {
          "month": "2025-10",
          "searches": 1300
        },
        {
          "month": "2026-09",
          "searches": 1900
        }
      ],
      "venue": "google-search",
      "locale": "en-US",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "estimated"
    },
    {
      "term": "rent ledger for 50 units",
      "searches": null,
      "cpc": null,
      "competition": null,
      "difficulty": 6,
      "intent": "buy",
      "trend": null,
      "venue": "google-search",
      "locale": "en-US",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "estimated"
    }
  ],
  "cost": {
    "usd": 0,
    "maxUsd": 0.0036,
    "units": 1,
    "misses": 1
  }
}
```

## Options and prices
| You get | Input that selects it | Price | Success rate |
| --- | --- | --- | --- |
| All five measures in one row | The default | About 18¢ per 100 terms | 99.94% of 7,106 calls |
| Monthly searches and cost per click | `{"measures":["searches","cpc"]}` | About 5¢ per 100 terms | 100% of 14,260 calls |
| Difficulty, for up to 1,000 terms at once | `{"measures":["difficulty"]}` | About 1.2¢ a batch of up to 1,000 terms | 100% of 1,360 calls |
| Intent, for up to 1,000 terms at once | `{"measures":["intent"]}` | About 1.2¢ a batch of up to 1,000 terms | 99.8% of 588 calls |
| The 12-month trend | `{"measures":["trend"]}` | About 12¢ per 100 terms | 97.9% of 7,973 calls |
| How often the term is asked of AI tools | `{"measures":["aiSearches"]}` | About 1¢ a term | 100% of 1,137 calls |
| The same measures on Bing | `{"venue":"bing"}` | About 9¢ a batch of up to 1,000 terms | 100% of 80 calls |

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `keywords` | `string[]` | None. | Up to 1,000 phrases, measured in one call. Pass this or `keyword`. |
| `keyword` | `Keyword` | None. | A saved Keyword id. Also served at `/keywords/{keyword}/measure`, as the hub defines it. |
| `venue` | `'google-search' \| 'bing'` | `'google-search'` | The search site. An answer engine or a forum is refused with `venue-unsupported`: those are other calls. |
| `locale` | `string` | `your home Market` | The market and language, such as `en-US`. |
| `measures` | `('searches' \| 'cpc' \| 'difficulty' \| 'intent' \| 'trend' \| 'aiSearches')[]` | `every one but 'aiSearches'` | What to measure. Each measure is its own job with its own price. |
| `maxAge` | `string` | `'30d'` | A stored reading younger than this returns at $0 instead of reading again. |
| `maxCost` | `number` | `0.50` | The most this call may spend, in USD. Over it, the call is refused before it spends, or stops early with what it has. |

## Outputs
| Field | Type | Unit | What `null` means |
| --- | --- | --- | --- |
| `measurements` | `Keyword[]` | None. | Never null: one row per term sent, in order. |
| `measurements[].term` | `string` | None. | Never null. |
| `measurements[].searches` | `number \| null` | searches a month, 12-month average | Unknown: no reading. Never zero demand, and never billed. |
| `measurements[].cpc` | `number \| null` | USD a click | No advertiser bids on it, or no reading. |
| `measurements[].competition` | `number \| null` | 0 to 1, advertisers bidding | No reading. |
| `measurements[].difficulty` | `number \| null` | 0 to 100 | Unknown, or not in `measures`. |
| `measurements[].intent` | `'buy' \| 'compare' \| 'learn' \| 'navigate' \| null` | None. | Unknown, or not in `measures`. |
| `measurements[].trend` | `'rising' \| 'flat' \| 'falling' \| null` | 12 months | Under 12 months of readings. |
| `measurements[].history` | `{ month: string; searches: number }[]` | searches a month | Absent without `trend`; a month with no reading is left out. |
| `measurements[].aiSearches` | `number \| null` | asks a month | Absent without `aiSearches`; `null` is no reading. |
| `measurements[]` | [`Observed`](/code/data/types#observed) | None. | Each row also carries `observedAt`, `method`, `venue` and `locale`. |
| `cost` | [`Cost`](/code/data/types#cost) | USD | Never null. In try mode `usd` is 0 and `maxUsd` is what the call would have cost. |

## Read the result
| Field | Value | What it means | What to do |
| --- | --- | --- | --- |
| `difficulty` | 10, 20, 30, 40, 50, 60, 70 | About 10, 22, 36, 56, 84, 129 and 202 referring domains to reach page one: the scale the score is defined on. | Your ceiling is the difficulty your referring domains reach. A new domain with 8 sits near 10; target at or under it. (the published referring-domain scale) |
| `difficulty` | Low on a local term | The score understates the map pack: one review read 7 for a term owned by local listings. | Read the results page with `Keyword.search` before you trust it. (reviews of the difficulty read) |
| `searches` | `null` | Unknown, not zero. You were not billed for it. | Keep the term if a buyer writes it in a problem post. (the contract every data call keeps) |
| `cpc` | Over $4.29 for a $149 a month product | Above break-even: 80% margin × 12 months × a 0.3% visit-to-paid rate. | Earn this click with a page; do not buy it. (break-even cost per click, the strategy numbers) |
| `trend` | `falling` | Demand is leaving the term. | Drop it if `history` fell more than 30% across 12 months. (our planning number) |

## Effects
**Action class**

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

**Modes**

- `try`: Runs the read and returns real rows; nothing is saved and nothing is billed.
- `send`: Returns a pending Action at once; its Event carries the rows when the read finishes.
- `do`: Waits for the read and returns the rows, billed per result.

**Idempotency**

The same `Idempotency-Key` with the same input replays the first answer at $0; with other input it returns 409.

**Saved or shared**

Each measurement is an immutable `Keyword.measured` Event; nothing is edited. Measurements are shared across every startup that measures the same phrase and locale.

**Emits**

`Keyword.measured` with `{ term, searches, cpc, competition, difficulty, intent, trend, venue, locale }, one Event per term`

**Performed by**

[Analyst](https://startupbuilder.dev/agents/marketing/analyst)

**Price**

About 18¢ per 100 terms. Free: a reading younger than `maxAge`, a term with no reading, volume and cost per click from your own Google Ads account. `maxCost` defaults to 50¢. 500 terms, every measure: 500 × 0.18¢ a term is about 90¢.

**Reliability**

The first route succeeds 99.94% of 7,106 calls. If it fails, the call refuses with `sources-busy` and nothing is spent.

**Freshness**

A reading younger than 30 days returns at $0. Readings are shared across startups for the same subject and locale.

**Batch and pages**

Up to 1,000 terms a call. It answers in one response.

**Rate**

[The shared rate rules](/code/data#rate): 10 calls a second per key, and an array input counts as one call.

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A search engine, answer engine or network the call does not read, such as ChatGPT sent to `Keyword.measure` | [`venue-unsupported`](/code/refusals#venue-unsupported) | Pick one the call’s `venue` input lists; AI answers are `Keyword.ask`, posts are `Keyword.search`. | [`Keyword.ask`](/code/data/keywords/ask) |

Every data call can also meet [the shared data-call refusals](/code/refusals#data-calls): [`filter-unsupported`](/code/refusals#filter-unsupported), [`input-missing`](/code/refusals#input-missing), [`limit-exceeded`](/code/refusals#limit-exceeded), [`max-cost`](/code/refusals#max-cost), [`rate-limited`](/code/refusals#rate-limited), [`sources-busy`](/code/refusals#sources-busy).

```json
{
  "type": "https://startupbuilder.dev/errors/venue-unsupported",
  "title": "The call does not read that search engine or network",
  "status": 422,
  "detail": "A search engine, answer engine or network the call does not read, such as ChatGPT sent to `Keyword.measure`.",
  "fix": "Pick one the call’s `venue` input lists; AI answers are `Keyword.ask`, posts are `Keyword.search`.",
  "next": {
    "verb": "Keyword.ask",
    "input": {
      "keyword": "best rent tracking software for a 40-unit property manager",
      "venues": [
        "chatgpt"
      ]
    }
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
How many people search “rent ledger template” a month, what does a click
cost, and how hard is page one? Spend up to 1¢.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/keywords/measure?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":["rent ledger template","rent ledger for 50 units"],"locale":"en-US","maxCost":0.01}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Keyword.measure --try --data '{"keywords":["rent ledger template","rent ledger for 50 units"],"locale":"en-US","maxCost":0.01}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Keyword.measure({ keywords: ['rent ledger template', 'rent ledger for 50 units'], locale: 'en-US', maxCost: 0.01 })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Keyword.measure({
  keywords: ['rent ledger template', 'rent ledger for 50 units'],
  locale: 'en-US',
  maxCost: 0.01,
})
```

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

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `rent ledger template` | `{"term":"rent ledger template","searches":1900,"cpc":2.4,"difficulty":18,"intent":"learn","trend":"rising"}` | 1,900 searches a month, $2.40 a click and difficulty 18, every time. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

Watch it on a schedule: a Monitor runs this call and records an Event only when the reading changes.

```ts
await $.Monitor.create({
  verb: 'Keyword.measure',
  input: { keywords: ['rent ledger template'], measures: ['searches', 'cpc'] },
  change: 'volume-change',
  every: '4w',
})
```

## Used by
- **Workflows:** [Keyword opportunity map](/capabilities/research-demand/keyword-opportunity-map) · [Pick who to sell to first](/capabilities/find-customers/pick-who-to-sell-to)
- **Agent tasks:** [Monthly search content](/capabilities/make-content/monthly-search-content)
- **Jobs:** [Research demand](/capabilities/research-demand) · [Run ads](/capabilities/run-ads)

## Related
[`Keyword.find`](/code/data/keywords/find) · [`Keyword.forecast`](/code/data/keywords/forecast) · [`Keyword.search`](/code/data/keywords/search) · [`Keyword.ask`](/code/data/keywords/ask)