Tidenda

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:

  • aiGenerated is the reader AI label: when true, 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. On core/image and core/audio the field of the same name has its own meaning, described there.
  • publishedWithoutReview is present when an AI workflow step moved the document into usable or withheld with no person reviewing the AI's answer: at, when the step published it, and operationRef, the operation's reference name. While it is present, aiGenerated is true and cannot be turned off. The platform removes it when a member next moves the document into usable or withheld. 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.

  • history is 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.
  • internal holds editorial working data in the document's own shape: meta for data about it, links for relations. On every type, internal.meta holds 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/video and core/video-project add their own fields to internal.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.time needs a target or a slot entry.
  • links needs a core/planning-item link and a core/planning-assignment link. source links name the wires the article was written from.
  • meta.slug is minted when the article first reaches usable and then stays. When it changes, the old slug moves to meta.slugAliases, which only grows.
  • meta.section is copied from the planning item when the article is created.
  • internal.meta.annotations are review findings on passages of the text: id, kind (finding), anchor (start, end, quote), category, title, note, optional replacement, status (open, applied, skipped), optional operationId, createdAt and createdBy (uri, title).
  • internal.meta.allowedSpelling lists words the spellchecker flags but this article allows: text, uri and title of the member who allowed it, and allowedAt.
  • internal.meta.rightsAcknowledgements records each time a member published the article although it embeds images with usage restrictions: at, by (kind user, userId, userEmail), the status moved into, and imageIds, 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.time needs both a startdate and an enddate.
  • Assignments live inside the planning item, not as separate documents. Each keeps its own id, and its deliverable has the same id: a core/video-project for a video assignment, a core/article for every other type. The deliverable links back with rel=planning and rel=assignment.
  • An assignment's type is fixed once its deliverable exists.
  • An assignment's meta holds only type and time. Its time is a hint; once the deliverable exists, its own target rules.
  • content.section applies 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" }
    ]
  }
}
  • source and sourceItemId are required and together identify the item: a second push of the same pair returns the existing wire.
  • iptcCategory is 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.
  • sourceUrl is the link to the original item at its source, an absolute http or https address.
  • sourceRef is 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.
  • byline is the author line the source gave the item, as sent.
  • attachments are files sent with the item, such as a PDF, an image or an audio clip, kept as links: url (absolute http or https), type (the file's media type, such as image/jpeg) and an optional name to show.
  • A wire from a registered source is updated when its item changes: meta.version goes up and the update is recorded in history. 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>" }
  ]
}
  • restrictions is true when instructions or usageTerms is set, aiGenerated is true, a usage scope is forbidden, or usage.expiresAt has 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.
  • usage records what the image may be used for: on social platforms, in paid placement, and modified (modify: crops, overlays, text on top). Each scope is allowed, forbidden or unknown; an absent usage or scope is unknown.
  • focalRegion is 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: x and y are its top-left corner and w and h its 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.
  • aiGenerated has three states. Absent means unknown, not false.
  • signature signs the original bytes; see Signatures.
  • A ready image always has contentHash, originalFormat, originalSize, dimensions and a delivery variant.

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.signature signs the original bytes. A segment's sig signs its text when an editor verifies it; compare signedTextHash with the SHA-256 of the current text to see whether it changed since. See Signatures.
  • A ready recording always has contentHash, originalFormat, originalSize, durationMs and the original variant. 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 through transcoding (processingPhase is queued, converting, poster or uploading, with processingProgress) and ends at ready or failed (processingError says why, for example a clip longer than three minutes). Once ready it has width, height, durationMs and hasAudio of its upright standard copy, and the poster, preview and standard variants. standard is the copy videos are made from: H.264, SDR, a constant 30 frames a second, upright.
  • An uploaded clip with sound links with rel=transcript to a core/audio of its sound, which links back with rel=source and is transcribed like any recording. Its finished transcript is copied onto the clip's content.transcript, in the shape of core/audio segments with word timings.
  • A render's document exists from the moment rendering starts, with processingStatus: "rendering"; processingProgress follows the renders. It becomes ready when every rendition is done, or failed with processingError.
  • Each rendition is its own file, signed over its bytes (tidenda-video-v1\0). An upload's signature signs the original. See Signatures.
  • There is one rendition per shape and preset, and each says its preset (default when absent). The default preset is 1080 wide; the whatsapp preset is 720 wide and under 16 MB, a file a messaging app forwards without compressing it again.
  • A render's content.captions are the burned-in captions as sentences with their words.
  • withdrawn is 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 private history when the video leaves a published status.
  • Captions are word-timed cues in the shape of core/audio segments; WebVTT is generated from them.
  • A ready render has at least one rendition and a source; a ready upload has contentHash and the original variant.
  • People who need protecting are flagged in internal.meta.persons, as for core/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 threeThings scene is a list: each line of its text is one point. Consecutive threeThings scenes build one list, each adding its own points, five at most.
  • A clip scene plays an uploaded core/video (its asset) from startMs to endMs in the clip (the whole clip when they are absent), with its own sound, for exactly that long, up to 60 seconds. lowerThird names 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.
  • voice is 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's durationMs then 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.
  • slowZoom and pan are 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. none keeps the photo still.
  • internal.meta holds 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.
  • sourceCheckDismissals lists each warning a member set aside before approval, shared by everyone working on the project: { flagId, reason, by, at }, where by is the member (as a history entry's user actor) and reason is required. A blocking finding cannot be set aside.
  • An approval's sourceCheck is the source check of the approved version: { checkedAt, entailmentChecked, flags }. Each flag has sceneId, layer (0 to 7), check, key, severity (block or warn), message and, for a warning set aside, dismissed: { by, at, reason }. entailmentChecked is 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 no sourceCheck. 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 uri is core/tag:<tag id> and whose rel is section or story.
  • A cancelled tag 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 sectionId appears only on that section's page; null means the front page. adCode applies to ad blocks.
  • A custom domain serves the site once verifiedAt is set.
  • The site header shows the published brand kit's logos.primary when it is set, and theme.logoHeaderImageId otherwise.

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 usable unless 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": []
}
  • statuses must include draft, where every document starts.
  • Every from, to, ai.onSuccess and ai.onFailure names a declared status.
  • An ai.onSuccess of usable or withheld publishes 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, withheld or cancelled to an editable status must set bumpsVersion: true.
  • New documents bind to the usable default 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.