Nuwtonic AI SEO Agent Logo
Nuwtonic

Integrations

Connect Zapier, n8n, Make, or a custom webhook to publish articles and apply content and SEO fixes.

How to connect Zapier, n8n, Make, or any webhook

Nuwtonic pushes published articles and supported events to a Zapier, n8n, Make, or custom HTTPS webhook URL that you provide. Nuwtonic does not charge for webhook delivery. If you use an automation provider, you pay that provider.

Use this when you want to send articles or optimization instructions to WordPress, Slack, Sheets, a CRM, a custom CMS, or another system.

Important: a webhook delivers instructions; it does not update your CMS by itself. Your Zap, n8n workflow, Make scenario, or custom receiver must find the target page and perform the requested update. If you want Nuwtonic to update WordPress without building that workflow, use the native WordPress integration.

For complete node-by-node examples for article publishing and content fixes, open Zapier, n8n, Make, and webhook examples.

1. Choose what the workflow will do

One webhook URL receives all Nuwtonic events. Route each request using the top-level event field:

  • Publish an article: process content.generated only when data.result contains title, content, or content_html.
  • Apply content and SEO fixes: process only fix.apply. Find the existing page using data.target_url, then apply the instructions in data.fixes.
  • Ignore or log publish completion: a content.generated payload containing data.result.published_count is a summary, not another article.
  • Ignore or log fix completion: fix.generated reports the final job status. It does not contain new fixes to apply.
  • Acknowledge tests: test only confirms that Nuwtonic can reach the URL.

Do not send every event to the same CMS action. That can create duplicate posts or write a completion payload into an article.

2. Paste your URL in Nuwtonic

  1. Open the workspace you want to connect.
  2. Open Integrations.
  3. Find Zapier, n8n, Make, or any webhook and select Connect.
  4. Paste the HTTPS URL from Zapier, n8n, Make, or your custom webhook endpoint.
  5. Turn Enable webhook on and save.
  6. Copy the signing secret if your receiver can verify webhook signatures.
  7. Select Send Test Event and confirm that your automation receives event: test.
  8. Test an article publish and a fix separately before enabling the workflow for production.

If the URL is missing, the toggle is off, or your endpoint is not HTTPS, Nuwtonic cannot send articles or fixes.

Trigger real article and fix samples

Send Test Event sends only event: test. It checks the connection but does not include an article or any fixes.

To test publishing:

  1. Create or open a generated article in Nuwtonic.
  2. Choose the webhook integration and publish a low-risk test article.
  3. Confirm that the automation receives an actionable content.generated event.

To test content and SEO fixes:

  1. Open SEO Performance → SEO Audit & Fix.
  2. Run an audit for a low-risk page that you can restore if needed.
  3. Review the generated changes.
  4. Select Apply Fix to CMS for one page. Use Apply All Fixes only after the single-page workflow succeeds.
  5. Confirm that the automation receives event: fix.apply, finds the same page from data.target_url, and applies data.fixes.
  6. Check the live page and CMS fields before using the workflow on more pages.

The webhook must be the enabled integration for that workspace when you publish or apply the fix.

3. Zapier setup

  1. In Zapier, create a Zap and choose Webhooks by Zapier → Catch Hook. This trigger needs a paid Zapier plan.
  2. Copy the Catch Hook URL.
  3. Paste that URL into Nuwtonic, enable the webhook, and save.
  4. Select Send Test Event in Nuwtonic and load that request in Zapier.
  5. Add Paths by Zapier inside this Zap:
    • Article path: continue when event equals content.generated, data.result.title or body exists, and data.result.published_count does not exist.
    • Fix path: continue when event equals fix.apply.
    • Other events: stop, or send them to a log.
  6. Configure the article and fix actions described below.
  7. Test each path, then turn the Zap on.

You pay Zapier, not Nuwtonic.

Zapier article path

Map these fields to Create Post, Update Post, or another destination action:

  • title: data.result.title
  • body: data.result.content_html or data.result.content
  • slug: data.result.slug
  • status: data.result.status
  • SEO description, if supported: data.result.meta_description

If the destination requires an existing record ID, add a lookup step before Update Post.

