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 tolimitdocuments ordered byupdated_atdescending, breaking ties onid. Eachcontentis 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.hasMore—trueif 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— thekeyIdcurrently signing new snapshots.nullwhile signing is disabled (development without keys configured).keys— every key, current and historical, that integrators should still trust. Look up an incoming snapshot'skeyIdhere to pick the rightpublicKey.
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
- API overview — authentication, scopes, contract scope.
- Signing and signatures — verifying snapshot
signatures from
GET /api/keys. - Receiving webhooks — outbound delivery contract.
- Document formats — per-type request schemas.