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:
| Import | Fit |
|---|---|
@42flowsdotcom/webhook/next | Next.js app router route handlers. |
@42flowsdotcom/webhook/express | Express and Connect-style middleware. |
@42flowsdotcom/webhook/web | Web standard Request handlers for SvelteKit, Astro, Hono, Workers, Bun, and Deno. |
@42flowsdotcom/webhook | Core 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.
| Field | Use it when |
|---|---|
content_html | The site stores rendered HTML, uses a database-backed blog, or does not support MDC/MDX components. |
content_markdown | The site renders markdown/MDC/MDX and can map 42flows rich blocks to components. |
json_ld | The site controls its own <script type="application/ld+json"> output. |
backbone | The 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:
- Verify the bearer token.
- Accept
ping. - Validate
publish_article. - Upsert by
article.slugso redelivery is safe. - Store either HTML or markdown plus metadata.
- Trigger revalidation, build, or cache purge if the framework needs it.
- Return a public
postUrl. - 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:
- Store the article in the CMS, database, or file store.
- Trigger the build or incremental revalidation.
- Return the final public URL if it is predictable.
- 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 field | Render target |
|---|---|
article.title | Page heading. |
article.meta_title | <title> and Open Graph title. |
article.meta_description | Meta description and social description. |
article.json_ld | JSON-LD scripts in <head> or page body. |
article.content_html | Main article body for HTML renderers. |
article.content_markdown | Main article body for markdown/MDC/MDX renderers. |
Verification and troubleshooting
| Symptom | Likely issue |
|---|---|
| Connection test fails with 401 | Token mismatch or missing Authorization: Bearer ... handling. |
| Connection test fails with validation error | The handler only accepts publish_article and forgot to accept ping. |
| Delivery succeeds but no page appears | The handler stored the article but did not trigger revalidation/build or the route cannot read the stored record. |
| Delivery fails after 30 seconds | The handler is doing slow work inline. Store quickly, trigger background work, and return. |
| Duplicate articles appear | The handler inserts instead of upserting by slug. |
| Verification cannot find the marker | The 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.