IndexNOW notify (integrator endpoint)

Обновлено: Aug 3, 2026Раздел: General

IndexNOW notify (integrator endpoint)

When Blog or Market content is published, updated, unpublished, or deleted, Mailoo can POST identifiers to your site. You build the public page URLs and submit them to IndexNow. Mailoo does not call IndexNOW directly and does not send absolute URLs.

Publish and catalog writes in Mailoo always succeed even if your endpoint is missing or returns an error (fire-and-forget). Mailoo records the outcome on the content row as indexNowStatus.

Related: Blog CMS --- blog-headless-cms{.interpreted-text role="doc"}; Market catalog --- market-catalog-external-api{.interpreted-text role="doc"}; Next.js blog example --- blog-nextjs-example{.interpreted-text role="doc"}; delivery webhook HMAC (same signing formula) --- transactional-email{.interpreted-text role="doc"}.

Architecture

  • Trigger: After a successful Blog article or Market product (or published-product variant) write.
  • Payload: Project/integration/content ids, optional blog slug, locales, and action --- no page URLs.
  • Your job: Map ids → public URLs → IndexNOW API with your key.
  • Status on content: PENDINGNOTIFIED | FAILED, or SKIPPED when indexNow is not configured. null means never attempted (e.g. draft-only).

Configuration

Per BLOG or MARKET integration:

  • Dashboard → Connection & settings → IndexNOW notify
  • Or PATCH /api/v1/projects/{uid}/integrations/{id} with body:
{
  "indexNow": {
    "notifyUrl": "https://www.example.com/api/indexnow/notify",
    "secret": "your-signing-secret"
  }
}

Send "indexNow": null to clear. Sanitized API responses expose notifyUrl and hasSecret (never the raw secret).

If notifyUrl / secret are unset, Mailoo sets indexNowStatus to ``SKIPPED`` and does not HTTP-call your site.

Endpoint contract

Method / URL: POST to the configured notifyUrl.

Headers:

  • Content-Type: application/json
  • X-Mailoo-Event: indexnow.notify
  • X-Mailoo-Signature: t={unixSeconds},v1={hex} --- v1 is HMAC-SHA256 of "{timestamp}.{rawBody}" using indexNow.secret (same formula as transactional delivery webhooks).

Body (JSON):

{
  "event": "indexnow.notify",
  "kind": "blog_post",
  "action": "published",
  "projectUid": "…",
  "integrationId": "…",
  "contentId": "…",
  "slug": "my-post",
  "locales": ["en", "de"],
  "timestamp": "2026-08-03T09:00:00.000Z"
}

Fields by kind

  • kind --- blog_post or market_product
  • contentId --- article id or product id
  • slug --- article slug (blog only; omitted for market)
  • locales --- blog: en plus keys from article locales JSON; market: defaultCatalogLocale or en
  • action --- published, updated, unpublished, or deleted

When Mailoo fires

  • Create with PUBLISHEDpublished
  • Update that becomes or stays PUBLISHEDpublished / updated
  • Status leaves PUBLISHEDunpublished
  • Delete while published → deleted (no status row update afterward)
  • Draft-only writes → no notify; indexNowStatus stays null

Expected responses

  • 2xx --- accepted (you may queue IndexNOW asynchronously). Mailoo sets NOTIFIED.
  • 404 / 5xx / network / timeout --- Mailoo sets FAILED. Content publish already committed.
  • Missing config --- SKIPPED without a request.

indexNowStatus values

  • PENDING --- notify in flight
  • NOTIFIED --- integrator returned 2xx
  • SKIPPED --- no indexNow config on the integration
  • FAILED --- request failed or non-2xx
  • null --- not applicable / never attempted

Integrator responsibilities

  1. Verify X-Mailoo-Signature (reject invalid signatures).
  2. Resolve public URL(s) from kind, contentId / slug, and locales.
  3. Call IndexNOW with your key (and host key file as required by IndexNOW).
  4. Return 2xx quickly; do heavy work asynchronously if needed.

Next.js App Router sketch

// app/api/indexnow/notify/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto'
import { NextRequest, NextResponse } from 'next/server'

const SECRET = process.env.MAILOO_INDEXNOW_SECRET!

function verify(signatureHeader: string | null, body: string): boolean {
  if (!signatureHeader) return false
  const m = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(signatureHeader.trim())
  if (!m) return false
  const [, t, v1] = m
  const expected = createHmac('sha256', SECRET)
    .update(`${t}.${body}`)
    .digest('hex')
  try {
    return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
  } catch {
    return false
  }
}

export async function POST(req: NextRequest) {
  const raw = await req.text()
  if (!verify(req.headers.get('x-mailoo-signature'), raw)) {
    return NextResponse.json({ error: 'invalid signature' }, { status: 401 })
  }
  const payload = JSON.parse(raw) as {
    kind: string
    action: string
    contentId: string
    slug?: string
    locales: string[]
  }
  // Build URLs from payload, then POST to https://api.indexnow.org/indexnow
  void payload
  return NextResponse.json({ ok: true })
}

Prerequisites

  • BLOG or MARKET integration
  • indexNow.notifyUrl + secret configured (optional; without it status is SKIPPED)
  • Your site implements the notify endpoint and IndexNOW key hosting