◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#Publishing Articles Through Site Modules

Status: Available for sites on Bitrix (module sapport.connector 1.2.1) and WordPress (module sapport-connector 1.1.1). Packages for FastAPI and Nuxt have not been released publicly. Today a publication is ordered in the Sapport dashboard; there is no public server API for publishing (see the articles API, status Planned).

A site module is a small plugin that you bind to your site. Every few minutes it asks the platform whether there are jobs for it, verifies the job signature, applies the job to your CMS, and reports the outcome. This page explains how a job is structured, how the platform protects your site, and what to do about conflicts. It is meant for both the site owner and the developer of an adapter.

Access: in the dashboard, the seo.manage permission to publish and seo.view to view. Paid: no. PII: no.

#How it works

  1. You bind the site: the dashboard creates a one-time binding code, and you enter it in the module. The platform issues the site a secret (shown once).
  2. In the dashboard you choose an article and click "Publish to site via module", specifying the mode: draft or publish.
  3. The platform creates a job, signs it, and queues it for your site.
  4. The module picks up jobs (it polls every 10 minutes; the hourly heartbeat reports the number of pending jobs), verifies the signature, and applies the job.
  5. The module returns the outcome: applied with the page address, or skipped, conflict, or failed with a reason code. In the dashboard, the article and the site show the status, the page address, the record ID in your CMS, and the error.

The platform address (api_base) for the module is the domain of the binding cell: RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id. The cell is determined by the prefix of the binding code.

The full cloud-to-module protocol (binding, heartbeat, job polling, outcome, unbinding, HMAC request signing) and the error codes are described in PROTOCOL.md and ERRORS.md in the connector package. This page covers what you need to understand article publishing.

#The job

FieldTypeDescription
job_idstring (uuid)Job ID
kind"article" | "post"Source: an article or a post
action"upsert" | "unpublish"Create or update / take down from the site
source_idstring (uuid)The article ID on the platform
versionintegerThe version of this source's publication on this site
payloadobjectThe content (below)
payload_sha256stringHash of the canonical JSON of the payload
kidstringSigning key ID
sigstringEd25519 signature in base64
issued_atintegerIssue time, Unix seconds
expect_cms_idstring, optionalThe ID of the CMS record the platform expects to update; present only if the platform has already applied this source on this site

#Content (payload)

FieldTypeDescription
titlestringTitle
slugstringPage address (human-readable URL); applied only on creation
htmlstringThe article's finished, sanitized HTML
excerptstringTeaser. For an article, its meta description (for platform articles, the blog teaser comes first); for a post, an empty string
cover_urlstring | nullCover image: an absolute https address on the cell domain
cover_altstring | nullCover caption; today equal to the title
seoobjecttitle, description, canonical (always null today)
tagsarray of stringUp to 10 tags, each up to 50 characters
category_hintstring | nullSection hint
localestringArticle language
publish_mode"draft" | "publish"Requested mode
scheduled_atinteger | nullDeferred publishing; always null today
recreatetrue, optionalRecreate a record that was deleted on the site
{
  "job_id": "00000000-0000-4000-8000-000000000001",
  "kind": "article",
  "action": "upsert",
  "source_id": "11111111-1111-4111-8111-111111111111",
  "version": 1,
  "payload": {
    "title": "Пептиды: полный гид",
    "slug": "peptidy-polnyj-gid",
    "html": "<p>Текст статьи</p>",
    "excerpt": "",
    "cover_url": "https://formula-cream.pro/storage/v1/object/public/sapport-files/covers/a.webp",
    "cover_alt": "Пептиды: полный гид",
    "seo": { "title": "Пептиды", "description": "Описание", "canonical": null },
    "tags": ["пептиды", "уход"],
    "category_hint": "ingredients",
    "locale": "ru",
    "publish_mode": "draft",
    "scheduled_at": null
  },
  "payload_sha256": "bbaee6d585670f93302af42addc4d8c2cb70638d1571e4616576a492fe0d26d1",
  "kid": "test-1",
  "sig": "mHgK/Ud/fbDzwnjpKiSKtyi49Mzth+sOOWc+ti2oXYHzzu0CtX92SxKVVH3aNYfuiatMnakLIsODBhieLRHfBw==",
  "issued_at": 1790000000
}

