← 42flows

Universal webhook integration

Use the universal webhook when the customer website is not WordPress, Shopify, or a clean GitHub/Nuxt Content file-backed site.

It is the recommended path for:

  • Next.js apps with a database, CMS, ISR, or custom page model.
  • SvelteKit, Astro, Remix, Express, Hono, Cloudflare Workers, Laravel, Rails, Django, Go, PHP, or any custom stack.
  • Customers who want to own validation, persistence, build triggers, and cache purge logic.

42flows sends a signed HTTPS POST. The customer handler receives a canonical article payload and decides how to store and render it.

npm package

For JavaScript and TypeScript stacks, install:

npm install @42flowsdotcom/webhook

The package provides adapters for:

ImportFit
@42flowsdotcom/webhook/nextNext.js app router route handlers.
@42flowsdotcom/webhook/expressExpress and Connect-style middleware.
@42flowsdotcom/webhook/webWeb standard Request handlers for SvelteKit, Astro, Hono, Workers, Bun, and Deno.
@42flowsdotcom/webhookCore validation, auth, and type helpers.

Non-JS stacks can implement the raw HTTP contract directly.

Endpoint contract

The customer creates one HTTPS endpoint:

POST https://customer.com/api/42flows/webhook
Authorization: Bearer <FLOWS42_API_KEY>
Content-Type: application/json
User-Agent: 42flows-delivery/1.0 (+https://42flows.com)

42flows rejects non-HTTPS webhook URLs during setup. During connection, 42flows generates a 64-character token unless the customer provides one.

Test ping

Before publishing real content, 42flows sends:

{
  "event_type": "ping",
  "timestamp": "2026-06-13T00:00:00.000Z",
  "data": {
    "message": "42flows webhook connection test"
  }
}

The endpoint should return any 2xx response.

Publish payload

Real delivery uses event_type: "publish_article".

{
  "event_type": "publish_article",
  "timestamp": "2026-06-13T00:00:00.000Z",
  "flow_type": "origin",
  "data": {
    "article": {
      "slug": "best-ai-seo-workflow-for-dentists",
      "title": "Best AI SEO workflow for dentists",
      "content_type": "article",
      "meta_title": "Best AI SEO workflow for dentists",
      "meta_description": "A practical guide to AI SEO workflows for dental clinics.",
      "primary_keyword": "ai seo for dentists",
      "secondary_keywords": ["dental seo", "clinic seo"],
      "published_at": "2026-06-13T00:00:00.000Z",
      "content_markdown": "# Best AI SEO workflow for dentists\n\n...",
      "content_html": "<article>...</article>",
      "json_ld": [{ "@context": "https://schema.org", "@type": "Article" }],
      "backbone": {}
    }
  }
}

Both content_markdown and content_html are populated.

FieldUse it when
content_htmlThe site stores rendered HTML, uses a database-backed blog, or does not support MDC/MDX components.
content_markdownThe site renders markdown/MDC/MDX and can map 42flows rich blocks to components.
json_ldThe site controls its own <script type="application/ld+json"> output.
backboneThe site wants the original structured 42flows content data for custom rendering.

Success response

Return 2xx when the article is accepted.

Recommended response:

{
  "ok": true,
  "postUrl": "https://customer.com/blog/best-ai-seo-workflow-for-dentists"
}

42flows looks for postUrl, post_url, or url. If one is present, it can be used for delivery status and verification.

Failure response

Return a non-2xx status when the article was not accepted.

{
  "ok": false,
  "error": "Missing required category"
}

42flows records the failure so the delivery can be reviewed. Make the customer handler idempotent by slug, so sending the same article again updates the existing article instead of creating a duplicate.

Next.js app router example

Create app/api/42flows/webhook/route.ts:

import { flows42NextRoute } from '@42flowsdotcom/webhook/next'

export const POST = flows42NextRoute({
  apiKey: process.env.FLOWS42_API_KEY!,
  async onPublish({ article }) {
    await upsertArticle({
      slug: article.slug,
      title: article.title,
      html: article.content_html,
      markdown: article.content_markdown,
      metaTitle: article.meta_title,
      metaDescription: article.meta_description,
      jsonLd: article.json_ld,
    })

    await revalidateArticlePath(`/blog/${article.slug}`)

    return {
      postUrl: `https://example.com/blog/${article.slug}`,
    }
  },
})

The route should also be able to answer the ping event. The package handles auth, ping parsing, validation, and response shaping.

Express example

import express from 'express'
import { flows42Express } from '@42flowsdotcom/webhook/express'

const app = express()

app.post('/api/42flows/webhook', flows42Express({
  apiKey: process.env.FLOWS42_API_KEY!,
  async onPublish({ article }) {
    await db.article.upsert({
      where: { slug: article.slug },
      update: {
        title: article.title,
        html: article.content_html,
        metaTitle: article.meta_title,
        metaDescription: article.meta_description,
      },
      create: {
        slug: article.slug,
        title: article.title,
        html: article.content_html,
        metaTitle: article.meta_title,
        metaDescription: article.meta_description,
      },
    })

    return { postUrl: `https://example.com/blog/${article.slug}` }
  },
}))

Web standard handler

Use the web adapter for SvelteKit, Astro, Hono, Workers, Bun, and Deno-style runtimes:

import { flows42WebHandler } from '@42flowsdotcom/webhook/web'

export const POST = flows42WebHandler({
  apiKey: process.env.FLOWS42_API_KEY!,
  async onPublish({ article }) {
    await saveArticle(article)
    return { postUrl: `https://example.com/blog/${article.slug}` }
  },
})

What the customer handler must do

A production handler should:

  1. Verify the bearer token.
  2. Accept ping.
  3. Validate publish_article.
  4. Upsert by article.slug so redelivery is safe.
  5. Store either HTML or markdown plus metadata.
  6. Trigger revalidation, build, or cache purge if the framework needs it.
  7. Return a public postUrl.
  8. Log the delivery in the customer app.

42flows can send the content. The customer app owns the final render route, storage, CDN purge, and build behavior.

Static build recipe

For static builds, the webhook handler should usually:

  1. Store the article in the CMS, database, or file store.
  2. Trigger the build or incremental revalidation.
  3. Return the final public URL if it is predictable.
  4. Make sure the rendered page contains the 42flows marker from the article body.

If the site cannot publish until a build finishes, return 202 plus the predictable URL, then rely on delivery verification after deployment.

SEO fields

The customer renderer should map:

42flows fieldRender target
article.titlePage heading.
article.meta_title<title> and Open Graph title.
article.meta_descriptionMeta description and social description.
article.json_ldJSON-LD scripts in <head> or page body.
article.content_htmlMain article body for HTML renderers.
article.content_markdownMain article body for markdown/MDC/MDX renderers.

Verification and troubleshooting

SymptomLikely issue
Connection test fails with 401Token mismatch or missing Authorization: Bearer ... handling.
Connection test fails with validation errorThe handler only accepts publish_article and forgot to accept ping.
Delivery succeeds but no page appearsThe handler stored the article but did not trigger revalidation/build or the route cannot read the stored record.
Delivery fails after 30 secondsThe handler is doing slow work inline. Store quickly, trigger background work, and return.
Duplicate articles appearThe handler inserts instead of upserting by slug.
Verification cannot find the markerThe rendered page strips article body comments/classes or serves a stale cached version.

What your app owns

The webhook is deliberately simple. Your app remains responsible for:

  • Listing or searching its own pages.
  • Editing existing pages outside the webhook article flow.
  • Undoing a published article if the customer asks for removal.
  • Triggering builds, cache purge, or ISR revalidation.
  • Making sure the returned URL becomes public.

That is the tradeoff: one endpoint is easy to add, and the website keeps control of how content is stored and rendered.