Publishing and Content
- Category
- publishing
- Provider
- Oddlynew
- Namespace
- content
Attach documents to public content routes, publish and schedule them pinned to a reviewed version, manage metadata, tags and authors, and handle publishing lifecycle state.
| Access | Method | Side Effect | Description |
|---|---|---|---|
| read | listContent | No | List content records in an organization. typeId matches every revision of a type family. Exact parameter shapes remain runtime truth. |
| read | getContent | No | Read one content record by id or route. Exact parameter shapes remain runtime truth. |
| read | getPublishedContentSummaries | No | Read the published summaries of up to 50 content ids in one call, as the site resolves a typed reference. Exact parameter shapes remain runtime truth. |
| read | getPublishedContentBodies | No | Read the top-level body blocks of chosen types for up to 50 content ids from each post's pinned publication. Exact parameter shapes remain runtime truth. |
| read | searchPublishedContent | No | Search the published titles the site serves, with the list filters. Exact parameter shapes remain runtime truth. |
| write | attachContent | Yes | Attach a document to a content route. Exact parameter shapes remain runtime truth. |
| write | updateContentMetadata | Yes | Update route and SEO/presentation metadata. Exact parameter shapes remain runtime truth. |
| write | publish | Yes | Publish the document's current draft. Publish first saves any edit the editor still holds as a new persisted version, then pins and publishes that version and answers with it, optionally pinned to the version, title and metadata digest a reviewer read. Exact parameter shapes remain runtime truth. |
| write | schedule | Yes | Capture and schedule the current persisted draft, pinned to what a reviewer read. Exact parameter shapes remain runtime truth. |
| write | cancelSchedule | Yes | Cancel an existing scheduled publication. Exact parameter shapes remain runtime truth. |
| write | unpublish | Yes | Return published content to a non-public state. Exact parameter shapes remain runtime truth. |
| read | listTags | No | List available content tags. Exact parameter shapes remain runtime truth. |
| write | setContentTags | Yes | Replace the tags attached to content. Exact parameter shapes remain runtime truth. |
| write | setContentAuthors | Yes | Replace the authors attached to content. Exact parameter shapes remain runtime truth. |
| write | trashContent | Yes | Move content to the publishing trash state. Exact parameter shapes remain runtime truth. |
| write | restoreContent | Yes | Restore trashed content. Exact parameter shapes remain runtime truth. |
| write | claimScheduleRepairNotifications | Yes | Claim bounded schedule-repair notifications for handling. Exact parameter shapes remain runtime truth. |
| write | acknowledgeScheduleRepairNotifications | Yes | Acknowledge claimed schedule-repair notifications. Exact parameter shapes remain runtime truth. |
- Method
- listContent
- Source Kind
- content record in an organization
Start here when you need a concrete connected source or content record.
Publishing state is separate from the underlying document. Content routes expose only what the publishing projection allows; exact runtime signatures remain authoritative for current metadata fields and scheduling receipts.
Have the draft reviewed, then publish or schedule with an expected pin (version, title, metadata digest) so the action is bound to the state that was reviewed. Repair notifications exist for schedule convergence failures.
Content calls authorize the concrete organization or content record at execution time. Publishing writes require the corresponding delegated write authority; public HTTP reads remain a separate read surface.
The publishing projection is built on Oddlynew documents. Media and downloadable assets are owned by the Files surface rather than by content itself.
Publishing and Content
What it does
The content namespace owns the site-facing projection of a document: its route, publication lifecycle, audience, SEO fields, feature image, tags, and authors. Documents own the title, body, settings, collaborative draft state, and version history. Publishing therefore has two halves: write and review the document with documents.*, then attach it and control how it appears on a site with content.*. Sites and services read what you publish through the read-only HTTP API on api.oddlynew.com with a key that carries the organization's content permissions. The HTTP surface is described at https://oddlynew.com/content-api.md and in the OpenAPI document.
For content modeling, start with Document types: choosing metadata, blocks and publishing fields. The type defines both document metadata and body blocks. Use metadata for stable whole-item facts and blocks for ordered or contextual material. The content.updateContentMetadata operation owns publishing fields such as routes and SEO; use documents.patchDocumentEnvelope for schema-defined document metadata. Do not infer API filters or visibility guarantees from the word metadata.
Which organization
Organization-scoped methods take organizationId explicitly: listContent, listTags, attachContent, getContent by path, and the schedule-repair methods. Methods that take a contentId infer the organization from the content item and reject an extra organizationId. Every call is checked live against your delegated MCP scopes, your role permissions in that organization, and the organization's content entitlement, so a call that is not allowed fails with an actionable error rather than a silent no-op.
Finding content
content.listContent({ organizationId }) returns bounded summaries and accepts optional filters: typeId, status for the lifecycle state (draft, published, scheduled, trashed), and authorId. Page with limit and cursor. typeId accepts any revision id of a document type and matches items of every revision in that type's lineage, so revising a type does not hide the posts published on the old revision. This differs from documents.listDocuments and documents.search, whose typeIds match exact ids only.
content.getContent reads one item by exactly one of { contentId } or { organizationId, path } and returns its lifecycle, SEO, tags, authors, and persisted-version facts. It never returns the body; read that with documents.getDocument.
Read calls take no operationId: omit it, and the read mints its own UUIDv7 and echoes it as operationId. Pass an echoed value back only to replay an exact retry of that same call.
Reading what the site serves
listContent and getContent show the catalog as an editor sees it. Two reads answer with what the published site serves instead, as an anonymous visitor sees it: the same reads as the HTTP summaries and bodies routes.
content.getPublishedContentSummaries({ organizationId, ids, typeId })
content.getPublishedContentBodies({ organizationId, ids, blocks, typeId })getPublishedContentSummaries resolves typed references, such as the recipe ids in a course's metadata. getPublishedContentBodies reads chosen body blocks for a whole grid of items, such as each recipe's ingredients, instead of one documents.getDocument per item.
ids: 1 to 50 lower-case UUIDs per call, duplicates counted. Each distinct id is answered once, at its first position, in request order; walk your own id list to restore an authored order. Nothing is expanded recursively.
typeId: optional, an existing revision id of the expected type family. It matches the whole family, so a published item outside that lineage answers
wrong_type; a typeId that does not resolve turns every otherwise available item intowrong_type.blocks (bodies only): 1 to 10 block types, each a top-level block's
_typeexactly as the document type names it.Outcomes: each item is
availableor says why not:missing(an unknown id, another organization's, trashed, never published, unpublished, or scheduled and not yet due),restricted(published behind required roles; a reference grants no access, whatever roles the caller holds) orwrong_type. When several apply, the first in that order wins, so a restricted item never reveals whether its type matches.Answer shape: both return
{ operationId, items }. An available summary is{ id, outcome, item }, whereitemis the published list item: title, slug, status, version, excerpt, feature image, tags, metadata, locale and publish dates. An available body is{ id, outcome, typeId, revisionId, blocks }with every top-level block of the asked types, whole and in body order, or an empty list. Other outcomes carry onlyidandoutcome. There is nomode,basisorassetsas on HTTP.Media:
oddlynew-asset:references stay as written; resolve them with thefilesnamespace.Size: the served blocks of one bodies call add up to at most 114688 bytes. Above that the call fails with
INVALID_INPUTinstead of truncating; ask for fewer ids or block types. The HTTP bodies route serves up to 7.6 MB per request.Freshness: each id resolves to its post's current pinned publication, so a republished target shows its new state while the referencing item's id list stays frozen until that item is republished. There is no draft mode here; read drafts with
documents.getDocument.
content.searchPublishedContent({ organizationId, query }) is the same read as the HTTP search route: published items matched by a case-insensitive substring of the published title, ordered by last publish, with role-gated posts left out. Optional filters are typeId (lineage-wide), tagSlugs (1 to 10, all must match), locale (exact), featured, and referenceField with referenceId and typeId, which finds the items whose published metadata references that id. Page with limit (1 to 50, default 20) and cursor. An unpublished title edit is neither found nor shown.
Attaching a document
content.attachContent({ organizationId, documentId, path, ... }) establishes the route for an existing document and its first content metadata. The path is the site path, for example /recipes/paella. Optional fields at attach time are tags (existing tag slugs), authorIds, featured, and requiredRoles (the audience, below). The receipt carries contentId, path, title, typeId and status. content.updateContentMetadata({ contentId, ... }) changes only publishing metadata later: route, audience, listing SEO, or feature image. Omitted fields stay unchanged. Nothing in the content namespace modifies the document's title, body, or settings.
Audience. The requiredRoles list decides who can read a content item. Attaching a document exposes nothing by itself: new content starts as a draft, and only published content reaches readers.
Empty
requiredRoles, the default: anyone can read the published item, and it appears in list reads.Non-empty
requiredRoles: for readers your own application authenticates, who must hold the listed role keys. The public read API never returns role-gated content; it is served only by an application that embeds the content workflows and checks the reader's roles itself.
Change the audience with content.updateContentMetadata({ contentId, requiredRoles }). The change takes effect at once, without a republish: the body is frozen at publish, the audience is live. Once an item has been published with no required roles, its required roles can no longer change. The audience belongs to the content item, not the organization; a private organization serves public content to a site key that is a member with content read permission.
Publish and schedule
Publishing saves the latest edit, then freezes that version into a publication revision. Later edits to the document do not change the publication until you publish again.
content.publish({ contentId, operationId })
content.publish({ contentId, operationId, expected: { version, title, metadataDigest } })Without expected, the head is published as it stands. With expected, the publish is pinned to what a reviewer read: version is the draft's version.number (the first persisted version is 1), title is the title the reviewer read, and metadataDigest is the draft's version.metadataDigest when the read showed it. A site in draft mode shows all three on the detail read; documents.getDocument shows the version number and title. Use the pin whenever a person approved a specific state. The receipt carries path, typeId, artifactId, versionId, version.number (the published version number) and publishedAt.
A stale pin fails with ContentVersionMismatch. The error names the expected pin, the current head, and one of two reasons:
moved: a newer version, title, or metadata exists. Re-read the draft, have it reviewed again, and pin its current version.unpersisted_edit: the editor holds an unsaved edit while the persisted head still matches the pin. Wait for the save, then retry with the same pin.ContentSavePendingis a separate publish error, not one of these reasons: it publishes nothing and suppliesretryAt; reasonblockedrequires a document change androlled_backrequires a fresh read.
content.schedule({ contentId, operationId, publishAt, expected }) captures the persisted draft now and publishes it at publishAt (an ISO date-time). expected is required here and has the same shape and the same failure as on publish. content.cancelSchedule({ contentId }) returns scheduled content to draft. content.unpublish({ contentId }) removes content from the public state while keeping the document and its metadata; it is idempotent. Publishing with no required roles is a one-way door: after that, unpublish, schedule and trashContent are refused for the item, and deleting its document is the way to retract it.
operationId is the retry identity of a publish or schedule. Generate it as a UUIDv7 on your own host before the first attempt and keep the literal; call ids.mintOperationIds only if your host cannot generate one, in a run of its own. Reuse the same value only when retrying the same call after an ambiguous response. A retry with the same operationId and the same pin replays the captured publication even if the head moved on since.
Tags and authors
content.listTags({ organizationId }) returns the organization's tag catalog ordered by slug; page with cursor while hasMore is true. content.setContentTags({ contentId, tagSlugs }) replaces all tags with existing slugs; an empty array removes them. content.setContentAuthors({ contentId, authorIds }) replaces the authors with user ids; an empty array removes them. Tags and authors are frozen into the publication at publish time; a change after publish reaches the public API on the next publish.
Trash and restore
content.trashContent({ contentId }) moves content to recoverable trash and removes public visibility without purging history. content.restoreContent({ contentId }) returns it to draft.
Schedule repair notices
If a scheduled publication could not run, a notice is addressed to the person who scheduled it. content.claimScheduleRepairNotifications({ organizationId }) claims a bounded batch of such notices; surface every returned notice to the user, then call content.acknowledgeScheduleRepairNotifications({ organizationId, claimToken }). Stale or replayed claim tokens acknowledge zero rows.
Images
Upload images with the files namespace and reference them from the document or from the feature image field. The content namespace does not upload files.
Reading published content from a site
Published content is read over HTTPS from api.oddlynew.com with an organization API key (created under Organization > Integrations > Content API) that carries content:read. Sites should use the content routes rather than raw document routes, because the content projection is the stable public contract.
Every HTTP request carries the key as a bearer token and a required Idempotency-Key header holding a fresh UUIDv7. Reuse a key only when retrying the same request after an ambiguous response.
GET /v1/orgs/{orgId}/content?typeId=...&tags=paella&tags=rice&locale=es&limit=20
GET /v1/orgs/{orgId}/content/search?q=paella
GET /v1/orgs/{orgId}/content/%2Frecipes%2FpaellaSlugs are leading-slash site paths and must be percent-encoded into one path segment, as above. An unencoded slash changes the route and answers 404. The list filters are typeId (any revision id of the type, lineage-wide), tags (repeated, all must match, at most 10), locale (exact), limit (1 to 50) and cursor. Search takes the same filters plus q, a 2 to 200 character substring of the published title, and keeps the list ordering.
Detail response: a detail read returns mode, revisionId, status, scheduledFor, post, version, meta, tags, authors, and assets. The ordered document body is version.content: render those blocks in order. Image and audio blocks keep a stable oddlynew-asset:<fileId> value in src; remove that prefix and use the file id to look up assets[fileId]. Each descriptor contains url, mimeType, width, and height; dimensions are currently always null. List response: a list read returns { mode, items, nextCursor, hasMore, assets }. Page with nextCursor while hasMore is true. The list-level assets map contains feature images needed by the items on that page; body assets belong to the detail response.
Status. Every item and the detail response carry status: draft, scheduled, published, or published_with_newer_draft. Outside draft mode every item is published. This is a different vocabulary from the MCP lifecycle status (draft, published, scheduled, trashed): trashed content never appears on the read API, and published_with_newer_draft is a read-side derivation that says an edit has persisted since the last publish.
Caching: content changes attempt an awaited purge of affected platform-cache entries; list, search and summary responses otherwise use 60 seconds fresh plus 15 seconds stale, while production detail responses use one day fresh plus one day stale under a revision-stamped key. Search and detail-stamp reads also use a database cache with a 60-second fresh and 15-second stale window, so cache windows can stack and fresh search results are not guaranteed on the next read or within one minute. Detail carries a strong ETag; send it as If-None-Match and an unchanged post answers 304. Assets: the public asset map resolves image and audio blocks. Attachment and HTML-file references are intentionally omitted, so a headless site must not assume every oddlynew-asset: reference has a public URL. In production, returned URLs are permanent immutable public capabilities backed by Files; non-production environments may return expiring capabilities. Cache the document data and refresh it conditionally instead of persisting asset URLs separately. The API runtime schema and OpenAPI description remain authoritative for exact fields and parameter shapes.
Draft mode on your site
Draft mode shows an editor the site as it would look after publishing everything: drafts, scheduled items, and published items with their live title, tags and metadata. It is a property of the site's request, switched on by one request header, and it needs a site key that carries content:preview as well as content:read.
The switch:
Your site decides who is an editor, from its own session and its own rule. For those requests, send
X-Oddlynew-Draft: trueon every content read: list, search and detail. Only the exact valuetrueswitches draft mode on; any other value, or no header, is a published read. Draft mode is a whole-site state, not a per-page one; a list without the header shows the published catalog while the detail shows a draft.Render draft content only when the response says
mode: "draft". Showstatusandversion.numberto the editor, because those are what a publisher pins. Answer draft pages withCache-Control: private, no-store, mark them noindex, and never write one into a shared cache. The API itself answersno-storewith no ETag.Handle every refusal.
403witherror: "insufficient_scope"means the key does not carrycontent:preview: serve the published read for that request and tell the editor draft mode is unavailable; never serve a published page under a draft banner.404: no content at that path, or it has been trashed.429or503: serve a retryable "draft unavailable" page. Nothing is minted and nothing expires; your session decides when draft mode ends.
Every draft read is uncached and metered, so keep draft mode to authenticated editors and never switch it on for anonymous visitors.
In draft mode the detail read serves the document's live editor state: the body, metadata, settings and checksum are what the editor sees now, and the response says so with basis: "live". The pin stays the persisted head: version.id, version.number, version.title and version.metadataDigest are the persisted head's, so a publisher pins what the next publish freezes, and the top-level title is the live title a page shows. When the document's journal cannot be read, the detail falls back to the persisted head with basis: "persisted". For an item whose status is published, the revision identity and the compared head fields are the publication's, while the body and the listing fields stay live. An autosave between the two reads is re-read once; only a remaining mismatch answers 503.