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 Markdownhtml— HTML onlymarkdown— 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
| Property | Value |
|---|---|
| Method | POST |
| Content type | application/json |
| Timeout | 30 seconds |
| Retries | 3 attempts, backing off 10s → 20s → 30s |
| Success | Any 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…"
}
}
| Field | Notes |
|---|---|
format | Echoes the configured format: json, html, or markdown |
html | Present when format is json or html |
markdown | Present when format is json or markdown |
meta_title | SEO title. null when the article has none |
meta_description | SEO 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.
| Field | Description |
|---|---|
id | Numeric article ID |
external_id | Stable public UUID — prefer this when storing a reference |
title | Article title |
description | SEO meta description |
content | Live article content (current version) |
original_content | Frozen generation output, before edits |
current_version_id / published_version_id | Version pointers |
status | Article status, e.g. published |
article_type | Content type used to generate |
target_keyword / keywords | Primary and secondary keywords |
target_word_count / actual_word_count | Requested and delivered length |
tone / point_of_view | Style settings |
language / country_targeting | Locale targeting |
ai_model_used / cost | Model and generation cost |
current_step | Workflow step name |
humanize / ai_detection_score / originality_checked_at | Humanization and originality |
additional_context | Extra context supplied at creation |
webhook_url | The article's own webhook target, if set |
project | { id, name } |
created_at / updated_at | ISO 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.