URL: /code/data/pages-and-sites/check-index · Id: `page:/code/data/pages-and-sites/check-index`

# Page.checkIndex

> Read each page’s index state on Google and Bing, with the engine’s own reason, its last crawl and the canonical it chose, and the fix.

POST https://api.sb/&#123;your-domain&#125;/pages/check-index

Read each page’s index state on Google and Bing, with the engine’s own reason, its last crawl and the canonical it chose, and the fix.

- **Price:** Free
- **Success rate:** 99.9% of 53,608 calls
- **Freshness:** Fresh for 1 day
- **Emits:** `Page.indexChecked`

```text title="Run it · Ask"
Is the rent ledger template page indexed on Google yet, and if not, why? Spend nothing.
```

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

const result = await $.try.Page.checkIndex({ pages: ['https://leaseline.app/', 'https://leaseline.app/rent-ledger-template'], venues: ['google-search'], maxCost: 0 })
```

```json title="Run it · Response"
{
  "pages": [
    {
      "url": "https://leaseline.app/",
      "venue": "google-search",
      "state": "indexed",
      "reason": null,
      "lastCrawl": "2026-10-24T11:02:00Z",
      "canonical": "https://leaseline.app/",
      "robots": "allowed",
      "lagDays": 2,
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "reported"
    },
    {
      "url": "https://leaseline.app/rent-ledger-template",
      "venue": "google-search",
      "state": "crawled-not-indexed",
      "reason": "Crawled, currently not indexed",
      "lastCrawl": "2026-10-23T08:41:00Z",
      "canonical": "https://leaseline.app/rent-ledger-template",
      "robots": "allowed",
      "lagDays": 2,
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "reported"
    }
  ]
}
```

## Inputs
| Name | Type | Meaning |
| --- | --- | --- |
| `pages` | `string[]` | Up to 2,000 URLs a day per site; your Site’s key pages when unset. Default `the Site’s key Pages`. |
| `sitemap` | `string` | A sitemap URL: every page it lists is checked, in place of `pages`. |
| `venues` | `('google-search' \| 'bing')[]` | The engines to read; each needs its account connected. Default `['google-search', 'bing']`. |
| `maxAge` | `string` | A reading younger than this returns at $0 and spends none of the day’s 2,000. Default `'1d'`. |

## Outputs
| Field | Type | What `null` means | What it means |
| --- | --- | --- | --- |
| `pages` | `PageIndex[]` | Never null: one row for each page and engine. | `not-seen`: The engine has never found the page. Confirm it is in the sitemap, link it from 2 indexed pages, and earn one followed outside link. *Source: the index-state playbook, 2026.*<br />`discovered-not-indexed`: Found, not crawled yet: low priority on a new domain. Link it from the home and pricing pages; request indexing for key pages only. *Source: the index-state playbook, 2026.*<br />`crawled-not-indexed`: Crawled and judged not worth indexing yet. Merge thin pages and add a fact only you have; do not request again until the page changed. *Source: the index-state playbook, 2026.*<br />Key pages indexed under 80% by week 2, or under 100% by week 4: Discovery is the problem, not content. Fix discovery before you write another page. *Planning estimate.*<br />Over 30% `crawled-not-indexed` at week 6: A quality problem across the site. Pause publishing and fix the thin pages first. *Planning estimate.* |
| `pages[].url` | `string` | Never null: the page as sent, or as the sitemap lists it. | None. |
| `pages[].state` | `'indexed' \| 'discovered-not-indexed' \| 'crawled-not-indexed' \| 'duplicate' \| 'not-seen' \| 'excluded'` | Never null. | None. |
| `pages[].reason` | `string \| null` | Null when indexed, or on Bing; otherwise the engine’s own wording. | None. |
| `pages[].lastCrawl` | `string \| null`, in ISO 8601 | Never crawled. | None. |
| `pages[].canonical` | `string \| null` | The engine has not chosen one yet, or Bing, which does not say. | None. |
| `pages[].robots` | `'allowed' \| 'blocked' \| null` | The engine did not report it. | None. |
| `pages[].lagDays` | `number`, in days | Never null: how far behind the engine’s own report runs. | None. |
| `pages[]` | [`Observed`](/code/data/types#observed) | Each row carries `observedAt`, `method` and `venue`: `reported` on Google, `sampled` on Bing. | None. |

### On each Venue
| Venue | What comes back | Limits | Price |
| --- | --- | --- | --- |
| Google Search | State, Google’s own reason, last crawl, the canonical Google chose and robots, `method: reported`. | 2,000 pages a day per Search Console property; the report runs about 2 days behind. | None. |
| Bing | State and last crawl, read by a browser agent signed in to Bing Webmaster, `method: sampled`. | No reason or canonical: Bing’s report shows neither. | None. |

## Effects
**Action class**

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

**Modes**

- `try`: Runs every check and returns the rows; nothing is written onto the pages.
- `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**

