Document reference
Every document type field by field, including the rich-text format and AI operation fields.
On this page
Every document type Tidenda exposes, field by field, in the shape you
meet it in snapshots, webhook payloads, GET /v1/documents and the
body of POST /v1/documents. For integrators parsing or producing
documents. For the model behind them, read
How Tidenda works first.
{
"id": "3b2c1a40-f128-4cb6-b8c2-19a4d8d7e2f5",
"type": "core/newswire",
"title": [
{ "id": "6f0e9b1c-...", "class": "text", "type": "core/text",
"children": [{ "text": "Cabinet reshuffle expected ahead of Thursday vote" }] }
],
"meta": {
"priority": 3,
"origin": "wire",
"source": "acme-wire",
"sourceItemId": "item-20260706-0142",
"status": "usable",
"version": 1,
"workflowId": "...",
"workflowVersion": 1,
"publishedVersion": 1
},
"content": {
"text": [
{ "id": "9a41d2e0-...", "class": "text", "type": "core/text",
"children": [{ "text": "The prime minister is expected to name three new ministers." }] }
]
},
"links": []
}
Every document has the same envelope: id, type, title, meta,
content and links. Types differ in what meta and content hold.
Only core/newswire, core/planning-item and core/article can be
pushed. A pushed body must follow its type's shape and rules on this
page exactly: a field not listed for the type is refused, not ignored.
| Type | What it is | Readable with GET /v1/documents |
|---|---|---|
core/article |
A story's text | yes |
core/planning-item |
A planned story and its assignments | no |
core/newswire |
An incoming wire, press release or media item | no |
core/image |
An image's metadata, rights and provenance | yes |
core/audio |
A recording, its transcript and signatures | yes |
core/video |
A video: an upload or a rendered video | yes |
core/video-project |
The storyboard a video is made from | no |
core/tag |
A section or story label | yes |
core/publisher-site |
The public site's configuration | yes |
core/brand |
The organisation's logos, colours and type | yes |
core/workflow |
Statuses and transitions for one type | no |
core/ai-operation |
A recipe for one AI task | no |
Types for members, private member data and organisation settings are not part of the integration surface and are not described here.
The shared envelope
{
"id": "string", // unique in the organisation; see Ids below
"type": "core/article",
"title": [ /* rich text; a string is accepted on push */ ],
"meta": {
"priority": 3, // 1-6, 1 most urgent. Required.
"visibility": "internal", // optional: "internal" | "public"
"time": [ // optional; each type says which entries it needs
{ "type": "target", "time": "2026-06-15T09:00:00Z", "timeZone": "Europe/Stockholm" },
{ "type": "startdate", "time": "2026-06-15", "timeZone": "Europe/Stockholm" }
],
"lang": "sv", // optional language code
// Set by the platform. Leave unset when pushing: a pushed
// document starts at "draft", and any other status is refused.
"status": "draft", // current workflow status
"version": 1, // current version, integer from 1
"workflowId": "...", // the workflow the document follows
"workflowVersion": 1, // the workflow's version at binding; for reference only
"publishedVersion": 3, // present only while a version is published
"aiGenerated": true, // optional: the reader AI label; see AI disclosure below
"publishedWithoutReview": { // optional, set by the platform; see AI disclosure below
"at": "2026-06-15T09:00:00Z",
"operationRef": "translate-to-swedish"
}
},
"content": { /* type-specific */ },
"links": [
{ "type": "core/planning-item", "rel": "planning", "uri": "doc:<document id>", "title": "Reshuffle" }
],
"history": [ /* private; see Private fields */ ],
"internal": { /* private; see Private fields */ }
}
Time entries. startdate and enddate take a date (YYYY-MM-DD);
start, end, target and slot take a full ISO 8601 date-time.
Every entry carries an IANA timeZone.
Statuses. draft, usable, withheld and cancelled mean the
same in every workflow; other values are statuses an organisation
defined. Entering usable or withheld makes a signed snapshot. See
Workflows.
AI disclosure. Two meta fields say how AI was involved, and both
are part of the published snapshot:
aiGeneratedis the reader AI label: whentrue, readers are to be told the document was produced with AI help. The platform sets it the first time AI changes the document's text; editors may turn it off or on again, and each change is logged in the AI record (see Private fields). A pushed document may set it. Oncore/imageandcore/audiothe field of the same name has its own meaning, described there.publishedWithoutReviewis present when an AI workflow step moved the document intousableorwithheldwith no person reviewing the AI's answer:at, when the step published it, andoperationRef, the operation's reference name. While it is present,aiGeneratedistrueand cannot be turned off. The platform removes it when a member next moves the document intousableorwithheld. Only the platform writes it; a pushed body that carries it has it dropped. See Publishing without review.
Ids. A document pushed through the API has a UUID chosen by the
caller. Some documents the platform creates use other forms: tags are
tag-<uuid>, the site configuration is pub-<organisation id>.
Treat ids as opaque strings.
Links. Each link has optional type, rel, uri and title.
title is the target's title when the link was written; read the
target for the current one. What uri holds depends on the target:
| Target | uri |
Common rel values |
|---|---|---|
| Another document | doc:<document id> |
planning, assignment, source |
| A tag | core/tag:<tag id> |
section, story |
| A member | core://user/<uuid>, with type core/user |
creator, author, assignee |
| An external page | A URL | any |
A link to another document is stored as doc:<document id>. A pushed
document may send the bare id instead; it is stored with the prefix.
Documents stored before the prefix was the rule may still hold the
bare id, so strip a doc: prefix before matching a document id.
Private fields
history and internal stay inside the platform. Neither appears in
a snapshot, a webhook payload or a GET /v1/documents response, and
neither is sent to a model by an AI operation.
historyis the append-only record of status changes: who moved the document, from which status to which, at what version, and the snapshot signature where one was made. A pushed document leaves it out or sends it empty.internalholds editorial working data in the document's own shape:metafor data about it,linksfor relations. On every type,internal.metaholds the state of an AI step: whether one is running, which locks the document, and the note left when one fails. It also holds the AI record, below. Only the platform writes the AI step state and the AI record; a pushed body that carries them has them dropped.core/article,core/image,core/videoandcore/video-projectadd their own fields tointernal.meta.
The AI record. internal.meta.aiUse lists every time AI changed
the document's text, oldest first. internal.meta.aiLabelLog lists
every time a member showed or hid the reader AI label. Both are
optional and only grow; a document without them has no recorded AI
use.
"internal": {
"meta": {
"aiUse": [
{
"id": "...",
"at": "2026-06-15T08:40:00Z",
"kind": "translation", // draft-from-wires | translation | summary
// | rewrite | suggestion | video-script
// | post-text | other
"operationRef": "translate-to-swedish", // optional: the operation's reference name
"invocationId": "...", // optional: the run in the invocation log
"by": { "kind": "system", "system": "ai-op:<operation id>" }
// or { "kind": "user", "userId": "...", "userEmail": "..." }
}
],
"aiLabelLog": [
{
"at": "2026-06-15T09:10:00Z",
"by": { "kind": "user", "userId": "...", "userEmail": "..." },
"shown": false
}
]
}
}
by has the same shape as a history entry's actor: the member who set
the AI to work, or the part of the platform that ran it on its own
(ai-op:<operation id> for a workflow step).
Rich text
Titles, article bodies, captions, descriptions and other prose fields
are rich text: an array of nodes, never a plain string, in every
snapshot and response. On push, a plain string is accepted and stored
as one core/text paragraph.
There are two kinds of node.
Elements have children:
{
"id": "9a41d2e0-...", // stable while the element exists
"class": "text", // "text" | "block" | "inline" | "void"
"type": "core/text", // see the list below
"properties": { "role": "h2" }, // optional, type-specific
"children": [ /* elements or text leaves */ ]
}
Text leaves hold the characters, with a boolean for each mark:
{ "text": "bold bit", "base/bold": true, "base/italic": true }
The marks are base/bold, base/italic, base/underline,
base/strikethrough and base/code.
The element types:
| Type | What it is | Children and properties |
|---|---|---|
core/text |
A paragraph or heading | Text leaves and base/link. properties.role: absent or paragraph, preamble (lead-in), tagline (teaser for listings, not body text), h1 to h4. |
base/link |
An inline link | Text leaves. properties: href, title, target. |
base/unordered-list, base/ordered-list |
A list | <list type>/list-item elements, each holding text. |
base/blockquote |
A quotation | base/blockquote/body, optional base/blockquote/attribution and base/blockquote/source, and a base/blockquote/footer void on quotes taken from audio. An audio quote's properties: audioId, speakerId, speakerLabel, startMs, endMs, segmentIds (comma-separated), audioContentHash, originalTextHash. |
base/code-block |
Preformatted text | One base/code-block/body. |
base/table |
A table | base/table/row elements holding base/table/row/cell elements. |
base/image |
An image from the archive | A base/image/content void, base/image/caption, base/image/byline. properties: imageId (the core/image id), src, title, width, height, and on some mimeType and size. |
base/audio-embed |
A recording | A base/audio-embed/content void and base/audio-embed/caption. properties: audioId (the core/audio id), src, title, durationMs. |
A void element carries one empty text leaf and no content. src
values are paths on the editing app, not public URLs: resolve media
through its document id. Plugins can add element types, so skip any
type you do not recognise rather than failing.
A body with a heading, a paragraph with a bold run, and a list:
[
{ "id": "a1", "class": "text", "type": "core/text", "properties": { "role": "h2" },
"children": [{ "text": "What happens next" }] },
{ "id": "a2", "class": "text", "type": "core/text",
"children": [{ "text": "The vote is " }, { "text": "on Thursday", "base/bold": true }, { "text": "." }] },
{ "id": "a3", "class": "text", "type": "base/unordered-list",
"children": [
{ "id": "a4", "class": "text", "type": "base/unordered-list/list-item", "children": [{ "text": "Finance" }] },
{ "id": "a5", "class": "text", "type": "base/unordered-list/list-item", "children": [{ "text": "Defence" }] }
] }
]
To get plain text for indexing or a notification, walk the tree:
function flatten(nodes) {
if (!Array.isArray(nodes)) return ''
return nodes.map((n) =>
typeof n.text === 'string' ? n.text
: Array.isArray(n.children) ? flatten(n.children)
: ''
).join('')
}
To render HTML, map each element type (and a core/text element's
role) to a tag, and each mark on a leaf to an inline element.
core/article
The text of a story, written against an assignment on a planning item.
{
"id": "...",
"type": "core/article",
"title": [ /* rich text: the headline */ ],
"meta": {
// shared meta, plus:
"time": [
{ "type": "target", "time": "2026-06-15T09:00:00Z", "timeZone": "Europe/Stockholm" }
],
"slug": "cabinet-reshuffle", // optional; set on first publish
"slugAliases": ["reshuffle-expected"], // optional; earlier slugs
"section": "sec-news" // optional; a site section id
},
"content": {
"text": [ /* rich text: the body */ ]
},
"links": [
{ "type": "core/planning-item", "rel": "planning", "uri": "doc:<planning id>" },
{ "type": "core/planning-assignment", "rel": "assignment", "uri": "doc:<assignment id>" },
{ "type": "core/newswire", "rel": "source", "uri": "doc:<wire id>" },
{ "type": "core/tag", "rel": "story", "uri": "core/tag:tag-...", "title": "Election 2026" },
{ "type": "core/user", "rel": "author", "uri": "core://user/...", "title": "Anna Andersson" }
],
"internal": {
"meta": {
"annotations": [ /* review findings, below */ ],
"allowedSpelling": [ /* allowed words, below */ ],
"rightsAcknowledgements": [ /* published despite image restrictions, below */ ]
}
}
}
meta.timeneeds atargetor aslotentry.linksneeds acore/planning-itemlink and acore/planning-assignmentlink.sourcelinks name the wires the article was written from.meta.slugis minted when the article first reachesusableand then stays. When it changes, the old slug moves tometa.slugAliases, which only grows.meta.sectionis copied from the planning item when the article is created.internal.meta.annotationsare review findings on passages of the text:id,kind(finding),anchor(start,end,quote),category,title,note, optionalreplacement,status(open,applied,skipped), optionaloperationId,createdAtandcreatedBy(uri,title).internal.meta.allowedSpellinglists words the spellchecker flags but this article allows:text,uriandtitleof the member who allowed it, andallowedAt.internal.meta.rightsAcknowledgementsrecords each time a member published the article although it embeds images with usage restrictions:at,by(kinduser,userId,userEmail), thestatusmoved into, andimageIds, the images listed to the member. See Images and audio.
core/planning-item
A planned story: what will be covered, when, and by which media.
{
"id": "...",
"type": "core/planning-item",
"title": [ /* rich text */ ],
"meta": {
// shared meta, plus:
"time": [
{ "type": "startdate", "time": "2026-06-15", "timeZone": "Europe/Stockholm" },
{ "type": "enddate", "time": "2026-06-16", "timeZone": "Europe/Stockholm" }
]
},
"content": {
"slugline": "reshuffle",
"description": [ /* rich text */ ],
"section": "sec-news", // site section id; "" for none
"assignments": [
{
"id": "...",
"type": "core/planning-assignment",
"title": [ /* rich text */ ],
"meta": {
"type": "text", // text | flash | image | graphic | video | audio
"time": [ // optional planning hint
{ "type": "target", "time": "2026-06-15T09:00:00Z", "timeZone": "Europe/Stockholm" }
]
},
"content": {
"slugline": "reshuffle-text",
"description": [ /* rich text */ ]
},
"links": [
{ "type": "core/user", "rel": "assignee", "uri": "core://user/...", "title": "Anna Andersson" }
]
}
]
},
"links": []
}
meta.timeneeds both astartdateand anenddate.- Assignments live inside the planning item, not as separate
documents. Each keeps its own
id, and its deliverable has the same id: acore/video-projectfor avideoassignment, acore/articlefor every other type. The deliverable links back withrel=planningandrel=assignment. - An assignment's
typeis fixed once its deliverable exists. - An assignment's
metaholds onlytypeandtime. Itstimeis a hint; once the deliverable exists, its owntargetrules. content.sectionapplies to every article made from the planning item.
core/newswire
An item from a wire service, a press release or a media drop, stored as it arrived. The example at the top of this page is a complete newswire.
{
"type": "core/newswire",
"meta": {
// shared meta, plus:
"origin": "wire", // wire | pressrelease | media
"source": "acme-wire", // the feed's name
"sourceItemId": "item-20260706-0142", // the feed's id for the item
"iptcCategory": "http://cv.iptc.org/newscodes/mediatopic/11000000", // optional
"sourceUrl": "https://acme.example/news/0142", // optional: the original
"sourceRef": "6f1c0a52-...", // optional: the registered source
"byline": "Jane Doe" // optional: the item's author line
},
"content": {
"text": [ /* rich text: the body */ ],
"attachments": [ // optional
{ "url": "https://acme.example/0142.pdf", "type": "application/pdf", "name": "Report" }
]
}
}
sourceandsourceItemIdare required and together identify the item: a second push of the same pair returns the existing wire.iptcCategoryis one of the 17 top-level IPTC Media Topic URIs. It may be added shortly after the wire arrives, and is absent when no category was found.sourceUrlis the link to the original item at its source, an absolutehttporhttpsaddress.sourceRefis the id of the registered source the platform fetched the wire from (see Sources). It is set by the platform only: a pushed wire that carries it has it removed.bylineis the author line the source gave the item, as sent.attachmentsare files sent with the item, such as a PDF, an image or an audio clip, kept as links:url(absolutehttporhttps),type(the file's media type, such asimage/jpeg) and an optionalnameto show.- A wire from a registered source is updated when its item changes:
meta.versiongoes up and the update is recorded inhistory. See Updated items. - See Wires for how wires are used, and Pushing documents in to send one.
core/image
An image in the organisation's archive. The document holds the metadata; the bytes are fetched separately. See Media for uploads, variants and the rights model.
{
"type": "core/image",
"title": [ /* rich text */ ],
"meta": {
"priority": 3, // defaults to 3
// status, version, workflowId, workflowVersion, publishedVersion,
// visibility, time and lang as in the shared envelope
// Identity, set when the upload completes
"contentHash": "9f86d081...", // SHA-256 of the original, lowercase hex
"originalFormat": "image/jpeg",
"originalSize": 4823651, // bytes
"dimensions": { "width": 4032, "height": 3024 },
// Authorship
"copyright": "(C) 2026 Jane Doe",
"photographer": "Jane Doe",
"credit": "Acme Photo",
"jobId": "ACME-2026-04127",
// Rights
"restrictions": true, // set by the platform; see below
"instructions": "Free for editorial use during Q2 2026.",
"usageTerms": "Editorial use only. No derivatives.",
"usage": { // optional
"social": "forbidden", // allowed | forbidden | unknown (default)
"paid": "unknown",
"modify": "allowed", // crops, overlays, text on top
"expiresAt": "2026-12-31T23:59:59Z", // optional; ISO 8601
"territories": ["SE", "NO"] // optional; ISO 3166-1 alpha-2
},
// Framing
"focalRegion": { "x": 0.25, "y": 0.1, "w": 0.5, "h": 0.6 }, // optional
// Capture, when the file carried it
"capturedAt": "2026-04-15T11:23:54Z",
"capturedAtTimezone": "Europe/Stockholm",
"geo": { "lat": 59.3293, "lon": 18.0686, "altitude": 12 }, // altitude optional
"camera": { "make": "Canon", "model": "EOS R5", "lens": "RF24-70mm" },
// Provenance
"c2paManifest": { "present": true, "issuer": "Adobe",
"claimGenerator": "Adobe Photoshop 2026",
"validatedAt": "2026-04-15T11:30:00Z" },
"aiGenerated": false, // true, false, or absent for unknown
"signature": { "alg": "ed25519", "keyId": "...", "sig": "<base64>" },
// Processing
"processingStatus": "ready", // pending | ready | failed
"processingError": "...", // present when failed
"variants": ["original", "thumb", "preview", "delivery"]
},
"content": {
"altText": "Crowd lines the parade route under a cloudy sky.",
"caption": [ /* rich text */ ],
"rights": [ /* rich text: longer usage notes */ ]
},
"links": [
{ "type": "core/planning-item", "rel": "planning", "uri": "doc:<planning id>" },
{ "type": "core/planning-assignment", "rel": "assignment", "uri": "doc:<assignment id>" }
]
}
restrictionsistruewheninstructionsorusageTermsis set,aiGeneratedistrue, ausagescope isforbidden, orusage.expiresAthas passed. It depends on the clock, so the value in a payload is the one at the time it was sent. Show a warning wherever the image is used.usagerecords what the image may be used for: on social platforms, in paid placement, and modified (modify: crops, overlays, text on top). Each scope isallowed,forbiddenorunknown; an absentusageor scope isunknown.focalRegionis the part of the image that must stay in frame when it is cropped to another shape. A region is a rectangle normalised to the image:xandyare its top-left corner andwandhits size, each from 0 to 1, and it lies within the image (x + w <= 1,y + h <= 1).- People shown in the image who need protecting are flagged in the
private
internal.meta.persons, never published:[{ "region": { "x", "y", "w", "h" }, "minor": true, "victim": true, "anonymity": true }], every field optional. A person without a region applies to the whole image. aiGeneratedhas three states. Absent means unknown, notfalse.signaturesigns the original bytes; see Signatures.- A
readyimage always hascontentHash,originalFormat,originalSize,dimensionsand adeliveryvariant.
core/audio
A recording, with its transcript and signatures. See Media for the pipeline and the rights model.
{
"type": "core/audio",
"title": [ /* rich text */ ],
"meta": {
"priority": 3, // defaults to 3
// workflow and envelope fields as for core/image
"contentHash": "b1946ac9...",
"originalFormat": "audio/mpeg",
"originalSize": 47182360,
"durationMs": 2734120,
"languages": ["sv", "en"], // from the transcript; [] when none found
"copyright": "(C) 2026 Tidningen AB",
"recordist": "Anna Andersson",
"credit": "Tidningen AB",
"jobId": "...",
"restrictions": true, // as for core/image
"instructions": "Attribute every quote.",
"usageTerms": "Editorial use only.",
"usage": { "social": "allowed" }, // optional; as for core/image
"capturedAt": "2026-04-15T09:00:00Z",
"capturedAtTimezone": "Europe/Stockholm",
"capturedAtLocation": "City Hall press conference",
"c2paManifest": { "present": false },
"aiGenerated": false,
"signature": { "alg": "ed25519", "keyId": "...", "sig": "<base64>" },
"processingStatus": "ready", // pending | transcribing | diarising | ready | failed
"processingError": "...", // present when failed
// processingPhase, diarizeError, diarizeAction and diarizeHint
// describe transcription progress and settings, for display only
"variants": ["original", "waveform"]
},
"content": {
"altText": "Interview on the housing crisis.",
"caption": [ /* rich text */ ],
"rights": [ /* rich text */ ],
"speakers": [
{ "id": "A", "displayName": "Anna Andersson",
"verifiedAt": "2026-04-15T14:00:00Z", "verifiedBy": "..." },
{ "id": "B" }
],
"segments": [
{
"id": "...",
"startMs": 12340,
"endMs": 18720,
"speakerId": "A",
"text": "It was late that night when the announcement came.",
"language": "sv",
"words": [ { "startMs": 12340, "endMs": 12610, "text": "It", "probability": 0.98 } ],
"verifiedAt": "2026-04-15T14:03:22Z",
"verifiedBy": "...",
"signedTextHash": "3d2f8a...", // SHA-256 of the signed text
"sig": { "alg": "ed25519", "keyId": "...", "sig": "<base64>" }
}
],
"groups": [ { "id": "...", "label": "Opening", "segmentIds": ["..."] } ]
},
"links": []
}
meta.signaturesigns the original bytes. A segment'ssigsigns its text when an editor verifies it; comparesignedTextHashwith the SHA-256 of the current text to see whether it changed since. See Signatures.- A
readyrecording always hascontentHash,originalFormat,originalSize,durationMsand theoriginalvariant. Its segments may still be empty.
core/video
A video: a clip someone uploaded, or a video rendered from a
core/video-project. Same lifecycle and rights model as core/image
and core/audio.
{
"type": "core/video",
"title": [ /* rich text */ ],
"meta": {
"priority": 3, // defaults to 3
// workflow and envelope fields as for core/image
"origin": "render", // upload | render
"contentHash": "...", // uploads: SHA-256 of the original
"originalFormat": "video/mp4", // uploads
"originalSize": 48211760, // uploads
"durationMs": 31200,
"width": 1080,
"height": 1920,
"hasAudio": true,
"copyright": "(C) 2026 Tidningen AB",
"videographer": "Anna Andersson",
"credit": "Tidningen AB",
"restrictions": false, // as for core/image
"instructions": "...",
"usageTerms": "...",
"usage": { "social": "allowed" }, // as for core/image
"capturedAt": "2026-04-15T09:00:00Z",
"capturedAtTimezone": "Europe/Stockholm",
// Renders: what the video was made from
"source": {
"projectId": "...", // the core/video-project id
"projectVersion": 4,
"specHash": "5e884898...", // SHA-256 of the render spec
"brandVersion": 2 // optional: the core/brand version used
},
// What AI did, as disclosed to viewers
"aiDisclosure": {
"voice": { "model": "...", "voiceId": "...", "licence": "...", "watermark": true },
"scriptModel": "...", // the model that drafted the script, when AI did
"labelText": "Script drafted with AI"
},
"c2paManifest": { "present": false },
"aiGenerated": false,
"signature": { "alg": "ed25519", "keyId": "...", "sig": "<base64>" }, // uploads
"withdrawn": { "at": "2026-10-30T08:00:00Z", "reason": "Licence expired" },
"processingStatus": "ready", // pending | rendering | transcoding | ready | failed
"processingPhase": "...", // what processing is doing, for display
"processingProgress": 0.4, // 0..1, while processing
"processingError": "...", // present when failed
"variants": ["poster"], // original | poster | preview | standard
"renditions": [
{
"ratio": "9:16", // 9:16 | 4:5 | 1:1 | 16:9
"preset": "default", // default | whatsapp
"width": 1080,
"height": 1920,
"bitrate": 6000000, // bits per second
"sizeBytes": 23400000,
"sig": { "alg": "ed25519", "keyId": "...", "sig": "<base64>" }
}
]
},
"content": {
"altText": "Flooded street in the old town.",
"caption": [ /* rich text */ ],
"rights": [ /* rich text */ ],
"captions": [ /* cues, in the shape of core/audio segments */ ],
"transcript": [ /* uploads with speech, the same shape */ ]
},
"links": [
{ "type": "core/video-project", "rel": "source", "uri": "doc:<project id>" }, // renders
{ "type": "core/audio", "rel": "transcript", "uri": "doc:<audio id>" } // uploads with sound
]
}
- A rendered video links to its project with
rel=source. The project does not list its renders. - An uploaded clip starts at
pending, moves throughtranscoding(processingPhaseisqueued,converting,posteroruploading, withprocessingProgress) and ends atreadyorfailed(processingErrorsays why, for example a clip longer than three minutes). Oncereadyit haswidth,height,durationMsandhasAudioof its upright standard copy, and theposter,previewandstandardvariants.standardis the copy videos are made from: H.264, SDR, a constant 30 frames a second, upright. - An uploaded clip with sound links with
rel=transcriptto acore/audioof its sound, which links back withrel=sourceand is transcribed like any recording. Its finished transcript is copied onto the clip'scontent.transcript, in the shape ofcore/audiosegments with word timings. - A render's document exists from the moment rendering starts, with
processingStatus: "rendering";processingProgressfollows the renders. It becomesreadywhen every rendition is done, orfailedwithprocessingError. - Each rendition is its own file, signed over its bytes
(
tidenda-video-v1\0). An upload'ssignaturesigns the original. See Signatures. - There is one rendition per shape and preset, and each says its
preset(defaultwhen absent). Thedefaultpreset is 1080 wide; thewhatsapppreset is 720 wide and under 16 MB, a file a messaging app forwards without compressing it again. - A render's
content.captionsare the burned-in captions as sentences with their words. withdrawnis set when the video must no longer be shown. Show a notice in its place; its files are no longer served. Who withdrew it is recorded in the privatehistorywhen the video leaves a published status.- Captions are word-timed cues in the shape of
core/audiosegments; WebVTT is generated from them. - A
readyrender has at least one rendition and asource; areadyupload hascontentHashand theoriginalvariant. - People who need protecting are flagged in
internal.meta.persons, as forcore/image.
core/video-project
The storyboard a video is made from: the sources it may draw on, its
scenes, the voice and the settings. A video assignment's project
has the assignment's id. Not readable through GET /v1/documents.
{
"type": "core/video-project",
"title": [ /* rich text */ ],
"meta": { /* shared meta */ },
"content": {
"sources": [
{
"id": "...", // stable id of the entry
"type": "core/article", // core/article | core/image | core/audio | core/video
"uri": "doc:<document id>",
"title": "Floods in the old town", // optional, for display
"use": { "kind": "whole" } // whole | range { startMs, endMs } | quote { text }
}
],
"scenes": [
{
"id": "...", // stable, never reused
"order": "a0", // fractional order key; scenes sort by it
"text": [ /* rich text */ ],
"sourceRefs": ["..."], // the source sentences the text rests on: "<paragraph id>#<n>",
// the nth sentence of an article paragraph, or a paragraph's id
"asset": { // optional
"type": "core/image", // core/image | core/video
"uri": "doc:<document id>",
"crops": { "9:16": { "x": 0.3, "y": 0, "w": 0.4, "h": 1 } } // optional, per ratio
},
"layout": { "kind": "fullBleed" },
// fullBleed | quoteCard { speaker } | headline | number { value, unit }
// | threeThings { index: 1-3 }
// | clip { startMs, endMs, lowerThird: { name, role } }; extra fields optional
"durationMs": 4000, // optional
"motion": "none" // none | slowZoom | pan
}
],
"voice": { "kind": "none" }, // none | recorded { audioId } | synthetic { voiceId }
"settings": {
"ratios": ["9:16", "4:5"], // 9:16 | 4:5 | 1:1 | 16:9; defaults to ["9:16"]
"targetDurationMs": 30000, // defaults to 30000
"templateId": "...", // optional
"language": "es-MX" // optional, BCP 47
}
},
"links": [
{ "type": "core/planning-item", "rel": "planning", "uri": "doc:<planning id>" },
{ "type": "core/planning-assignment", "rel": "assignment", "uri": "doc:<assignment id>" }
]
}
- Work in progress is valid: a scene without an asset, a duration or text saves. Whether a project can be rendered is checked when it is exported.
- Scenes are shown sorted by
order, not by their place in the list, so a scene moves without renumbering the others. - A scene's crop for a ratio overrides the asset's
focalRegion. - A
threeThingsscene is a list: each line of its text is one point. ConsecutivethreeThingsscenes build one list, each adding its own points, five at most. - A
clipscene plays an uploadedcore/video(itsasset) fromstartMstoendMsin the clip (the whole clip when they are absent), with its own sound, for exactly that long, up to 60 seconds.lowerThirdnames who is speaking: the name, and the role when given, shown over the clip; without it there is none. The scene's text is not drawn on a clip: it is the scene's caption, for the post text, and the voice-over does not read it. A vertical clip fills 9:16; a clip of another shape is shown whole over a blurred copy of itself. People flagged on the clip are blurred as on a photo. Its captions are the words said in it, from its transcript, where no voice-over is heard. voiceis one recording for the whole video. It is silent while a clip scene plays, and carries on afterwards where it stopped; where it meets a clip's sound, the clip is lowered under it. Once it is transcribed, the scenes follow it: each scene starts where its words are spoken, and the captions follow the words. A scene'sdurationMsthen applies only when its words are not in the recording and it runs after the voice. Before the transcript, the scenes keep their own lengths and stretch to the voice's.slowZoomandpanare gentle moves that keep the image's focal region and any protected person in frame; their direction varies from one photo scene to the next.nonekeeps the photo still.internal.metaholds the render in progress (renderJob), the approval records (renderRecords: the render spec hash a member approved, who and when, the shapes they viewed, the models involved, the source check and the acknowledged rights), the source check's warnings set aside (sourceCheckDismissals) and the share log (shares). None of it is published.sourceCheckDismissalslists each warning a member set aside before approval, shared by everyone working on the project:{ flagId, reason, by, at }, wherebyis the member (as a history entry's user actor) andreasonis required. A blocking finding cannot be set aside.- An approval's
sourceCheckis the source check of the approved version:{ checkedAt, entailmentChecked, flags }. Each flag hassceneId,layer(0 to 7),check,key,severity(blockorwarn),messageand, for a warning set aside,dismissed: { by, at, reason }.entailmentCheckedis false when the meaning check could not run. A version with a blocking finding cannot be approved, so a recorded flag is a warning. A version the check could not run on (no article to check against) has nosourceCheck. Records approved before this shape carry{ warnings, dismissedBy? }instead: the warnings in words and the member who dismissed any. - An approval record is appended when a member approves a version; a video is rendered only from a version with one. Each record names the asset ids whose unknown social rights the approver acknowledged.
core/tag
A label from the organisation's vocabulary: a site section, or a story thread that runs across articles.
{
"id": "tag-...",
"type": "core/tag",
"title": [ /* rich text: the tag's name */ ],
"meta": { /* shared meta */ },
"content": {
"tagType": "story", // story | section
"language": "sv", // "" for any language
"slug": "election-2026", // "" until first published
"description": "Coverage of the 2026 general election.",
"iptc": "medtop:20000638" // optional IPTC code; "" when unmapped
},
"links": []
}
- Articles and planning items reference tags with links whose
uriiscore/tag:<tag id>and whoserelissectionorstory. - A
cancelledtag stays resolvable from existing links but cannot be added again.
core/publisher-site
The configuration of the organisation's public site. There is one per organisation. See Public site.
{
"id": "pub-...",
"type": "core/publisher-site",
"title": [ /* rich text */ ],
"meta": { "priority": 3 /* and the workflow fields */ },
"content": {
"siteId": "...",
"name": "Morgonposten",
"description": "",
"language": "sv",
"locale": "sv-SE",
"status": "live", // provisioning | live | suspended
"template": "classical", // classical | contemporary
"theme": {
"preset": "broadsheet", // classical: broadsheet | editorial | independent
// contemporary: newsroom | financial | investigative
"typeface": "traditional", // classical: traditional | serif | magazine
// contemporary: grotesque | angular | hybrid
"logoHeaderImageId": "...", // optional core/image id; see below
"logoFaviconImageId": "...", // optional core/image id
"logoHeaderUrl": null, // public URL resolved from the image, or null
"logoFaviconUrl": null
},
"analytics": { "provider": "none", "id": null }, // none | gtm | ga4
"sections": [
{ "id": "sec-news", "slug": "news", "label": "News", "order": 0, "visible": true }
],
"blocks": [
{ "id": "blk-hero", "type": "hero", "design": "A",
"sectionId": null, "order": 1, "adCode": null }
// type: flash | hero | high_priority | low_priority | text_only | ad
],
"domains": [
{ "host": "news.example.com", "kind": "custom", "verifiedAt": "2026-06-01T10:00:00Z" }
]
},
"links": []
}
- A block with a
sectionIdappears only on that section's page;nullmeans the front page.adCodeapplies toadblocks. - A
customdomain serves the site onceverifiedAtis set. - The site header shows the published brand kit's
logos.primarywhen it is set, andtheme.logoHeaderImageIdotherwise.
core/brand
The organisation's brand kit: its logos, colours and typeface, and the
look of its videos. There is one per organisation, with the id
brand. Its published version is what the public site and new videos
use, and a rendered video records the version it was made with
(meta.source.brandVersion on core/video).
{
"id": "brand",
"type": "core/brand",
"title": [ /* rich text */ ],
"meta": { "priority": 3 /* and the workflow fields */ },
"content": {
"logos": {
"primary": "...", // optional core/image id: the main logo
"onDark": "...", // optional core/image id: for dark backgrounds
"mark": "..." // optional core/image id: a square mark
},
"colours": {
"primary": "#1F2937", // #RRGGBB
"accent": "#E11D48" // #RRGGBB
},
"typeface": "grotesque", // a type set from core/publisher-site's theme
"video": {
"captionStyle": "boxed", // boxed | outline
"endCardText": [ /* rich text, optional */ ]
}
},
"links": []
}
- An organisation without a brand kit gets one, in
draft, the first time it is read. Its default colours pass the contrast check. - Text on the primary colour is white. The kit cannot move to
usableunless that text reaches the WCAG AA contrast ratio of 4.5:1; the transition is refused with the measured ratio. A draft saves with any colour. - The kit stays editable while
usable; publishing it again makes the edits current. Editing and publishing it need the permission to configure the public site.
core/workflow
The statuses a document type can be in and the transitions between them. See Workflows for how they behave.
{
"type": "core/workflow",
"title": [ /* rich text */ ],
"meta": {
"priority": 3,
"appliesToType": "core/article", // the type it governs
"isDefault": true, // at most one default per type
"status": "usable" // and the other workflow fields
},
"content": {
"statuses": [
{
"name": "draft", // lowercase kebab-case, starts with a letter
"title": "Draft",
"description": "Work in progress.", // optional
"icon": "circle-dashed", // optional
"color": "neutral" // optional
}
],
"transitions": [
{
"from": "draft",
"to": "usable",
"title": "Publish",
"description": "...", // optional
"requiredRoles": ["publisher"], // owner | admin | publisher | editor; [] for any member
"bumpsVersion": false,
"ai": { // optional
"operationRef": "translate-to-swedish", // an operation's exportName
"onSuccess": "usable",
"onFailure": "draft" // optional
},
"confirm": { // optional dialog
"title": "Publish now?",
"body": "...",
"confirmLabel": "Publish",
"isDestructive": false
}
}
]
},
"links": []
}
statusesmust includedraft, where every document starts.- Every
from,to,ai.onSuccessandai.onFailurenames a declared status. - An
ai.onSuccessofusableorwithheldpublishes the AI's answer without review. A workflow holding one is refused when saved unless the organisation allows AI publishing without review. See Publishing without review. - A transition from
usable,withheldorcancelledto an editable status must setbumpsVersion: true. - New documents bind to the
usabledefault workflow for their type. Bound documents follow the workflow's current definition.
core/ai-operation
One AI task: what to send, what to ask, what shape the answer must
have, and where it lands. An operation runs on a workflow transition
whose ai.operationRef names it, or from the editor when it is
offered on demand. See AI integrations.
{
"type": "core/ai-operation",
"title": [ /* rich text */ ],
"meta": {
"priority": 3,
"appliesToType": "core/article", // the type it reads
"connectorId": "...", // the AI connector it runs on
"exportName": "translate-to-swedish", // the name workflows use
"onDemand": false, // optional; offer in the editor
"status": "usable" // only usable operations run
},
"content": {
"instructions": [ /* rich text: the system prompt */ ],
"documentProjection": { "mode": "fields", "paths": ["title", "content.text"] },
"staticContext": [
{ "name": "glossary", "description": "...", "data": [ /* rich text holding JSON */ ] }
],
"inputs": [
{ "kind": "enum", "name": "targetLanguage", "label": "Target language",
"description": "...", "required": true, "default": "sv",
"options": [{ "value": "sv", "label": "Swedish", "extras": { "locale": "sv-SE" } }] },
{ "kind": "string", "name": "audience", "label": "Audience",
"placeholder": "Teenagers", "required": false }
],
"outputSchema": { "schema": [ /* rich text holding a JSON Schema */ ] },
"outputTargets": [
{ "kind": "path", "from": "title", "to": "title" },
{ "kind": "inputPath", "from": "targetLanguage", "to": "meta.lang" },
{ "kind": "textPatch", "from": "patches", "targetField": "content.text" },
{ "kind": "richTextReplace", "from": "body", "targetField": "content.text" },
{ "kind": "createDocument", "from": "companion", "documentType": "core/article" }
],
"sampling": { "model": "...", "temperature": 0.2, "maxTokens": 4000 },
"safety": { "maxInvocationsPerHour": 60, "cooldownSeconds": 30, "maxAutoRetries": 2 }
},
"links": []
}
documentProjection decides what the model sees. full sends the
whole document without its private fields. fields sends only the
listed dot-paths; paths into private fields select nothing, and a
projection that selects nothing fails the run instead of calling the
model.
instructions is the system prompt, written as prose.
staticContext is named data the prompt can refer to, such as a
glossary or a category list. Each entry is rendered into the prompt as
JSON under its name.
inputs are values a member picks when the operation runs. An
enum input offers fixed options, each with optional extras the
prompt can use; a string input takes free text. required defaults
to true. The values reach the prompt as a userInputs object.
Static-context and input names start with a letter, use letters,
digits and underscores, and are unique across both lists.
outputSchema is a JSON Schema (draft 2020-12). The model's answer
is validated against it after every call, whatever the provider
promises. Edits apply from the next call.
outputTargets say where parts of the validated answer go. from
is a dot-path into the answer.
| Kind | Writes |
|---|---|
path |
The value at from to the dot-path to on the source document. |
inputPath |
The member's input named from to to, such as the chosen language. |
textPatch |
A list of { id, text } into the rich text at targetField: each element with that id gets the new text. Structure is kept; marks inside patched elements are lost. |
richTextReplace |
A rich-text node array that replaces the field at targetField whole. |
createDocument |
A new document of documentType built from the value. The source is untouched. |
No two path or inputPath targets share a to, and no two
textPatch or richTextReplace targets share a targetField. An
empty list makes the operation advisory: the answer is only logged.
sampling passes the model name and provider settings.
safety caps runs per hour, sets a per-document cooldown and caps
automatic retries, for workflow and on-demand runs alike.
meta.exportName is lowercase kebab-case. The operation editor
requires it before the operation can be made usable. A transition
runs the usable operation with the matching name, so promoting a
new version takes effect on the next run without touching the
workflow. Keep names unique: which of two operations with the same
name runs is not defined.
meta.onDemand offers the operation in the editor of
appliesToType documents, where anyone who may edit the document can
run it. Only advisory operations are offered.