Tidenda

Reference

API reference

Every public endpoint, request and response shape, scopes, error codes, rate-limit headers.

API reference

Field-level reference for every endpoint on the public surface. For the high-level orientation (authentication, scopes, rate limits) see API overview.

All /v1/* endpoints require a per-tenant API key in Authorization: Bearer vk_.... GET /api/keys is unauthenticated. All responses are JSON unless noted. Errors share an envelope:

{ "ok": false, "error": { "code": "INVALID_PAYLOAD", "message": "..." } }

Success responses share { "ok": true, "data": {...} }, except GET /api/keys, which returns the trust list at the top level (see its section below).

POST /v1/documents

Create a new document of any type the tenant has a workflow for.

Scope: documents:create.

Request

POST /v1/documents HTTP/1.1
Authorization: Bearer vk_<prefix>_<secret>
Content-Type: application/json
{
  "id": "3b2c1a40-f128-4cb6-b8c2-19a4d8d7e2f5",
  "type": "core/newswire",
  "title": "Cabinet reshuffle expected ahead of Thursday vote",
  "meta": {
    "priority": 3,
    "origin": "wire",
    "source": "your-importer"
  },
  "content": {
    "text": "..."
  },
  "links": []
}
Field Type Required Notes
id UUID v4 string yes 8-4-4-4-12 hex. Caller-chosen. Idempotency key.
type string yes A document type known to the tenant (e.g. core/newswire, core/article).
title string or rich text yes Plain string is accepted; the platform stores it as the document's title field.
meta object varies Required keys depend on type. See Document formats.
content object varies Type-specific. See Document formats.
links array no Cross-document references. Required shape depends on type.

The platform stamps meta.status (always draft on intake), meta.version, meta.workflowId, and meta.workflowVersion. Do not set these.

Idempotency

Retrying a create with the same id returns 409 CONFLICT with no side effects, so a network failure followed by a retry with the same id is safe.

Responses

Status Body Meaning
201 Created { "ok": true, "data": { "id": "..." } } Created.
400 Bad Request { "ok": false, "error": { "code": "INVALID_PAYLOAD", "message": "..." } } Missing id, bad UUID, unknown type, schema validation failure.
401 Unauthorized { "ok": false, "error": { "code": "UNAUTHORIZED", "message": "..." } } Missing or invalid Authorization header.
403 Forbidden { "ok": false, "error": { "code": "FORBIDDEN", "message": "..." } } API key lacks documents:create scope.
409 Conflict { "ok": false, "error": { "code": "CONFLICT", "message": "Document <id> already exists" } } A document with the same (tenant, id) already exists.
429 Too Many Requests { "ok": false, "error": { "code": "RATE_LIMITED", "message": "..." } } Per-IP or per-key rate limit hit. Retry-After header is set.

GET /v1/documents

Read documents out of a tenant. Filter by type, status, and updated-since; cursor-paginate through the result set.

Scope: publisher:read.

Request

GET /v1/documents?type=core/article&status=usable&updatedSince=2026-07-01T00:00:00Z&limit=50 HTTP/1.1
Authorization: Bearer vk_<prefix>_<secret>

Query parameters

Name Type Notes
type string Optional. A document type (e.g. core/article, core/newswire). Omit to receive every type.
status string Optional. Workflow status (e.g. draft, usable, withheld).
updatedSince string Optional. ISO 8601. Only documents whose updated_at is at or after this instant.
limit integer Optional. 1–200. Default 50.
cursor string Optional. Opaque page cursor from a previous response's nextCursor. Malformed cursors are treated as "from the top".

Response

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "3b2c1a40-f128-4cb6-b8c2-19a4d8d7e2f5",
        "type": "core/article",
        "title": "Cabinet reshuffle expected ahead of Thursday vote",
        "meta": { "status": "usable", "version": 3 },
        "content": { "text": "..." },
        "updated_at": "2026-07-06T14:42:00Z"
      }
    ],
    "nextCursor": "eyJ1IjoiMjAyNi0wNy0wNlQxNDo0MjowMFoiLCJpIjoiM2IyYzFhNDAifQ==",
    "hasMore": true
  }
}
  • items — up to limit documents ordered by updated_at descending, breaking ties on id. Each content is the stored JSON projection; snapshot bytes and signatures come from the version-specific endpoint.
  • nextCursor — pass back verbatim on the next request. Not a URL parameter, an opaque string.
  • hasMoretrue if a page after this one exists.

Cursor pagination

Keyset paging on (updated_at DESC, id DESC). A caller reading a tenant's articles from the beginning follows the nextCursor on each response until hasMore is false. To catch up on changes since a checkpoint, keep the last-seen updated_at and re-issue with updatedSince.

Responses

Status Body Meaning
200 OK See above. Success.
400 Bad Request { "ok": false, "error": { "code": "INVALID_QUERY", "message": "..." } } updatedSince not a valid ISO 8601 timestamp, or bad limit.
401 Unauthorized { "ok": false, "error": { "code": "UNAUTHORIZED", "message": "..." } } Missing or invalid Authorization header.
403 Forbidden { "ok": false, "error": { "code": "FORBIDDEN", "message": "..." } } API key lacks publisher:read scope.
429 Too Many Requests { "ok": false, "error": { "code": "RATE_LIMITED", "message": "..." } } Rate limit hit. Retry-After header is set.

GET /v1/phrases

List or export the tenant's custom dictionary.

Scope: phrases:read.

Query parameters

Name Type Default Notes
language string all BCP47 code. Filters to phrases for that language only.
format string json json returns the standard envelope. csv returns raw CSV.

Response — JSON

{
  "ok": true,
  "data": {
    "phrases": [
      { "id": "...", "language": "sv", "phrase": "Stockholm", "...": "..." }
    ]
  }
}

Response — CSV

Content-Type: text/csv; charset=utf-8, no envelope, one row per phrase.

PUT /v1/phrases

Upsert a single phrase. The body is one phrase object; the row is created or replaced.

Scope: phrases:write.

Request body

{
  "language": "sv",
  "phrase": "Stockholm",
  "...": "..."
}

The set of accepted keys is the same as the export shape. See Document formats for the canonical phrase fields.

Responses

Status Body Meaning
200 OK { "ok": true, "data": { "phrase": { ... } } } Upserted.
400 Bad Request INVALID_PAYLOAD Validation failure.
429 Too Many Requests RATE_LIMITED See above.

POST /v1/phrases/import

Bulk seed. Accepts a JSON array, a JSON object with a phrases array, or raw CSV.

Scope: phrases:write.

Request

Content-Type: application/json:

{ "phrases": [ /* phrase objects */ ] }