Writes each page’s state onto its page (`indexed` on the SEO System’s page), so `Metric.query` can chart it; nothing is shared with other startups.

**Emits**

`Page.indexChecked` with `{ pages }`

**Performed by**

[Content lead](https://startupbuilder.dev/agents/marketing/content-lead)

**Price**

Free. Free: every check, read from your own Search Console and Bing Webmaster.

**Reliability**

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

**Freshness**

A reading younger than 1 day returns at $0. Readings about people are never shared across startups. The source runs 2 days behind.

**Batch and pages**

Up to 2,000 pages a day per site, shared with every other tool on that property. Over 50 pages it answers in `send` mode with a pending Action at once.

**Rate**

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

**Capability**

[Domains](https://startupbuilder.dev/products/platform/domains)

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| The call needs an account you have not connected, such as your Search Console property or your Meta ad account | [`account-not-connected`](/code/refusals#account-not-connected) | Connect the account with [`Integration.connect`](https://startupbuilder.dev/business/nouns/integration/connect), approve it at the connectUrl, then send the call again. | [`Integration.connect`](/code/integrations/calls/connect) |
| The account’s own daily or per-minute cap is used up, such as 2,000 page checks a day per Search Console property | [`quota-reached`](/code/refusals#quota-reached) | Retry after the reset time the response gives; nothing was sent or spent. | [`Action.retry`](https://startupbuilder.dev/business/nouns/action/retry) |
| 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/account-not-connected",
  "title": "Account not connected",
  "status": 409,
  "detail": "The call needs an account you have not connected, such as your Search Console property or your Meta ad account.",
  "fix": "Connect the account with Integration.connect, approve it at the connectUrl, then send the call again.",
  "next": {
    "verb": "Integration.connect",
    "input": {
      "system": "ads",
      "venue": "facebook",
      "scope": "startup",
      "grants": [
        "ads_management",
        "ads_read"
      ]
    }
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
A call over 50 rows returns a pending Action; its `Page.indexChecked` Event carries the rows; nothing needs your approval.

```ts
await $.Watch.create({
  selector: 'Page.indexChecked',
  url: 'https://leaseline.app/hooks/sb',
})
```

## Example
A batch: over 50 pages, the check answers in `send` mode with a pending Action, and `Page.indexChecked` carries the rows.

**Ask**

```text
Check every page in my sitemap on Google and Bing: 60 pages with the 12
new guides.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/pages/check-index?mode=send" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "sitemap": "https://leaseline.app/sitemap.xml",
  "venues": [
    "google-search",
    "bing"
  ]
}'
```

**CLI**

```bash
npx startup-builder do Page.checkIndex --send --data '{
  "sitemap": "https://leaseline.app/sitemap.xml",
  "venues": [
    "google-search",
    "bing"
  ]
}'
```

`--send` returns the pending Action at once.

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "send",
    "code": "return await $.Page.checkIndex({ sitemap: 'https://leaseline.app/sitemap.xml', venues: ['google-search', 'bing'] })"
  }
}
```

**SDK**

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

const result = await $.send.Page.checkIndex({
  sitemap: 'https://leaseline.app/sitemap.xml',
  venues: ['google-search', 'bing'],
})
```


```json title="Response, in send mode"
{
  "mode": "send",
  "action": {
    "$id": "https://api.sb/leaseline.app/actions/action_Ix5pC2kQ8v",
    "operation": "Page.checkIndex",
    "status": "pending",
    "reason": "running"
  },
  "pages": []
}
```

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/checks#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `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: 'Page.checkIndex',
  input: { venues: ['google-search'] },
  change: 'index-change',
  every: '1d',
})
```

## Used by
- **Jobs:** [Get indexed](/capabilities/get-indexed) · [Measure](/capabilities/measure)

## Related
[`Page.audit`](/code/data/pages-and-sites/audit-page) · [`Site.audit`](/code/data/pages-and-sites/audit-site)