Documentation Index
Fetch the complete documentation index at: https://docs.trysight.ai/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The webhook integration is the build-it-yourself path. Every time an article is ready to ship — whether it was generated manually, by Autopilot, or by an AI agent — Sight AI POSTs the full article (HTML, SEO copy, images, metadata) as a single signed JSON request to a URL you control. What happens next is up to you: write it to your CMS, push it through a Zapier/Make scenario, drop it into a database, fan it out to multiple downstream systems — anything that can accept an HTTP POST. This page is the official guide for building that endpoint. If you’re integrating with a custom Next.js site (the most common case), we walk you through the entire route handler at the bottom — copy, paste, change three lines, ship.How it works (read this first)
A 30-second mental model that prevents 90% of the bugs we see:One event type — every time
article.ready, regardless of whether it’s the first time the article has ever shipped or the hundredth re-sync. Don’t branch on the event name — branch on whether the article already exists in your system.Upsert on `article.id`, not `article.slug`
article.id is Sight AI’s stable identifier and never changes for the lifetime of an article. article.slug can change — users edit it from inside Sight AI when they want to rename a URL. If you key on slug, a rename in Sight AI will look like “delete the old article + create a new one” on your side. Always upsert by article.id.Dedupe on `event_id` (optional but recommended)
event_id. If the same event_id arrives twice (network retry, queue at-least-once, etc.), it’s the same logical event — drop the duplicate. Sight AI also dedupes server-side within a 60-minute window, so this is belt-and-braces for the rare cases that slip through.Verify the HMAC signature, then return 2xx fast
X-SightAI-Signature against the raw request body before you trust anything in the payload. Then return a 2xx within 30 seconds — even if you haven’t finished processing yet. Long synchronous handlers cause timeouts and trigger retries you don’t need.Use cases
- Custom sites built on Next.js, SvelteKit, Nuxt, Astro, Remix, etc.
- Headless CMSes we don’t natively integrate with (Payload, Sanity, Strapi, Contentful, Hygraph, Storyblok, Directus…)
- Automation tools like Zapier, Make, n8n, Pipedream
- Custom pipelines that fan content out to multiple destinations
- Internal CMSes behind your firewall (with a public-facing relay)
The endpoint contract
| Item | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Auth | HMAC-SHA256 signature in X-SightAI-Signature |
| Replay window | 5 minutes (X-SightAI-Timestamp in milliseconds) |
| Expected response | 2xx within 30 seconds |
| Retry policy | Up to 3 retries with exponential backoff on 5xx / timeout |
| Auto-pause | After 10 consecutive failed deliveries |
Headers Sight AI sends
| Header | Value | Notes |
|---|---|---|
Content-Type | application/json | Always. |
X-SightAI-Signature | sha256=<hex> | HMAC-SHA256 of the raw request body, keyed with your webhook secret. The sha256= prefix is part of the value. When signing is disabled, this is the literal string unsigned. |
X-SightAI-Timestamp | <unix_ms> | Unix timestamp in milliseconds when the request was signed. Reject anything more than 5 minutes off your clock. |
X-SightAI-Version | 1.0 | Payload schema version. |
User-Agent | SightAI-Webhook/1.0 | Helpful in your access logs. |
X-IndexPilot-Signature / X-IndexPilot-Timestamp / X-IndexPilot-Version | (legacy) | Identical values to the X-SightAI-* headers, sent for backward compatibility with older integrations. New integrations should read only the X-SightAI-* headers. |
request.headers.get('x-sightai-signature') (all lowercase). HTTP header names are case-insensitive per the spec — both forms work.The payload
Field reference
| Path | Type | Required | What it’s for |
|---|---|---|---|
event_id | string | Yes | Idempotency key. Same logical event → same event_id. Use it to dedupe retries on your side. |
event | string | Yes | Currently always "article.ready". See Event types. |
timestamp | ISO-8601 string | Yes | When the event was generated. (Replay protection uses the X-SightAI-Timestamp header, not this.) |
site.id / site.name / site.host | string | Yes | The Sight AI workspace this article belongs to. site.host is whatever the user entered in their site settings, including the protocol. |
article.id | string | Yes | The upsert key. Stable for the article’s lifetime. |
article.slug | string | Yes | URL slug — use this to construct /blog/<slug>. Can change between deliveries if a user renames the article. |
article.title | string | Yes | Display title. |
article.content | string (HTML) | Yes | Full article body, ready to render. |
article.summary | string | Optional | Short excerpt. May be null. |
article.seo_title | string | Optional | ≤60 chars recommended. May be null. |
article.seo_meta_description | string | Optional | ≤160 chars recommended. May be null. |
article.target_keyword | string | Optional | The primary keyword the article targets. |
article.main_image_url | string (URL) | Optional | Featured image. May be null. See Image handling. |
article.thumbnail_image_url | string (URL) | Optional | Smaller version of the featured image. |
article.article_type | string | Yes | e.g. "explainer", "listicle", "how-to". |
article.category | string | Optional | The category name assigned in Sight AI (not an ID). Use the Filters tab in Sight AI to set an external_id for mapping. May be null. |
article.author_name | string | Optional | Display name. |
article.read_time_minutes | number | Optional | Estimated read time. |
article.is_featured | boolean | Yes | Whether the user marked it as a featured article. |
article.published_at | ISO-8601 string | null | Optional | When the article was published in Sight AI. null if unpublished. |
article.created_at | ISO-8601 string | Yes | When the article was first created in Sight AI. |
article.updated_at | ISO-8601 string | Yes | When the article was last edited in Sight AI. Useful for “is this newer than what I have?” checks. |
Event types
event is currently always "article.ready". Every sync — first publish, manual re-send, autopilot delivery, agent-driven update — uses this same event name. The receiver decides whether it’s a create or an update by looking at whether article.id already exists in your system.
article.updated and article.published are reserved for future use and are not emitted by Sight AI today. If you want to be forward-compatible, treat any unknown event name as “ignore, return 200” so future event types don’t break your endpoint.
What to do when a webhook arrives
Your handler should do these six things, roughly in this order:Read the raw request body
await request.text() (not request.json()).Verify the signature
HMAC-SHA256(secret, raw_body) and compare it (timing-safe) to the value of X-SightAI-Signature after stripping the sha256= prefix. If it doesn’t match, return 401 and stop.Verify the timestamp
|Date.now() - X-SightAI-Timestamp| > 5 minutes. This prevents an attacker from replaying a captured request days later. Return 401 on failure.Parse the JSON
article.id, article.slug, article.title, and article.content exist — return 400 if not.Upsert by `article.id`
article.id. If found → update it in place. If not found → create a new record. Don’t branch on event — article.ready covers both cases.Acknowledge with 2xx, then do the heavy lifting async
Verifying the signature
The HMAC signature is the only thing standing between your endpoint and an attacker who knows the URL. Get this right.Image handling
Sight AI sendsmain_image_url and thumbnail_image_url as URLs hosted on our CDN. You have two reasonable strategies:
Strategy 1 — reference the URL directly (simplest)
Just store the URL string and render it as<img src="{main_image_url}">. No download, no copy, no media library bookkeeping. Recommended for most custom sites unless you have a specific reason to host the image yourself.
Strategy 2 — download and host the image yourself
Common when you’re feeding a CMS that has a Media collection (Payload, Sanity, etc.) and wants every asset stored locally. The trick is don’t re-download on every webhook — Sight AI re-sends the article (with the same image URL) on every edit, every autopilot run, every manual re-sync. If you naively re-download each time, you’ll pile up duplicates. A safe rule of thumb:- First time you see this
article.id→ download and store the image, save a reference on the article record. - Article exists but has no stored image yet → download.
- Article already has a stored image → leave it alone, even if
main_image_urllooks slightly different. CDN cache-buster query params drift cosmetically without the underlying asset changing. - User wants to swap the image → expose a manual “clear stored image” admin action, then trigger a re-send from Sight AI. The next webhook will see “no stored image” and download fresh.
Cache invalidation
If your site uses ISR, SSG, edge caching, or any kind of build-time rendering, the live page won’t reflect the new content until the cache expires. Bust the cache for the affected paths inside your handler so users see the update immediately. For Next.js App Router that means callingrevalidatePath for every page that displays this article — the article page, the index/listing, and any topic/category page it appears on. Other frameworks have equivalents (unstable_revalidate for SvelteKit, purge for Astro, etc.).
Worked example: Next.js App Router
A complete, copy-paste-ready route handler for a Next.js site. Drop this atapp/api/webhooks/sight-ai/route.ts, set SIGHT_AI_WEBHOOK_SECRET in your environment, and you’re done.
- ✅ Verifies the HMAC against the raw body
- ✅ Rejects timestamps older than 5 minutes
- ✅ Upserts by
article.id(so slug renames don’t create duplicates) - ✅ Dedupes by
event_id(so retries are no-ops) - ✅ Revalidates the article page, the listing, and the old slug if the URL changed
- ✅ Falls back gracefully if revalidation throws
Setting it up
Create the route file
app/api/webhooks/sight-ai/route.ts in your Next.js project.Set the webhook secret
SIGHT_AI_WEBHOOK_SECRET to your hosting environment (Vercel → Project → Settings → Environment Variables, or your platform’s equivalent). The value must match what you configure in Sight AI’s webhook settings.Deploy
curl https://yourdomain.com/api/webhooks/sight-ai should return {"status":"ok","endpoint":"sight-ai"}.Configure Sight AI
https://yourdomain.com/api/webhooks/sight-ai, save, and click Test connection. You should see a green “Connection successful”.Configuring the webhook in Sight AI
Webhook URL
The endpoint Sight AI will POST to. Must be HTTPS in production (HTTP is allowed forlocalhost during development). Allowed ports: 80, 443, 3000, 8080, 8443.
Webhook secret
You have two choices:- Managed by Sight AI (recommended) — Sight AI generates a strong random secret for you. You see the value once, after the first save. Copy it into your endpoint’s environment, then save again to confirm. Rotate anytime from the Connect tab.
- Bring your own — paste a secret you generated. Stored AES-256 encrypted at rest. Useful if you have central secret management.
Sign requests
On by default. When on, every delivery includes theX-SightAI-Signature HMAC. Leave this on in production unless you have a very specific reason — your endpoint URL alone is not authentication.
Verify SSL
On by default. When on, Sight AI rejects endpoints with invalid TLS certificates. Disable only for local development against self-signed certs.Trigger mode
Under Advanced settings:- Manual (default) — webhooks fire only when a user clicks Send Webhook on an article, or runs a bulk sync, or it’s triggered by Autopilot / an AI agent’s planner queue.
- Automatic — webhooks fire as soon as article generation completes, with no human in the loop.
Max retries / Request timeout
Under Advanced settings:| Setting | Default | Range |
|---|---|---|
| Max retries | 3 | 0 – 10 |
| Request timeout | 30 seconds | 5 – 120 seconds |
Filters (categories)
Optional. The Filters tab lets you define category names that appear as options when generating an article. Each category can have anexternal_id you set, which is not sent in the payload today — but article.category (the category name the user picked) is, so you can map names to your downstream IDs in your handler.
Testing your endpoint
From inside Sight AI
The Test connection button on the Connect tab fires a real signed request to your URL with theevent set to article.ready and the additional field "test": true in the payload. The article fields contain example data — your endpoint should accept it and return 2xx for the test to pass.
To distinguish test events from real ones in your handler, check for payload.test === true or for event_id starting with test_. We recommend short-circuiting test events:
From the command line
Response handling
Your endpoint should respond with:| Status | Meaning | Sight AI behavior |
|---|---|---|
2xx | Delivery accepted | Marked as delivered. No retry. |
4xx | Permanent failure (bad payload, bad signature, etc.) | Not retried — the request itself is wrong, retrying won’t help. |
5xx | Transient failure | Retried up to 3 times with exponential backoff. |
| Timeout (>30s) | Treated as transient | Retried. |
Delivery logs and monitoring
Inside Sight AI, the Monitoring tab on the webhook integration page shows:- Total deliveries / success rate / failure count
- The most recent 20 deliveries with HTTP status, timing, and error messages
- A “Refresh” button that hits the same data live
401 Invalid signature— see Troubleshooting500 Internal Server Error— your handler threw; check your server logsRequest timeout— your handler took longer than the configured timeout
Active CMS
Each Sight AI workspace can have one active CMS at a time. When you click Set as Active CMS on the webhook page, every article that ships from Sight AI for that workspace — manual, Autopilot, or AI agent — flows through your webhook instead of any previously connected CMS (WordPress, Webflow, etc.). Switching to a different CMS later is a one-click change; your webhook configuration is preserved.Disconnecting
To stop sending webhooks for a workspace:- Go to Integrations → Webhook for that workspace.
- Click Disconnect.
Troubleshooting
401 Invalid signature
401 Invalid signature
- You hashed the wrong bytes. Body parsers (Express
body-parser, Hono’sc.req.json(), etc.) consume the raw body before you see it. Capture the raw body first, hash that, then parse. - You re-stringified the JSON.
JSON.stringify(JSON.parse(body))does not produce the same bytes — key order and whitespace can differ. Always hash the original string. - Wrong secret. Confirm the secret in your environment matches what’s in Sight AI exactly. No whitespace, no quotes, no
Bearerprefix. - Missing
sha256=prefix. The header value issha256=<hex>. Don’t strip the prefix before storing it; do strip it before comparing. - Hex case mismatch. We send lowercase hex. Use a buffer comparison, not a string
===.
401 Timestamp too old or in future
401 Timestamp too old or in future
X-SightAI-Timestamp is a Unix timestamp in milliseconds (not seconds). It must be within 5 minutes of your server’s clock.- If your code is reading it as seconds, multiply by 1000 (or vice versa).
- If your server clock is drifting, enable NTP (
timedatectl statuson Linux). - If your queue or proxy can buffer requests longer than 5 minutes, that’s a problem — by the time the request lands the timestamp is already stale. Sight AI’s retry will re-sign with a fresh timestamp on the next attempt, so this usually self-corrects.
503 Webhook not configured
503 Webhook not configured
SIGHT_AI_WEBHOOK_SECRET (or SIGHTAI_WEBHOOK_SECRET) environment variable isn’t set. Add it to your hosting environment and redeploy.400 Missing required article fields
400 Missing required article fields
article.id, article.slug, article.title, and article.content. Everything else can be null or absent — make sure your validator accepts that.Same article keeps creating new records on my side
Same article keeps creating new records on my side
article.slug instead of article.id. When a user renames an article inside Sight AI, the slug changes but the id doesn’t — so a slug-keyed receiver looks like “delete + re-create” while an id-keyed receiver correctly updates in place. Migrate your lookup to article.id (store it as a column on your articles table) and the duplicates stop.Live page doesn't update even though the webhook succeeded
Live page doesn't update even though the webhook succeeded
- Did you call
revalidatePath(or your framework’s equivalent)? ISR/SSG will eventually pick up the change on its next regeneration window, but until then the stale page sticks. - Did the field you care about actually change? A re-send of an unedited article is intentionally a no-op visually — the database write happens, but the rendered HTML is identical.
Image collection is filling up with duplicates
Image collection is filling up with duplicates
main_image_url on every webhook. Sight AI re-sends the same article — with the same image URL — on every edit, every Autopilot run, every manual sync. Add a guard: only download when the article is brand new on your side, or when the local image record is missing. See Image handling.Endpoint shows as auto-paused in Sight AI
Endpoint shows as auto-paused in Sight AI
Best practices
- Be idempotent. Use
event_idas a dedupe key. Retries and at-least-once delivery should produce the same end state, not duplicate writes. - Upsert by
article.id. The single most important rule. Never key onslug. - Verify the HMAC on the raw body. Don’t parse, don’t re-stringify, don’t trim.
- Acknowledge fast, work async. A 200 within seconds is the goal; long synchronous handlers cause needless retries.
- Use HTTPS in production. Required for non-localhost endpoints.
- Store secrets safely. Treat the webhook secret like an API key — secrets manager or hosting env vars only, never in source control.
- Bust your cache. If your site is statically rendered or behind ISR, revalidate the affected paths inside your handler.
- Log generously while you’re integrating. Once you’re confident, dial logs back to errors only —
article.contentcan be large, and you don’t need it in every log line.
Example integrations
Zapier (Webhooks by Zapier)
- New Zap → trigger: Webhooks by Zapier → Catch Hook
- Copy the unique webhook URL Zapier generates.
- Paste it into Sight AI under Integrations → Webhook → Webhook URL.
- (Optional) Skip request signing in Sight AI — Zapier doesn’t natively verify HMACs. Treat the URL itself as the secret.
- Add Zap actions (Create Notion page, append to Google Sheets, post to Slack, etc.).
Make (Integromat)
- New scenario → Webhooks → Custom webhook.
- Copy the URL Make assigns.
- Configure in Sight AI as above.
- Build downstream modules.
n8n / Pipedream / others
Same pattern: create a webhook trigger node, paste the URL into Sight AI, build the rest of the workflow.Reference
| Concern | Answer |
|---|---|
| Endpoint method | POST |
| Auth | X-SightAI-Signature: sha256=<hmac_hex> |
| Replay window | 5 minutes (X-SightAI-Timestamp in ms) |
| Upsert key | article.id |
| Dedupe key | event_id |
| Image dedup | Don’t re-download once stored — gate on “is the image already saved?” |
| Cache busting | revalidatePath (Next.js) or framework equivalent |
| Test events | payload.test === true and/or event_id starts with test_ |
| Retries | Up to 3 with exponential backoff, on 5xx / timeout only |
| Auto-pause | After 10 consecutive failures |