IndexNow
Tell Bing, Yandex, Seznam, Naver and other IndexNow search engines about new, changed or deleted URLs the moment they change.
/v1/indexnow1 credit (refunded on failure)API keyIndexNow is an open protocol for notifying search engines that a URL changed. One submission from RankParse goes to api.indexnow.org, which shares it with every participating engine (Bing, Yandex, Seznam, Naver and others). RankParse submits to IndexNow only; it does not submit to any other search engine.
Two routes make up the flow: a free one that gives you a key for a host, and a 1 credit one that submits URLs.
How it works
Add the website
The host you submit must be one of your RankParse websites, or a subdomain of one. Add it in the dashboard under Sites with Add website. Submissions for any other host fail with indexnow_host_not_a_website.
Generate a key
Call POST /v1/indexnow/key?domain=example.com. You get back a key and the exact URL where it must be hosted. The call is free and returns the same key every time for the same host.
Host the key file
Publish a UTF-8 text file containing exactly the key at the root of the site, at the key_location from the previous step: https://example.com/<key>.txt. This proves to search engines that you control the host.
Submit URLs
Call POST /v1/indexnow with up to 1000 URLs from that host. On the first submission (and then at most once a day) RankParse fetches the key file to check it before sending.
POST /v1/indexnow/key
/v1/indexnow/keyFreeAPI keyReturns the IndexNow key for a host, creating it on first use. The key is unique to your workspace and host, and it is never rotated, so a key file you have already published stays valid.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
domain | query string | Yes | The host to get a key for, for example example.com or www.example.com. A full URL is accepted and reduced to its hostname. Hosts are lowercased |
example.com and www.example.com are different hosts and get different keys. Provision the key for the exact host that appears in the URLs you will submit.
Example
curl -X POST "https://api.rankparse.com/v1/indexnow/key?domain=example.com" \
-H "X-API-Key: rp_your_key"import requests
res = requests.post(
"https://api.rankparse.com/v1/indexnow/key",
headers={"X-API-Key": "rp_your_key"},
params={"domain": "example.com"},
)
info = res.json()
print(res.status_code, info["key_location"])const res = await fetch("https://api.rankparse.com/v1/indexnow/key?domain=example.com", {
method: "POST",
headers: { "X-API-Key": "rp_your_key" },
});
const info = await res.json();
console.log(res.status, info.key_location);Response
201 Created the first time, 200 OK on every later call for the same host. The body is a plain object, not wrapped in the usual data envelope.
{
"host": "example.com",
"key": "3f9a1c7e2b8d4e6f9a0b1c2d3e4f5a6b",
"key_location": "https://example.com/3f9a1c7e2b8d4e6f9a0b1c2d3e4f5a6b.txt",
"verified": false,
"instructions": "Host a UTF-8 text file containing exactly the key at https://example.com/3f9a1c7e2b8d4e6f9a0b1c2d3e4f5a6b.txt, then submit URLs with POST /v1/indexnow."
}Response fields
| Field | Type | Description |
|---|---|---|
host | string | The normalized host |
key | string | The key, 32 lowercase hexadecimal characters. Treat it as a per-host value; only you can retrieve it |
key_location | string | The URL where the key file must be hosted: https://<host>/<key>.txt |
verified | boolean | true when RankParse has confirmed the hosted key file within the last 24 hours |
instructions | string | A one-line summary of the hosting step |
The key file contents are the key and nothing else. Leading and trailing whitespace, such as a final newline, is ignored when RankParse checks it.
POST /v1/indexnow
/v1/indexnow1 credit (refunded on failure)API keySubmits a batch of URLs from one host to IndexNow.
Body parameters
Send JSON with Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | Yes | 1 to 1000 absolute public http or https URLs. All must have the same host. No explicit ports |
Rules for urls:
- Every URL must be a valid public
http(s)URL. Private or reserved addresses, URLs with credentials (user:pass@), URLs with a#fragment, and URLs with leading or trailing whitespace are rejected. - Every URL must use the default port.
https://example.com:8443/ais rejected because IndexNow keys are verified at the default port. - Every URL must have exactly the same hostname.
https://example.com/aandhttps://www.example.com/bin one call fail withindexnow_host_mismatch. Make one call per host. - URLs are normalized (for example the host is lowercased) before they are sent.
- Submit URLs that are new, changed or deleted. IndexNow is a change signal, not a way to re-submit unchanged pages.
Example
curl -X POST "https://api.rankparse.com/v1/indexnow" \
-H "X-API-Key: rp_your_key" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com/blog/new-post",
"https://example.com/pricing"
]
}'import requests
res = requests.post(
"https://api.rankparse.com/v1/indexnow",
headers={"X-API-Key": "rp_your_key"},
json={
"urls": [
"https://example.com/blog/new-post",
"https://example.com/pricing",
]
},
)
print(res.status_code, res.json())const res = await fetch("https://api.rankparse.com/v1/indexnow", {
method: "POST",
headers: {
"X-API-Key": "rp_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
urls: [
"https://example.com/blog/new-post",
"https://example.com/pricing",
],
}),
});
console.log(res.status, await res.json());Response
{
"data": {
"host": "example.com",
"url_count": 2,
"key_location": "https://example.com/3f9a1c7e2b8d4e6f9a0b1c2d3e4f5a6b.txt",
"status": "accepted"
},
"credits_used": 1,
"credits_remaining": 998
}Response fields
| Field | Type | Description |
|---|---|---|
data.host | string | The host that was submitted |
data.url_count | integer | Number of URLs sent |
data.key_location | string | Where the key file was verified |
data.status | string | accepted when IndexNow accepted the submission (HTTP 200 or 202) |
credits_used | integer | Always 1 on success |
credits_remaining | integer | Your balance after the call |
accepted means IndexNow accepted the notification. It does not mean any engine has crawled or indexed the pages yet.
Behavior and limits
- Cost: 1 credit per call, whatever the number of URLs (up to 1000). Any failed call is refunded automatically, including validation errors and upstream failures. See Credits.
- Order of checks: body and URL validation, then website ownership, then key lookup, then key file verification, then the upstream submission.
- Key file verification: RankParse requests
https://<host>/<key>.txtwith a 30 second timeout and requires an HTTP 200 whose body (trimmed) equals the key. The file must be small; anything over 4 KB is treated as not a key file. A successful check is cached for 24 hours, so steady-state submissions skip it. If IndexNow later rejects the key (indexnow_key_rejected), the cache is cleared and the next submission checks the file again. - Subdomains: a host that is a subdomain of one of your websites is accepted (
blog.example.comfor a websiteexample.com). Generate the key for the subdomain host itself. - Rate limit: the shared 60 requests per minute per key. IndexNow applies its own limits upstream and can answer with
indexnow_rate_limited; submit changes in batches instead of one URL per call.
Approval inbox for agents
If your workspace uses the approval inbox, a call made by an AI agent does not submit immediately. A call is agent-origin when it uses an API key and carries the header X-RankParse-Client: mcp, which is what the RankParse API-key MCP server sends for submit_indexnow_urls. Calls from your own code without that header, and RapidAPI calls, are never held for approval.
For an agent-origin call in such a workspace:
| Situation | Response |
|---|---|
| The website's agent policy does not allow automatic submission | 202 with data.status set to awaiting_approval, plus data.proposal_id, data.review_url and a message. credits_used is 0 and nothing is submitted until you approve it in RankParse |
| You set the policy for this website to automatic | The submission runs now and returns the normal 200 accepted response with an added data.proposal_id. The 1 credit is charged once |
| An automatic run is queued for the next hourly retry, or the same proposal was already approved | 202 with data.status set to approved and a message. It is not proposed again |
| An automatic run fails | The error status and code from the table below, with proposal_id added to the body. The credit is refunded |
{
"data": {
"host": "example.com",
"url_count": 2,
"status": "awaiting_approval",
"proposal_id": "prop_abc123",
"review_url": "https://rankparse.com/dashboard/sites/site_abc123/today?proposal=prop_abc123"
},
"credits_used": 0,
"credits_remaining": 998,
"message": "Waiting for the user's approval in RankParse: ..."
}Show the review_url to the person who must approve. Track the outcome with proposals. Agents that should always go through the inbox can call propose_indexnow_submit (below) instead.
Errors
All errors use the standard shape { "error", "code", "message" }. See Error codes. Failed submissions are refunded.
| Status | Code | Meaning and fix |
|---|---|---|
400 | invalid_domain | POST /v1/indexnow/key needs a valid domain |
400 | indexnow_invalid_url | Body is not JSON, urls is missing or empty, an entry is not a string or not a valid public http(s) URL, or a URL has an explicit port |
400 | indexnow_too_many_urls | More than 1000 URLs in one call |
400 | indexnow_host_mismatch | The URLs do not all share one host. Make one call per host |
400 | indexnow_host_not_a_website | The host is not one of your RankParse websites (or a subdomain of one). Add it under Dashboard, Sites |
400 | indexnow_key_not_found | No key exists for this host. Call POST /v1/indexnow/key first |
400 | indexnow_key_not_verified | The key file at key_location is missing, not HTTP 200, or does not contain exactly the key. Host it, then retry |
400 | indexnow_invalid_request | IndexNow rejected the submission as malformed |
401 | invalid_api_key | Missing, invalid or revoked API key |
402 | insufficient_credits | Not enough credits. Nothing was charged |
402 | spend_cap_reached | Your monthly spend cap was reached. Nothing was charged |
403 | capability_missing | Your workspace role cannot make this call |
403 | indexnow_key_rejected | IndexNow rejected the key. Confirm the key file is still hosted at its location, then retry |
422 | indexnow_url_mismatch | IndexNow rejected the URLs: they do not belong to the submitted host, or the key does not match |
429 | rate_limited | More than 60 requests per minute for this key |
429 | indexnow_rate_limited | IndexNow's rate limit was reached. Slow down and retry later |
503 | indexnow_unavailable | IndexNow is unreachable or returned an unexpected status. Retry later |
MCP tools
| Tool | Endpoints | Purpose |
|---|---|---|
provision_indexnow_key | API-key MCP | Get or create the key for a host (0 credits). Tells the agent to ask the user to host the file |
submit_indexnow_urls | API-key MCP | Submit up to 1000 URLs on one host (1 credit, refunded on failure). In an approval-inbox workspace it returns a pending proposal instead |
propose_indexnow_submit | API-key MCP and secure OAuth connector | Ask the user to approve a submission. Always goes through the inbox. 0 credits to propose, 1 credit only when it runs |
provision_indexnow_key and submit_indexnow_urls are not available on the secure OAuth connector, which can only propose. See MCP Server.