or

[ /* phrase objects */ ]

Content-Type: text/csv: a CSV body using the same column shape the export emits.

Response

{ "ok": true, "data": { "imported": 42 } }

DELETE /v1/phrases/:id

Delete a single phrase by id.

Scope: phrases:write.

Responses

Status Body Meaning
200 OK { "ok": true, "data": { "id": "..." } } Deleted.
404 Not Found { "ok": false, "error": { "code": "NOT_FOUND" } } No phrase with that id.

GET /api/keys

Public, unauthenticated. Returns the Ed25519 keys integrators should trust when verifying snapshot signatures.

Response

{
  "active": "void-2026-05",
  "keys": [
    {
      "keyId": "void-2026-05",
      "algorithm": "ed25519",
      "publicKey": "<base64 SPKI DER>",
      "validFrom": "2026-05-12T00:00:00Z",
      "validUntil": null
    },
    {
      "keyId": "void-2025-11",
      "algorithm": "ed25519",
      "publicKey": "<base64 SPKI DER>",
      "validFrom": "2025-11-01T00:00:00Z",
      "validUntil": "2026-05-12T00:00:00Z"
    }
  ]
}
  • active — the keyId currently signing new snapshots. null while signing is disabled (development without keys configured).
  • keys — every key, current and historical, that integrators should still trust. Look up an incoming snapshot's keyId here to pick the right publicKey.

Cache the response for around an hour. See Signing and signatures for the verification recipe.

Error envelope

Every error response uses the same shape:

{
  "ok": false,
  "error": {
    "code": "INVALID_PAYLOAD",
    "message": "Human-readable explanation."
  }
}
Code HTTP When
INVALID_PAYLOAD 400 Body malformed, missing fields, schema validation failure.
UNAUTHORIZED 401 Missing or invalid Authorization header.
FORBIDDEN 403 Key valid but lacks the required scope.
NOT_FOUND 404 Resource doesn't exist in this tenant.
CONFLICT 409 Idempotency conflict (e.g. document id collision).
RATE_LIMITED 429 Per-IP volumetric ceiling or per-key budget exceeded. Retry-After header set.

Rate-limit headers

When a request is rate-limited the response carries:

Retry-After: 30

(Seconds until the bucket refills.) The error envelope's message describes which limit fired.

Related pages