Citera home
Browse documentation

Preview and publishing

Preview and publishing

Validate a destination, publish with confirmation, and verify results.

Summarize with AIChatGPTClaudePerplexity

The flow is preview → publish → poll.

POST /v1/brands/{brand_id}/publishing/previews (publishing:preview, beta)

Headers: Idempotency-Key (required). Costs 1 credit. Never publishes.

{ "content_id": "<uuid>", "integration_id": "int_wp_<uuid>",
  "options": { "destination_status": "draft" } }
{ "content_id": "<uuid>", "integration_id": "int_wp_<uuid>",
  "options": { "destination_status": "draft" } }
{ "content_id": "<uuid>", "integration_id": "int_wp_<uuid>",
  "options": { "destination_status": "draft" } }
{ "content_id": "<uuid>", "integration_id": "int_wp_<uuid>",
  "options": { "destination_status": "draft" } }

options.destination_status defaults to publish. options.include_tracking may only be true or omitted (tracking is always applied).

Checks: write capability (api.publishing.enabled) → brand → content in brand → destination in brand, channel not frozen (403), healthy (409 integration_not_ready).

Produces warnings, which make status: "invalid":

Warning code

When

incompatible_content_type

Channel cannot take this content type

content_not_ready

Status not pending_approval/approved/published; failed quality checks (unless tagged self_write); placeholder title

draft_not_supported

draft requested on a channel without draft support

Response: publishing_preview with changed_fields (title, body, excerpt, slug, tags, plus featured_media if a cover exists), preserved_fields (product_references for self-hosted WordPress with a content profile), warnings, would_publish, a content_fingerprint (SHA-256 of title, slug, excerpt, body and tags) and expires_at (1 hour). Invalid previews are stored and charged too.

POST /v1/brands/{brand_id}/publishing/operations (publishing:write, beta)

Headers: Idempotency-Key (required). Costs 3 credits. External side effect.

Body

Rules

content_id

UUID in the brand

integration_id

int_<kind>_<uuid> in the brand, healthy, not frozen

preview_id

UUID; required when the flag api.publishing.require_preview is on, and always validated if supplied

destination_status

draft | publish (required)

confirm

must be true

tracking

{ enabled?: true, campaign?: ≤64 chars }

The server re-validates everything: scope, brand, destination health and freeze, content-type compatibility (422), draft support (422), content readiness (409 invalid_state). If a preview is used, it must belong to this owner and brand, match the content and integration, be valid, be unexpired, and its fingerprint must equal the current content (409 invalid_state, so create a new preview). Then: atomic admission including the per-channel in-flight cap → content_publications row (pending) → api_publishing_operations row → enqueue with attempts: 1.

Response 202: publishing_operation (accepted) with operation_id and job_id.

Duplicate protection: the same idempotency key never creates a second operation. API publish jobs never auto-retry. WordPress adapters do a slug read-before-write for API jobs.

GET /v1/brands/{brand_id}/publishing/operations/{operation_id}

Scopes (any): jobs:read, publishing:write. Derives and persists the current state from the publication row and the queue:

status

result

Meaning

queued

null

Waiting

accepted

null

Worker is publishing

provider_accepted

verification_required

Provider accepted but no URL to verify yet

verified

published or draft

Published; external_url set; verification.method = "read_after_write"

failed

failed

See failure

unknown

null

Queue job failed or is missing without a publication outcome — needs operator triage

cancelled

null

Cancelled before publishing

awaiting_verification is true until verified, failed or cancelled.