Zapier fix path

  1. Read data.target_url and find exactly one matching CMS page or post.
  2. Use Looping by Zapier, Code by Zapier, or a custom API request to process data.fixes.
  3. Route each fix by its type; do not paste every suggested_value into the article body.
  4. Save the updated page only after the target and current content have been validated.
  5. Log failures in Zap History and alert the user.

Zapier's standard WordPress actions may handle basic title and body updates but may not expose SEO-plugin metadata, JSON-LD, image alt text, or exact section replacement. For those fixes, call an authenticated CMS endpoint with Webhooks by Zapier, add a small custom receiver, or use Nuwtonic's native WordPress integration.

Zapier Catch Hook acknowledges the request before later actions finish. A successful webhook receipt therefore does not prove that every downstream Zapier action succeeded. Monitor Zap History and configure error alerts.

4. n8n setup

  1. Add a Webhook node with method POST.
  2. Select its Production URL. Do not use the temporary Test URL for the permanent Nuwtonic connection.
  3. Set the Webhook node to respond using Respond to Webhook.
  4. Activate the workflow, copy the Production URL into Nuwtonic, enable the webhook, and save.
  5. Add a Switch node using the event field:
    • content.generated → check for a real article and reject summaries containing published_count.
    • fix.apply → run the fix workflow.
    • test, fix.generated, and summaries → acknowledge and stop or log.
  6. Configure the article and fix branches described below.
  7. Add Respond to Webhook after the destination action. Return {"id":"<destination-id>","link":"<final-url>"} after a successful article publish or fix application.
  8. Test both branches while the workflow is active.

You pay n8n (or your n8n host), not Nuwtonic.

n8n article branch

Use a WordPress, HTTP Request, database, or CMS node. Map the article fields from data.result. Return the destination record ID and final URL with Respond to Webhook.

n8n fix branch

  1. Resolve data.target_url to one editable CMS record.
  2. Use a Code, Split Out, or loop node to inspect data.fixes.
  3. Apply the fix rules in How to apply fix.apply below.
  4. Prefer loading the page once, applying all safe fixes, and saving once.
  5. Return 404 if no page matches, 409 if current_value no longer matches, or 422 if your CMS cannot support a fix type.
  6. Return the destination ID and URL only after the update succeeds.

5. Make setup

  1. In Make, create a scenario and add Webhooks → Custom webhook.
  2. Copy its URL into Nuwtonic, enable the webhook, and save.
  3. Select Run once in Make, then send a Nuwtonic test event so Make detects the payload.
  4. Add a Router with filters:
    • Article route: event = content.generated, an article title or body exists, and published_count does not exist.
    • Fix route: event = fix.apply.
    • Notification route: log or ignore test, fix.generated, and publish summaries.
  5. Configure the article and fix routes described below.
  6. Put Webhook Response after the destination update. On success, return status 200 and {"id":"<destination-id>","link":"<final-url>"}.
  7. Test both routes, choose an appropriate schedule, and activate the scenario.

You pay Make, not Nuwtonic.

Make article route

Map data.result.title, content_html or content, slug, and status to a WordPress, HTTP, database, or CMS module.

Make fix route

  1. Find the page using data.target_url.
  2. Use an Iterator for data.fixes, or use a code/API module that applies all fixes in one operation.
  3. Route by fix type and follow the safe-apply rules below.
  4. Aggregate the changes and save once where possible.
  5. Use Webhook Response only after the destination confirms the update.

6. How to apply fix.apply

The fix.apply event contains instructions for an existing page:

  • data.target_url: the exact page Nuwtonic audited.
  • data.fixes: the list of requested changes.
  • each fix has a type, location, and suggested_value; action may be before, after, or replace and defaults to after. Some fixes also include current_value, location_heading, or reasoning.

