Webhook
On every article publish we POST a signed JSON payload to the URL you configured at Settings → Integrations.
Authentication
Two layers, both optional but both recommended:
Authorization: Bearer <your-token>— opaque, you supply or we generate.X-Earlyseo-Signature: <hex>— HMAC-SHA256 of the raw body.
Payload
{
"event": "article.published",
"siteId": "<uuid>",
"article": { id, title, slug, metaDescription, contentRawHtml, contentCss, tags, publishedAt }
}Retries
| Response | Action |
|---|---|
| 2xx | marked success, response_code recorded |
| 4xx | marked failed, no retry (your endpoint rejected) |
| 5xx / network error | attempt += 1, next_retry_at = now + 60s × 2^attempt |
| 10 attempts | marked exhausted, no further retries |
Receivers
Next.js
// app/api/earlyseo/route.ts
import crypto from "crypto";
export async function POST(req) {
const body = await req.text();
const sig = req.headers.get("x-earlyseo-signature");
const expected = crypto.createHmac("sha256", process.env.EARLYSEO_HMAC_SECRET).update(body).digest("hex");
if (sig !== expected) return new Response("invalid", { status: 401 });
const { article } = JSON.parse(body);
// ... persist or proxy
return Response.json({ ok: true });
}Express
app.post("/earlyseo", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("x-earlyseo-signature");
const expected = crypto.createHmac("sha256", process.env.EARLYSEO_HMAC_SECRET).update(req.body).digest("hex");
if (sig !== expected) return res.status(401).end();
// ...
});Flask
@app.post("/earlyseo")
def receive():
body = request.get_data()
sig = request.headers.get("X-Earlyseo-Signature")
expected = hmac.new(SECRET.encode(), body, "sha256").hexdigest()
if not hmac.compare_digest(sig, expected): abort(401)
# ...
return {"ok": True}