API reference

Index check

Is a URL in Google's index? Query page 1 of `site:<url>` and track when results are fresh.

Endpoint overview

The index check endpoints answer whether each URL appears in Google's index by running a site: crawl on page 1 only (no AI Overview, no feature capture). Use /index/v1/check to start new checks or refresh stale ones; /index/v1/status to poll without resubmitting. Both return the same response format.

Each URL that enters a new crawl (not cached, not already in flight) consumes one request against your quota. Polling and retrying a failed crawl are always free.

Start or refresh a check

POST/index/v1/check

Check whether each URL is in Google's index. Starts a crawl for URLs never checked before, older than 24 hours, or when force: true is set. Reuses results from the last 24 hours unless forced. A failed crawl is automatically retried (up to 3 rounds).

request
curl -X POST https://serp-api.hoangha.shop/index/v1/check \
  -H "X-API-Key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/page",
      "https://example.com/blog/post",
      "https://example.org/article"
    ],
    "force": false,
    "gl": "vn",
    "hl": "vi"
  }'
response
{
  "isSuccess": true,
  "content": [
    {
      "url": "https://example.com/page",
      "status": "INDEXED",
      "reason": null,
      "lastCheckedAt": "2026-10-06T14:23:45Z",
      "submitted": true
    },
    {
      "url": "https://example.com/blog/post",
      "status": "NOT_INDEXED",
      "reason": "NO_RESULTS",
      "lastCheckedAt": "2026-10-06T10:15:30Z",
      "submitted": false
    },
    {
      "url": "https://example.org/article",
      "status": "PENDING",
      "reason": null,
      "lastCheckedAt": null,
      "submitted": true
    }
  ]
}
FieldTypeDescription
urlsrequiredarrayList of URLs to check. 1–100 URLs per request. Each URL must be a valid http(s) URL with a hostname, up to 395 characters. Duplicates (after normalisation) are deduplicated.
forcebooleanRe-crawl even if the URL was checked in the last 24 hours. Default false. Does not count as a new submission when a fresh result already exists.
glstringCountry code, e.g. "vn", "us", "gb". Geo-targets the search. Passed to the `site:` crawl. Optional.
hlstringInterface language, e.g. "vi", "en". Language-targets the search interface. Passed to the `site:` crawl. Optional.

Poll without submitting

POST/index/v1/status

Look up the current index status for each URL without starting a new crawl or refreshing stale results. Useful for polling in a loop. A failed crawl is still retried up to 3 rounds while you poll.

request
curl -X POST https://serp-api.hoangha.shop/index/v1/status \
  -H "X-API-Key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/page",
      "https://example.com/blog/post"
    ]
  }'

The response format is identical to /check.

FieldTypeDescription
urlsrequiredarrayList of URLs to look up. 1–100 URLs per request. Same format and validation as /check.

Response statuses

INDEXED

The URL appears in Google's index. A result from page 1 matches the URL after normalisation (scheme, www., trailing slash, fragment, and host case are ignored). lastCheckedAt is the timestamp when the crawl finished.

NOT_INDEXED

The URL is not in Google's index. reason explains why:

  • NO_RESULTS: the site: search returned no results at all.
  • ONLY_SIMILAR_URLS: results exist but none match this URL. (The site: operator is a prefix match, so related URLs may appear.)

lastCheckedAt is the timestamp when the crawl finished.

PENDING

A crawl is queued, running, or being retried. reason is null unless:

  • CRAWL_FAILED: the crawl failed 3 times and all retries are exhausted. Call /check again to restart it — the restart is free (no charge, no force needed).

lastCheckedAt is null while in this state.

NOT_SUBMITTED

The URL has never been checked, or the cached state expired (7 days). Use /check to start a crawl. lastCheckedAt is null.

Caching and freshness

Results are cached for 24 hours from the finish time of the crawl. A subsequent check returns the cached result without a new crawl.

  • On /check: if a result is less than 24 hours old, it is returned and submitted: false (no charge). Set force: true to crawl fresh.
  • On /status: only reads cached state; never submits a new crawl.
  • A crawl that fails is retried automatically (no manual action needed) and does not count as a new submission.

Typical usage

Start a check with /check, then poll with /status to avoid accidental resubmissions. This way you pay once per URL per 24 hours.

URL normalisation

URLs are compared after normalisation. The following variations are treated as the same page:

  • http:// vs https://
  • example.com vs www.example.com
  • example.com/page vs example.com/page/ (trailing slash)
  • example.com/page#section (fragments are ignored)
  • Example.com vs example.com (host case is ignored)

Query strings and path case are NOT normalised and are compared exactly.

Error handling

FieldTypeDescription
400errorInvalid request: missing or empty urls list, more than 100 URLs, a URL that is not http(s), or a URL longer than 395 characters.
401errorMissing or invalid API key.
402errorInsufficient credits. Only on serp-portal (portal bills 1000 credits per new check).
429errorRate limit exceeded or quota backpressure. On /check when no new crawls can be queued. Retry after a delay.

Billing (serp-portal)

On the serp-portal website, index checks consume credits:

  • Each URL that starts a NEW crawl (not from cache, not already in flight) costs 1,000 credits.
  • Polling for results and retrying a failed crawl are free (no charge).
  • Duplicate URLs in one request are deduplicated; you pay once per unique URL.
The public API (serp-api.hoangha.shop) has its own quota system. See Usage & quotas.

Implementation example

poll until indexed or timeout
async function checkIndexStatus(urls, apiKey) {
  const maxRetries = 30;
  const delayMs = 2000;

  // Start checks
  const response = await fetch('https://serp-api.hoangha.shop/index/v1/check', {
    method: 'POST',
    headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
    body: JSON.stringify({ urls })
  });
  const results = await response.json();
  console.log('Submitted:', results.content.filter(r => r.submitted).map(r => r.url));

  // Poll status
  for (let i = 0; i < maxRetries; i++) {
    await new Promise(resolve => setTimeout(resolve, delayMs));

    const statusRes = await fetch('https://serp-api.hoangha.shop/index/v1/status', {
      method: 'POST',
      headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({ urls })
    });
    const status = await statusRes.json();

    const indexed = status.content.filter(r => r.status === 'INDEXED');
    const notIndexed = status.content.filter(r => r.status === 'NOT_INDEXED');
    const pending = status.content.filter(r => r.status === 'PENDING');

    console.log(`Indexed: ${indexed.length}, Not indexed: ${notIndexed.length}, Pending: ${pending.length}`);

    if (pending.length === 0) {
      return status.content;
    }
  }

  throw new Error('Timeout waiting for index check to complete');
}