# IndexNow Publish Submitter and Receipt Ledger (`kingii98/indexnow-publish-submitter`) Actor

Submits published URLs to IndexNow endpoints (Bing, Yandex) with a preflight check and a receipt ledger. HTTP only, no login, no paid API.

- **URL**: https://apify.com/kingii98/indexnow-publish-submitter.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 url submitteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## IndexNow Publish Submitter and Receipt Ledger

This Actor tells Bing and Yandex about new or changed pages on your site,
through the [IndexNow protocol](https://www.indexnow.org/documentation).
It checks each page first, submits the ones that qualify, and writes a
receipt for every URL and every endpoint.

It is HTTP only. It does not use a browser, a proxy, an account, or a
paid API.

### Before you run it

You must host an IndexNow key file on your own domain, at:

```
https://<host>/<indexNowKey>.txt
```

The file body must be exactly the key, with no extra text. The Actor
checks this file first. If the file is missing or does not match, the
run stops and reports the problem; it does not submit any URLs.

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `host` | string | — | The domain that owns the URLs. Must be a public host, not `localhost` or a private address. |
| `urls` | string\[] | — | The pages to submit. 1 to 10000 entries. Every URL must be on `host`. |
| `indexNowKey` | string | — | The key you host at `https://<host>/<indexNowKey>.txt`. 8-128 letters, digits, or hyphens. |
| `endpoints` | string\[] | `api.indexnow.org`, `www.bing.com/indexnow`, `yandex.com/indexnow` | The IndexNow endpoints to submit to. Up to 10. |
| `preflight` | boolean | `true` | Check each URL before submission: HTTP status, `robots.txt`, meta robots, and canonical tag. |
| `recheckAfterHours` | number | `0` | Reserved for a later scheduled run. The Actor itself does not wait or schedule; use `previousReceiptDatasetId` on a later run to act on it. |
| `previousReceiptDatasetId` | string | — | Optional. When set, the Actor runs in recheck mode: it re-reads the URLs from that dataset and reports whether they are still indexable. It does not submit new URLs in this mode. |

### Output

The run dataset holds these record types:

- `keyFileCheck`: one record, the result of checking the key file.
- `endpointHandshake`: one record for each IndexNow endpoint the Actor called.
- `urlReceipt`: one record for each URL, with its `preflight` result, its
  `eligibility` (`ELIGIBLE`, `BLOCKED_NOINDEX`, `BLOCKED_ROBOTS`,
  `BLOCKED_CANONICAL_ELSEWHERE`, or `BLOCKED_NOT_200`), and the endpoints it
  was submitted to.
- `summary`: one record with counts of submitted, blocked, and
  endpoint-rejected URLs.
- `recheck`: in recheck mode, one record for each re-checked URL, with
  `stillIndexable` and `changedSinceSubmission`.

### What "verify" means here

This Actor cannot prove that Google, Bing, or Yandex indexed a page; that
needs a paid, account-based API. The preflight and recheck steps check
only what a plain HTTP request can check: the page returns 200, it is
not marked no-index, `robots.txt` allows it, and its canonical tag (if
any) points back at itself. Endpoint acceptance means the endpoint
returned a success status for the batch; it does not mean the URL is
indexed.

### Guardrails

- Rejects any URL, endpoint, or host that is not a public HTTP(S)
  target: no `localhost`, no private or reserved IP address (checked by
  literal address and by DNS resolution), no other scheme.
  See `src/indexnow_submitter/urlsafety.py`.
- Every input list is bounded: up to 10000 URLs, up to 10 endpoints.
- Every HTTP GET is bounded: a timeout, a redirect limit with a safety
  check on each hop, and a response-size limit.
- Preflight and recheck run with bounded concurrency (10 requests at a
  time).
- `robots.txt` is fetched once per host and reused for the whole run.

### Billing

Pay-per-event, charged from the run itself:

- `url-submitted` — one event for each URL actually sent to the
  IndexNow endpoints (blocked and rejected URLs are not charged).
- `url-rechecked` — one event for each URL a later verification run
  re-reads.
- `host-key-verified` — one event per run once the key file check
  passes. A run that stops at a failed key file check charges nothing.

### Default fixture

`.actor/default_input.json` points at `example.com`, the IANA reserved
example domain, which is always public and always available. It does
not host an IndexNow key file, so a run against it demonstrates the
key-file guardrail: the run succeeds without submitting anything and
writes a `keyFileCheck` record plus a zero-count `summary` with status
`KEY_FILE_NOT_VERIFIED`. To see a full submission, run with your own
`host`, `urls`, and `indexNowKey`.

The input schema gives `host`, `urls`, and `indexNowKey` the same values as
defaults. A run started with an empty input therefore takes these defaults
and succeeds, instead of a rejection for a missing required field.

### Development

```sh
uv sync --dev
uv run pytest
uv run ruff check .
```

`src/indexnow_submitter/runner.py` holds the pure run logic (`run_submit`,
`run_recheck`), independent of the Apify SDK, so it runs under `pytest`
without a platform connection. `main.py` is the thin Actor entrypoint
that wires Apify input/output to that logic.

# Actor input Schema

## `host` (type: `string`):

The domain that owns the URLs, for example example.com. This domain must already serve the IndexNow key file. The default is the demo domain example.com, which submits nothing until you host the key file.

## `urls` (type: `array`):

The pages to submit. Every URL must be on the host above. Maximum 10000 URLs in one run. The default is a single demo URL on example.com.

## `indexNowKey` (type: `string`):

The key you already host at https://<host>/<key>.txt. 8 to 128 letters, digits, or hyphens. The default is a demo key that no domain hosts.

## `endpoints` (type: `array`):

The IndexNow endpoints to submit to. Each entry is a host, or a host plus path.

## `preflight` (type: `boolean`):

Check each URL before submission: HTTP status, robots.txt, meta robots, and canonical tag.

## `recheckAfterHours` (type: `integer`):

Kept for a later scheduled run. Not used when previousReceiptDatasetId is empty.

## `previousReceiptDatasetId` (type: `string`):

Optional. Set this to the dataset ID of a prior run to re-check its URLs instead of submitting new ones.

## Actor input object example

```json
{
  "host": "example.com",
  "urls": [
    "https://example.com/"
  ],
  "indexNowKey": "example0000000key0",
  "endpoints": [
    "api.indexnow.org",
    "www.bing.com/indexnow",
    "yandex.com/indexnow"
  ],
  "preflight": true,
  "recheckAfterHours": 0
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "host": "example.com",
    "urls": [
        "https://example.com/"
    ],
    "indexNowKey": "example0000000key0"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/indexnow-publish-submitter").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "host": "example.com",
    "urls": ["https://example.com/"],
    "indexNowKey": "example0000000key0",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/indexnow-publish-submitter").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "host": "example.com",
  "urls": [
    "https://example.com/"
  ],
  "indexNowKey": "example0000000key0"
}' |
apify call kingii98/indexnow-publish-submitter --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/indexnow-publish-submitter"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/PCpJf37b5cGMfQPvN/builds/lboQjxYg2cYexdWfv/openapi.json