Your automation must handle each type correctly:

  • meta: update the SEO title or meta-description field. Do not insert the <title> or <meta> markup into the visible article body.
  • schema: parse and validate suggested_value as JSON-LD, then update the CMS schema/head field. Do not show JSON-LD as visible text.
  • content_addition: add the supplied HTML before, after, or inside the named location. Check that equivalent content is not already present.
  • content_replacement: replace only the supplied current_value at the identified location. If it no longer matches, stop and report a conflict instead of replacing similar text.
  • structural: update the identified heading or section only when the target is unambiguous.
  • image_alt: find the image using the URL in location and update only its alt text.
  • internal_link: add or update the uniquely identified link without replacing unrelated text.
  • faq: add visible FAQ content. Some requests send FAQ as two fixes: content_addition for visible HTML and schema for FAQPage JSON-LD; apply both without duplicating either.

If a destination cannot support a fix type, stop that fix and record a clear error. Do not silently mark it applied.

Destination requirements

  • WordPress: the workflow needs permission to find and update the post/page. SEO title, meta description, and schema often belong to Yoast, Rank Math, another plugin, or custom fields that must be exposed through the WordPress REST API. The standard WordPress automation action may not expose them.
  • Shopify, Webflow, Ghost, or another CMS: use the platform's update API and credentials. Confirm which fields accept metadata, JSON-LD, HTML, and image alt text.
  • Slack, Sheets, email, or a CRM: these can log fixes or request approval, but they do not apply a website fix unless another step updates the CMS.

Required success and failure responses

For an applied article or fix, n8n, Make, and custom receivers should return a 2xx response with the destination ID and final URL:

{
  "id": "cms-post-123",
  "link": "https://example.com/final-page"
}

Return a non-2xx response when the operation was not safely applied. A 2xx response means success to Nuwtonic.

Zapier Catch Hook returns its own acknowledgment before the Zap finishes, so it cannot confirm the final CMS result in this response. With Zapier, Nuwtonic can confirm delivery to Zapier but not the success of later actions. Use Zap History and error alerts.

7. Prevent duplicates and unnecessary automation charges

A publish job can send the article and a completion summary to the same URL. A fix job can send fix.apply and a later fix.generated notification. Only the first event in each pair is actionable.

  • Publish only when event = content.generated, an article title or body exists, and data.result.published_count does not exist.
  • Apply fixes only when event = fix.apply and both data.target_url and data.fixes exist.
  • Never apply changes for fix.generated.
  • Store each top-level event id and do not process it twice.

8. Article payload example

Map these fields in Zapier, n8n, or Make:

{
  "id": "evt_0f1d2c3b4a5e6f70",
  "event": "content.generated",
  "created": 1785058200,
  "data": {
    "job_id": "content-job-001",
    "workspace_id": "workspace-id",
    "status": "SUCCESS",
    "result": {
      "id": "generated-article-id",
      "keyword": "best ai seo tools",
      "title": "Best AI SEO Tools",
      "slug": "best-ai-seo-tools",
      "status": "publish",
      "content": "<h1>Best AI SEO Tools</h1><p>...</p>",
      "content_html": "<h1>Best AI SEO Tools</h1><p>...</p>",
      "meta_description": "A clear description for search results."
    }
  }
}

Use data.result.title and data.result.content or data.result.content_html as the article. Ignore a later payload that has data.result.published_count. Automation field pickers may display these as labels such as Data → Result → Title instead of dot notation.

9. Fix payload example

This example asks the workflow to update one existing article. It must not create a new post:

{
  "id": "evt_214a2f9bdc294df1",
  "event": "fix.apply",
  "created": 1785059000,
  "data": {
    "target_url": "https://example.com/blog/best-ai-seo-tools",
    "fixes": [
      {
        "type": "meta",
        "location": "<head>",
        "action": "replace",
        "suggested_value": "<title>Best AI SEO Tools for 2026</title>"
      },
      {
        "type": "content_replacement",
        "location": "Introduction",
        "action": "replace",
        "current_value": "Old introduction text.",
        "suggested_value": "Updated introduction text."
      }
    ]
  }
}

The workflow must find the existing page from target_url, validate current_value, update the SEO title in the metadata field, replace the exact introduction text, save the page, and return its ID and URL.

10. Security and authentication

Incoming Nuwtonic webhook: Nuwtonic sends a signed JSON POST. Your custom receiver should verify X-ContentKit-Signature with the signing secret. Managed Catch Hook / Custom Webhook triggers can receive the request without adding an API key, but you should keep their generated URLs private.