The example comes from the package's test vectors (the test-1 key is a test key and is not used by production cells).

#Limits

WhatLimit
Article HTMLat most 2,000,000 UTF-8 bytes
The whole payload as JSONat most 3,000,000 bytes
Cover imageJPEG, PNG, or WebP, up to 5 MB
Tagsup to 10, up to 50 characters each
Signature age7 days; older is skipped expired

An article that exceeds a limit does not become a job: the dashboard shows the outcome "too large" or "empty".

Relative image and link addresses in the HTML are converted to absolute ones on the cell domain. The module downloads media only from the platform address (api_base), without redirects: a cover from a foreign CDN ends the job with the error media_foreign_origin.

#Job signature

Every job is signed with the cell's Ed25519 private key. This protects your site from a job being tampered with in transit and from jobs meant for other sites:

Canonical JSON: object keys are sorted by UTF-16 code units, recursively, without whitespace; numbers are integers only, within ±(2^53−1); nesting is no deeper than 32. Fractional numbers, lone surrogates, and duplicate keys are rejected. Test vectors and the implementation are in the connector package (the tests/_vectors/ directory).

The signing of the module's own requests to the platform (HMAC-SHA256 with the headers X-Sapport-Site, X-Sapport-Ts, X-Sapport-Nonce, X-Sapport-Sig, a ±300-second window) is a separate site-binding mechanism; it is described in the connector protocol.

#What the module does with the content

Job fieldBitrixWordPress
TitleNAMEpost_title
BodyDETAIL_TEXT (allowed-tags list)post_content (wp_kses_post)
TeaserPREVIEW_TEXTpost_excerpt
Address (slug)CODE, only on creationpost_name, only on creation
TagsTAGSTags
SectionFrom the allowed sections, by hintCategory
SEOThe element's SEO templatesYoast or Rank Math
Draft, publishACTIVEdraft, publish, future
Cover imageThe element's imagesThe post's featured image
Taking down from the siteACTIVE = NStatus draft

In any case, the module sanitizes the HTML itself, independently of the sanitizing on the platform.

#Mode ceiling

The site owner can restrict the module to "drafts only". A publish job is then applied as a draft, and an article that a person has already published on the site returns the conflict published_awaits_approval.

#Versions

#Job outcome

The module returns one outcome per job; the first recorded outcome is final and is not overwritten.

OutcomeFieldsMeaning
appliedcms_id, urlApplied; url is present for a published record
skippederrorSkipped (stale version, signature age, nothing to take down)
conflicterrorConflict with a page on the site
failederrorError

Field requirements: cms_id matches ^[A-Za-z0-9_-]{1,64}$; url is http or https only; error is up to 500 characters. The module does not send transient failures (database, network, lost lease): the job returns in 15 minutes.

#Conflict and skip codes

CodeOutcomeMeaningWhat to do
stale_versionskippedThe version is not newer than the applied oneNothing: normal for retries
expiredskippedThe signature is more than 7 days oldCreate the job again
deleted_on_siteskipped (on take-down) / conflict (on publish)The record was deleted on the siteFor publishing, "Recreate"
not_foundskippedA source that was not published here is being taken downNothing
edited_on_siteconflictThe article was edited on the site after the module wrote itUpdate manually, or revert the edit and send again
published_awaits_approvalconflictThe ceiling is "draft", but the record was already published by a personRaise the ceiling or move it manually
cms_id_mismatchconflictThe platform expects a different recordContact Sapport support
issued_in_futurefailedThe site clock is behind by more than 5 minutesFix the server time

The full list (signature, cover image, site database, section setup) is in ERRORS.md in the connector package.

#What you see in the dashboard

On the article and on the site (the "Publications" block): the title, publication mode, job status (queued, issued, applied, skipped, conflict, error), page address, CMS record ID, and error text. The job list is read by staff members with the seo.view, seo.manage, or admin.api_keys permission.

#There is no public API for this yet

The dashboard publishing routes require a staff session and are not intended for external systems. A server-side "publish" call with a key is part of the future articles API: POST /articles/{id}/publish with target: connector.

#Open questions