**TL;DR.** IndexNow is a free, standardized API that lets you instantly notify Bing, Yandex, Seznam, and Naver of URL changes. For an ecommerce site, that means a new product appears in Bing within minutes instead of days. Implement it as a queue drained by a background job, not synchronous calls.

## What IndexNow is

IndexNow is an open protocol announced by Microsoft and Yandex in 2021. Search engines that participate accept a simple HTTP POST listing URLs that have changed; they crawl those URLs on priority.

Participating engines:

- Bing
- Yandex
- Seznam (Czech)
- Naver (Korean)

Notably absent: Google. Google does not participate in IndexNow; for Google you rely on regular crawl or the Indexing API (which is limited to job postings, livestreams, and a few other content types).

## How it works

1. You generate an API key (any 32+ character random string).
2. You host the key at `https://yourdomain.com/{key}.txt` — the body contains just the key. This verifies you control the domain.
3. You POST to `https://api.indexnow.org/indexnow` (or any participating engine's IndexNow endpoint) with:

```json
{
  "host": "yourdomain.com",
  "key": "your-api-key",
  "keyLocation": "https://yourdomain.com/your-api-key.txt",
  "urlList": [
    "https://yourdomain.com/products/leather-bag",
    "https://yourdomain.com/products/wool-coat"
  ]
}
```

4. The engine queues the URLs for crawl.

That's it. No authentication beyond the key-file ownership check, no rate-limit token system.

## When to ping

Ping IndexNow whenever a publicly indexable URL changes meaningfully:

| Event                                | Ping                                         |
| ------------------------------------ | -------------------------------------------- |
| Product created                       | Ping new product URL                          |
| Product updated (title, description) | Ping product URL                              |
| Product slug changed                  | Ping BOTH old and new URLs                    |
| Product unpublished or deleted        | Ping URL (engine will see 404/410 and drop)   |
| Category created                      | Ping category URL                             |
| Category updated                      | Ping category URL                             |
| Brand or page created/updated         | Ping the URL                                  |
| Sitemap regenerated                   | Optionally ping sitemap URL                   |

Don't ping for:

- Inventory updates that don't change content.
- Internal-only changes (price audit log, admin actions).
- Bulk imports — batch the post-import ping.

## Queue, don't call synchronously

A naive implementation:

```ts
// BAD: synchronous IndexNow call inside product save
async function saveProduct(product: Product) {
  await db.update(...);
  await fetch('https://api.indexnow.org/indexnow', { method: 'POST', body: ... });
  // ↑ adds 100–500ms to every save
}
```

Better:

```ts
// GOOD: enqueue, drain async
async function saveProduct(product: Product) {
  await db.update(...);
  await enqueueIndexNow(product.url);
  // returns immediately
}

// Background task, runs every 1–5 minutes
export const indexNowDrainTask = task({
  id: 'indexnow-drain',
  cron: '*/1 * * * *',
  run: async () => {
    const urls = await fetchPendingUrls(MAX_BATCH);
    if (urls.length === 0) return;
    await postToIndexNow(urls);
    await markAsDrained(urls);
  },
});
```

Benefits of queueing:

- Mutations stay fast.
- Failures don't break user-facing flows.
- Easy to batch.
- Easy to log per attempt.
- Survives platform downtime (queue persists, drains when API is back up).

## Handling slug changes

A common subtle case: a product slug changes from `/products/old` to `/products/new`. Enqueue BOTH URLs:

- The new URL needs crawling so the engine adds it to the index.
- The old URL needs crawling so the engine re-fetches it, sees the 301 (or 410), and drops it.

```ts
async function updateProductSlug(productId: string, oldSlug: string, newSlug: string) {
  await db.products.update({ id: productId, slug: newSlug });
  await writeRedirect({ from: `/products/${oldSlug}`, to: `/products/${newSlug}`, status: 301 });
  await enqueueIndexNow([`/products/${oldSlug}`, `/products/${newSlug}`]);
}
```

For deletes, write a 410 redirect (Ordiko's gone-paths table) and ping the URL. The engine sees 410 and drops the URL from its index permanently.

## Batching

The API accepts up to 10,000 URLs per POST. Common ecommerce pattern: drain 100–500 URLs per minute, depending on mutation volume.

```ts
const MAX_BATCH = 500;
const BATCH_INTERVAL_MS = 60_000;

export const indexNowDrainTask = task({
  id: 'indexnow-drain',
  cron: '*/1 * * * *',
  run: async () => {
    const urls = await fetchPendingUrls(MAX_BATCH);
    if (urls.length === 0) return;

    try {
      const res = await fetch('https://api.indexnow.org/indexnow', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          host: 'yourdomain.com',
          key: process.env.INDEXNOW_KEY,
          keyLocation: `https://yourdomain.com/${process.env.INDEXNOW_KEY}.txt`,
          urlList: urls,
        }),
      });

      if (res.status === 200 || res.status === 202) {
        await markAsDrained(urls, 'ok');
      } else if (res.status === 429) {
        await markAsDrained(urls, 'retry'); // re-queue for next drain cycle
      } else {
        await markAsDrained(urls, 'failed', `${res.status} ${await res.text()}`);
      }
    } catch (err) {
      await markAsDrained(urls, 'failed', String(err));
    }
  },
});
```

## Logging

Every ping (success or failure) should be logged. Useful audit table schema:

```sql
CREATE TABLE seo_revalidation_events (
  id SERIAL PRIMARY KEY,
  store_id UUID NOT NULL,
  url TEXT NOT NULL,
  step TEXT NOT NULL, -- 'indexnow' | 'revalidate_tag' | 'sitemap'
  status TEXT NOT NULL, -- 'ok' | 'failed' | 'retry'
  error TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
```

This lets you answer "why isn't this product showing up in Bing?" by querying the table for the product URL.

## Verifying

Bing Webmaster Tools has a dedicated IndexNow dashboard:

1. Sign in at [bing.com/webmasters](https://www.bing.com/webmasters).
2. Add your site.
3. Navigate to **IndexNow** in the sidebar.
4. View submissions over time, success/failure counts.

Yandex Webmaster has equivalent reporting under **Indexing**.

## How Ordiko implements IndexNow

- `stores.indexNowApiKey` column stores the key per store.
- Key file served at `/{key}.txt` automatically.
- Every entity service (`product.service.ts`, `category.service.ts`, etc.) calls `notifyIndexNowOnChange(url)` on mutation.
- Queue: `pending_indexnow` table with `(storeId, url, enqueuedAt)`.
- Trigger.dev cron task `indexnow-drain.task.ts` runs every minute.
- Each drain logs to `seo_revalidation_events` with `step: "indexnow", status: ok|failed|retry`.

## FAQ

**Does IndexNow work for Google?**
No. Google does not participate in IndexNow. For Google, submit your XML sitemap and rely on regular crawl cycles, or use Google's own Indexing API for limited content types (job postings, livestream events). For Bing, Yandex, Seznam, and Naver, IndexNow is the fastest way to signal updates.

**How many URLs can I submit per call?**
Up to 10,000 URLs per POST. The API returns 200-OK for accepted submissions. For larger volumes, batch and rate-limit — typical safe rate is 1–10 batches per minute. The drain job in Ordiko sends one batch per minute by default.

**What happens if I submit too aggressively?**
You get 429 Too Many Requests. The API doesn't ban you — back off, retry with exponential backoff. Sustained high-volume submission of unchanged URLs can lead to deprioritization but not blocking.

**How does Ordiko implement IndexNow?**
Every entity service (product, category, brand, page) calls notifyIndexNowOnChange on create/update/delete/unpublish. The queue table pending_indexnow holds entries; a Trigger.dev cron task indexnow-drain.task.ts drains it on a schedule. Every ping is logged in seo_revalidation_events for audit.