Calls from your automation to your CMS: use the authentication required by that CMS. Never place a CMS password or token in a Nuwtonic webhook URL.

Calls from an HTTP module to the Nuwtonic REST API: use Authorization: Bearer <api_key>. Do not use X-API-Key.

Developer signature details are in the technical guide below.


Webhook Integration Guide

Use the webhook integration when you want Nuwtonic to send generated content and optimization instructions to your own CMS, backend, or automation.

You configure one webhook URL. The same URL receives:

  • generated articles that your system must publish;
  • optimization fixes that your system must apply;
  • completion notifications describing the final result.

Your endpoint must inspect the top-level event field and the contents of data.result before deciding what to do.

1. Configuration

Configure the integration with this credential structure:

{
  "webhook": {
    "url": "https://cms.example.com/webhooks/nuwtonic",
    "secret": "generated-by-nuwtonic",
    "enabled": true
  }
}

Configuration fields:

  • url is the public HTTPS endpoint that receives every webhook event.
  • secret is generated by Nuwtonic and signs each request with HMAC-SHA256. Copy it when you create or rotate it if your receiver verifies signatures.
  • enabled controls delivery. When false, publishing and fix application are skipped.

Use the nested webhook object shown above for all webhook integrations.

Your endpoint must:

  1. Accept POST requests with JSON bodies.
  2. Read and preserve the raw request body before parsing JSON.
  3. Verify the signature.
  4. Route the request using event.
  5. Make processing idempotent using the top-level event id.
  6. Return a 2xx response within 30 seconds.

2. Common request envelope

Every request uses this structure:

{
  "id": "evt_0f1d2c3b4a5e6f70",
  "event": "content.generated",
  "created": 1785058200,
  "data": {}
}

Common fields:

  • id: unique event identifier. Store it and do not process the same ID twice.
  • event: event type used to route the request.
  • created: Unix timestamp in seconds.
  • data: event-specific payload.

The two actionable event types are:

  • content.generated: publish the article only when data.result is an article object.
  • fix.apply: apply the supplied fixes to data.target_url.

Completion notifications are:

  • content.generated: publish-job summary when data.result.published_count exists.
  • fix.generated: fix-job completion status.

3. Headers and signature verification

Signed requests include:

Content-Type: application/json
X-ContentKit-Signature: t=1785058200,v1=<hex-hmac-sha256>
X-ContentKit-Timestamp: 1785058200

Signature algorithm

Build the signed value from the timestamp header, a period, and the exact raw request body:

signed_payload = X-ContentKit-Timestamp + "." + raw_request_body
signature = HMAC_SHA256(webhook_secret, signed_payload)

Compare the hexadecimal result with the v1 value from X-ContentKit-Signature using a constant-time comparison.

Do not parse and re-serialize the JSON before verification. Whitespace and key order are part of the signature.

Recommended checks:

  • reject requests with missing signature headers;
  • reject invalid signatures with 401;
  • reject timestamps older than five minutes to reduce replay risk;
  • store processed event IDs and return 2xx for already processed IDs.

Python verification example

import hashlib
import hmac
import time


