PublishingWebhooks

Webhooks

Publish to any system by receiving the full article payload over HTTP.

Webhooks

A webhook integration turns "publish" into an HTTP POST. Serpon sends the finished article — content, metadata, and SEO fields — to an endpoint you control, so you can deliver into any platform, native connector or not.

Set up

Create the integration

Under Integrations, add an integration and choose Custom Webhook. Provide the endpoint URL that will receive the payload.

Choose a payload format

Payload content format controls which encodings of the article body ride along:

  • json (default) — both HTML and Markdown
  • html — HTML only
  • markdown — Markdown only

The rest of the payload is identical in all three cases.

Test it

Test connection posts a small probe to your endpoint and accepts any 2xx:

{ "event": "connection_test", "integration": "My Webhook" }

Note this probe is not shaped like a real article payload — treat it only as a reachability check.

Delivery

PropertyValue
MethodPOST
Content typeapplication/json
Timeout30 seconds
Retries3 attempts, backing off 10s → 20s → 30s
SuccessAny 2xx. Anything else is retried, then logged as failed on the article

Payloads are not signed. Serpon sends no HMAC or signature header, so your endpoint cannot verify authenticity from the request alone. Treat the URL itself as the secret: make it unguessable, serve it over HTTPS, and add your own check — a token in the query string or a path segment only you know.

Delivery is queued and asynchronous, so a publish is recorded as soon as the job is accepted. Check the article's activity log for the delivery outcome, including the status code and the first 1000 characters of your endpoint's response — which is usually enough to see why a rejection happened.

Payload

Three top-level blocks sit beside a success flag:

{
  "success": true,
  "data": { },
  "content": { },
  "seo": { }
}

content

The article body, in the encodings your chosen format selects.

{
  "content": {
    "format": "json",
    "html": "<h2>Choosing a mattress</h2><p>…</p>",
    "markdown": "## Choosing a mattress

…",
    "meta_title": "How to Choose a Mattress (2026 Guide)",
    "meta_description": "A practical guide to picking the right mattress…"
  }
}
FieldNotes
formatEchoes the configured format: json, html, or markdown
htmlPresent when format is json or html
markdownPresent when format is json or markdown
meta_titleSEO title. null when the article has none
meta_descriptionSEO description. null when the article has none

content.html and content.markdown are the live article — the current version, including any edits made after generation. Use these for publishing rather than data.original_content.

seo

Targeting and quality signals, useful for routing and reporting.

{
  "seo": {
    "target_keyword": "how to choose a mattress",
    "keywords": ["memory foam", "firmness"],
    "language": "en",
    "country_targeting": "US",
    "word_count": 1840,
    "ai_detection_score": 12
  }
}

ai_detection_score is null unless the article was checked for originality.

data

The full article record.

FieldDescription
idNumeric article ID
external_idStable public UUID — prefer this when storing a reference
titleArticle title
descriptionSEO meta description
contentLive article content (current version)
original_contentFrozen generation output, before edits
current_version_id / published_version_idVersion pointers
statusArticle status, e.g. published
article_typeContent type used to generate
target_keyword / keywordsPrimary and secondary keywords
target_word_count / actual_word_countRequested and delivered length
tone / point_of_viewStyle settings
language / country_targetingLocale targeting
ai_model_used / costModel and generation cost
current_stepWorkflow step name
humanize / ai_detection_score / originality_checked_atHumanization and originality
additional_contextExtra context supplied at creation
webhook_urlThe article's own webhook target, if set
project{ id, name }
created_at / updated_atISO 8601 timestamps

Both data.content and content.markdown may arrive wrapped in a JSON envelope for some workflows, where the body lives at article.content. content.html and content.markdown are always unwrapped and ready to publish — another reason to prefer them.

Receiving the payload

Respond 2xx quickly and do the slow work afterwards; anything else is retried three times and then recorded as failed.

app.post("/serpon", async (req, res) => {
  const { content, seo, data } = req.body

  res.sendStatus(200) // acknowledge first

  await publishSomewhere({
    externalId: data.external_id,
    title: data.title,
    body: content.markdown,
    metaTitle: content.meta_title,
    metaDescription: content.meta_description,
    keyword: seo.target_keyword,
  })
})

Use data.external_id as the idempotency key. A retried delivery repeats the same article, so keying on it prevents duplicates if your endpoint times out after doing the work.