Skip to Content
ReferenceArticle delivery contract

Article delivery contract

Developer reference for receiving complete published articles.

Configure a separate signing secret for each receiver. Secrets stay in encrypted connection storage and are never returned in connection summaries. Treat the signature as authentication; do not trust a site ID before verifying it.

Request and verification

Structura sends HTTPS POST with Content-Type: application/json, X-Structura-Event-Id, X-Structura-Timestamp (Unix seconds) and X-Structura-Signature (sha256= followed by a lowercase hex digest). Compute HMAC-SHA256 over timestamp + "." + rawBody using the secret as UTF-8. Compare equal-length digest bytes in constant time, reject timestamps more than five minutes from your clock, then parse JSON. Validate the header event ID matches the body. Do not stringify parsed JSON for verification.

Payload

{ "schema_version": 1, "event": "post.published", "event_id": "article_<sha256>", "delivered_at": "2026-09-28T10:00:00.000Z", "site": { "id": "site_123" }, "post": { "id": "post_123", "slug": "first-article", "title": "First article", "html": "<p>Article body</p>", "markdown": "Article body", "excerpt": null, "metaTitle": null, "metaDescription": null, "focusKeyword": null, "locale": "en", "publishedAt": "2026-09-28T10:00:00.000Z", "updatedAt": "2026-09-28T10:00:00.000Z", "canonicalUrl": "https://example.com/blog/first-article", "featuredImage": null, "author": null, "jsonLd": [] } }

id is a string for headless posts or a number for WordPress posts. Optional metadata is null; jsonLd is an array. HTML is sanitized and Markdown is generated from that HTML. Content locale is independent of the notification language. Images remain hosted at their supplied URLs. Author details appear only when the source supplies a public identity.

Before inlining jsonLd inside a <script type="application/ld+json"> element, serialize it and escape every < as \u003c. A valid JSON value can contain </script>; HTML sanitization does not make that JSON safe to inline.

The complete request is limited to 1,000,000 UTF-8 bytes, including HTML, Markdown, metadata and JSON escaping. Normalization allows 995,904 bytes for the article and reserves 4,096 bytes for the event envelope. Oversized articles are skipped with article_too_large; use the Content API for larger content.

Storage and delivery semantics

Use a durable unique constraint on event_id. In the same transaction, upsert by site.id, post.id and post.locale, and record the event as processed. Acknowledge only after commit. A duplicate valid event returns 2xx without reapplying it. Compare updatedAt to avoid replacing a newer stored revision with an older request. Event IDs include tenant/site/content identity, so a republished revision has a new ID.

All 2xx responses succeed. Network errors, HTTP 408, 429 and 5xx get one retry after 150 ms. Other statuses fail immediately. Each attempt has a 8-second deadline; response bodies are ignored. There is no later automatic replay. Redirects, private network addresses and non-HTTPS receivers are rejected. Keep processing fast: persist first, then queue any expensive work yourself.

Only publication triggers delivery. Subsequent edits/deletions do not emit events. Reconcile missed publications or updates using the public Content API. WordPress requires a plugin version carrying the optional article field; older plugins continue other notifications but cannot send complete articles.

Last updated on