def verify_webhook(raw_body: bytes, signature_header: str, timestamp: str, secret: str) -> bool:
    if not signature_header or not timestamp:
        return False

    parts = dict(
        part.split("=", 1)
        for part in signature_header.split(",")
        if "=" in part
    )
    if parts.get("t") != timestamp or not parts.get("v1"):
        return False

    try:
        timestamp_value = int(timestamp)
    except ValueError:
        return False

    if abs(int(time.time()) - timestamp_value) > 300:
        return False

    signed_payload = timestamp.encode() + b"." + raw_body
    expected = hmac.new(
        secret.encode(),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Node.js verification example

import crypto from "node:crypto";

export function verifyWebhook(rawBody, signatureHeader, timestamp, secret) {
  if (!signatureHeader || !timestamp) return false;

  const parts = Object.fromEntries(
    signatureHeader
      .split(",")
      .map((part) => part.split("=", 2))
      .filter(([key, value]) => key && value)
  );

  if (parts.t !== timestamp || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const expectedBuffer = Buffer.from(expected, "hex");
  const receivedBuffer = Buffer.from(parts.v1, "hex");
  return (
    expectedBuffer.length === receivedBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
  );
}

4. Generated article event

When a user publishes or saves generated content through the webhook integration, your endpoint receives one actionable content.generated event per article.

{
  "id": "evt_0f1d2c3b4a5e6f70",
  "event": "content.generated",
  "created": 1785058200,
  "data": {
    "job_id": "content-job-001",
    "workspace_id": "workspace-id",
    "domain": "example.com",
    "status": "SUCCESS",
    "summary": {
      "total": 1,
      "processed": 1,
      "failed": 0
    },
    "result": {
      "id": "generated-article-id",
      "keyword": "best ai seo tools",
      "title": "Best AI SEO Tools",
      "slug": "best-ai-seo-tools",
      "status": "publish",
      "content": "<h1>Best AI SEO Tools</h1><p>...</p>",
      "content_html": "<h1>Best AI SEO Tools</h1><p>...</p>",
      "meta_description": "A clear description for search results.",
      "category": "SEO",
      "categories": ["SEO"],
      "schema": {
        "@context": "https://schema.org",
        "@type": "Article"
      }
    }
  }
}

data.result is the generated article record. It is passed through from Nuwtonic, so optional fields can vary by generation workflow. Do not reject the complete event because an optional example field is absent.

At minimum, your publisher should:

  1. Validate the fields required by your CMS.
  2. Use result.id or the event ID as an idempotency key.
  3. Create or update the article.
  4. Respect result.status:
    • publish: make the article live;
    • draft: save it without making it live.
  5. Store and return the final CMS ID and article URL.

If slug is missing, Nuwtonic may derive it from keyword, but your system should still validate it before publishing.

Required response

Return a JSON response with the external resource ID and final article URL:

{
  "id": "cms-post-123",
  "link": "https://cms.example.com/blog/best-ai-seo-tools"
}

url is accepted instead of link:

{
  "id": "cms-post-123",
  "url": "https://cms.example.com/blog/best-ai-seo-tools"
}

Always return the real article URL. Responses without link or url cannot reliably identify the published article.

5. Publish completion notification

After all requested articles have been processed, the same URL can receive another content.generated event. This second event is a job summary and must not create another article.

Identify it by the presence of data.result.published_count and data.result.results.

{
  "id": "evt_91ba9852ea9146ab",
  "event": "content.generated",
  "created": 1785058260,
  "data": {
    "job_id": "publish-job-123",
    "workspace_id": "workspace-id",
    "domain": "example.com",
    "status": "SUCCESS",
    "summary": {
      "total": 1,
      "processed": 1,
      "failed": 0
    },
    "result": {
      "published_count": 1,
      "results": [
        {
          "keyword": "best ai seo tools",
          "status": "published",
          "post_id": "cms-post-123",
          "link": "https://cms.example.com/blog/best-ai-seo-tools",
          "category": "SEO",
          "categories": ["SEO"]
        }
      ],
      "selected_category": "SEO",
      "selected_categories": ["SEO"]
    }
  }
}

For this summary event:

  • record or display the job result if needed;
  • do not publish data.result as an article;
  • return any 2xx response.

6. Optimization fix event

When optimizations are pushed through the webhook integration, your endpoint receives:

{
  "id": "evt_214a2f9bdc294df1",
  "event": "fix.apply",
  "created": 1785059000,
  "data": {
    "target_url": "https://cms.example.com/blog/best-ai-seo-tools",
    "summary": {
      "total": 4,
      "processed": 4,
      "failed": 0
    },
    "fixes": [
      {
        "type": "meta",
        "location": "<head>",
        "action": "replace",
        "suggested_value": "<title>Best AI SEO Tools for 2026</title>"
      },
      {
        "type": "content_replacement",
        "location": "Introduction",
        "location_heading": "H2",
        "action": "replace",
        "current_value": "Old introduction text.",
        "suggested_value": "Updated introduction text.",
        "reasoning": "The updated introduction answers the search intent directly."
      },
      {
        "type": "image_alt",
        "location": "https://cdn.example.com/images/seo-dashboard.jpg",
        "action": "replace",
        "current_value": "",
        "suggested_value": "SEO analytics dashboard showing keyword growth"
      },
      {
        "type": "schema",
        "location": "<head>",
        "action": "after",
        "suggested_value": "{\"@context\":\"https://schema.org\",\"@type\":\"Article\"}"
      }
    ]
  }
}

Use data.target_url to find the existing page or article. Do not apply a fix to another page when the URL cannot be resolved uniquely.

Fix object fields

Each item in data.fixes contains:

  • type: required fix type.
  • location: required target or placement instruction.
  • action: before, after, or replace. If omitted, treat it as after.
  • suggested_value: required new value.
  • current_value: optional value observed during the audit.
  • location_heading: optional heading or element hint.
  • reasoning: optional explanation for the recommendation.

Optional values are omitted from the JSON rather than sent as null.

Fix types

meta

Updates SEO metadata. suggested_value normally contains either:

<title>Updated SEO title</title>

or:

<meta name="description" content="Updated search description">

Update SEO metadata rather than renaming the visible article unless your CMS intentionally uses the same field for both.

schema

Adds or updates JSON-LD. suggested_value contains a serialized JSON object.

Validate it with a JSON parser before saving it. Render it as:

<script type="application/ld+json">...</script>

Do not insert an equivalent schema block twice.

content_addition

Adds HTML content relative to location. Common placements include end of content or a named heading.

Before adding it, check whether equivalent content already exists.

content_replacement

Replaces existing content. When current_value is present, only replace content that still matches it. If it has changed since the audit, skip the fix and report the conflict.

structural

Changes document structure, such as headings or sections. Use location, location_heading, and current_value together. Skip ambiguous targets.

image_alt

Updates image alt text:

  • location is the image URL;
  • current_value is the previous alt text when available;
  • suggested_value is the new alt text.

Match the image uniquely. Preserve the image itself, file ID, dimensions, caption, and URL.

internal_link

Adds or updates an internal link. Preserve the surrounding text and only change the uniquely identified anchor.

faq

Adds FAQ content. Manual fix requests may use type: "faq".

High-level SEO requests usually produce two separate fixes instead:

  1. content_addition containing visible FAQ HTML;
  2. schema containing FAQPage JSON-LD.

Support both representations.

Applying fixes safely

Recommended processing order:

  1. Resolve target_url to exactly one editable resource.
  2. Load the latest version from your CMS.
  3. Verify every supplied current_value.
  4. Apply metadata and schema.
  5. Apply image alt fixes.
  6. Apply additions and links.
  7. Apply replacements and structural changes.
  8. Save using optimistic locking or version checks when available.
  9. Publish only after the complete update succeeds.

Skip a fix instead of guessing when its target is missing or ambiguous.

Required response

After applying the fixes, return:

{
  "id": "fix-operation-456",
  "link": "https://cms.example.com/blog/best-ai-seo-tools"
}

url is accepted instead of link.

Return 2xx only after your system has applied or intentionally and safely skipped the fixes. Return a non-2xx response when the complete operation should be treated as failed.

7. Fix completion notification

After the actionable fix.apply request finishes, the same URL can receive a fix.generated completion event:

{
  "id": "evt_65e5d1e6c83d4040",
  "event": "fix.generated",
  "created": 1785059060,
  "data": {
    "job_id": "optimization-job-456",
    "workspace_id": "workspace-id",
    "domain": "cms.example.com",
    "status": "SUCCESS",
    "summary": {
      "total": 1,
      "processed": 1,
      "failed": 0
    },
    "result": {
      "status": "success",
      "id": "fix-operation-456",
      "link": "https://cms.example.com/blog/best-ai-seo-tools"
    }
  }
}

A failed job uses:

{
  "event": "fix.generated",
  "data": {
    "status": "FAILED",
    "summary": {
      "total": 1,
      "processed": 0,
      "failed": 1
    },
    "result": {
      "error": "Webhook returned error status 500"
    }
  }
}

Completion events are notifications only. Record their status if needed and return any 2xx response.

8. Single-endpoint routing example

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = "replace-with-the-configured-secret"


@app.post("/webhooks/nuwtonic")
async def nuwtonic_webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("X-ContentKit-Signature", "")
    timestamp = request.headers.get("X-ContentKit-Timestamp", "")

    if not verify_webhook(raw_body, signature, timestamp, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    payload = await request.json()
    event_id = payload.get("id")
    event = payload.get("event")
    data = payload.get("data") or {}

    if not event_id:
        raise HTTPException(status_code=400, detail="Webhook event ID is missing")

    if await event_was_processed(event_id):
        return {"status": "already_processed"}

    if event == "content.generated":
        result = data.get("result") or {}

        if "published_count" in result and "results" in result:
            await record_publish_summary(payload)
            await mark_event_processed(event_id)
            return {"status": "summary_recorded"}

        post = await publish_or_update_article(result)
        await mark_event_processed(event_id)
        return {"id": post.id, "link": post.url}

    if event == "fix.apply":
        operation = await apply_fixes(
            target_url=data.get("target_url"),
            fixes=data.get("fixes") or [],
        )
        await mark_event_processed(event_id)
        return {"id": operation.id, "link": operation.url}

    if event == "fix.generated":
        await record_fix_completion(payload)
        await mark_event_processed(event_id)
        return {"status": "completion_recorded"}

    # Acknowledge unknown notification events so future additions do not
    # create repeated delivery failures. Log them for later review.
    await record_unknown_event(payload)
    await mark_event_processed(event_id)
    return {"status": "ignored", "reason": "unsupported_event"}

The storage and CMS functions in this example are placeholders that must be implemented for your system.

9. Delivery and retry behavior

Actionable deliveries:

  • content.generated article events use a 30-second timeout.
  • fix.apply events use a 30-second timeout.
  • Any HTTP status from 200 through 299 is accepted.
  • A non-2xx response causes that publish or fix operation to fail.
  • Direct actionable deliveries do not currently guarantee automatic webhook retries.

Completion notifications:

  • use a 30-second timeout;
  • retry failures up to three times with exponential backoff;
  • can therefore arrive more than once.

Because retries and network ambiguity can always produce duplicates, idempotency is required for every event.

10. HTTP response guidance

Use these responses so users receive clear results:

  • 200 or 201: operation completed successfully.
  • 202: accepted only if your system can safely finish asynchronously and has already persisted the operation.
  • 400: payload is invalid or required fields are missing.
  • 401: signature is missing or invalid.
  • 404: target_url cannot be resolved.
  • 409: content changed after the audit and cannot be safely updated.
  • 422: a fix is valid JSON but unsupported by your CMS.
  • 429: temporary rate limit; include Retry-After where possible.
  • 500 or 503: temporary server failure.

Return clear error JSON:

{
  "error": "target_not_found",
  "message": "No editable article matches data.target_url."
}

Do not return 2xx with an error body. Nuwtonic treats every 2xx response as successful.

11. Production checklist

  • Use a public HTTPS endpoint.
  • Store the webhook secret securely.
  • Verify signatures against the raw body.
  • Enforce a timestamp tolerance.
  • Deduplicate using the event ID.
  • Distinguish actionable articles from publish summaries.
  • Return the final article URL for publish requests.
  • Resolve fix target URLs exactly.
  • Check current_value before replacing content.
  • Preserve image references when changing alt text.
  • Validate and deduplicate JSON-LD.
  • Log event ID, event type, target URL, response status, and processing result.
  • Keep processing below 30 seconds or persist work before returning 202.
  • Monitor non-2xx responses and completion events.

12. Current limitations

  • One URL receives all publish, fix, and completion events; separate event URLs are not supported.
  • The article object is a pass-through record, so optional fields vary by generation workflow.
  • The same content.generated event name is used for both actionable article delivery and publish completion summaries. Inspect data.result before processing.
  • The webhook integration sends instructions; your receiver is responsible for CMS-specific updates, conflict handling, publishing, and rollback.
  • Returning 202 does not provide a later callback API for your system to report its own asynchronous result.
  • Direct article and fix deliveries do not currently guarantee automatic retries.