The push: the deposit contract, version 1

Technical page, for a publisher who writes its own plugin or its own push code.

This page describes what a sender submits to Eiger Vault, what it receives in return, and how it behaves in the event of an error or an interruption. It follows the published contracts as they stand today on the main branch of our repositories: the native document ev-native/1 for the request, the deposit contract v1 for what the vault answers, and the rights format ev-rights/1. These contracts are versioned, and a published version is never changed: an addition arrives through a new version, which removes nothing from the previous one. A few points will be clarified later: see section 11.

Opening status. The interface that receives your requests is not yet open on the Internet; it will be open only on the arrival of the first publisher. Its address will be communicated at that time. The examples below use https://<native interface address>.

1. The principle

You do not call the vault directly: you call the native interface, which translates your native document into a deposit request, passes it to the vault with your key, and returns its response to you as it is. It keeps nothing of what passes through it and has no queue: it responds only after the vault's response. If the vault is unreachable, it responds 503.

Push: the publisher's CMS sends to Eiger Vault. Fetch: Eiger Vault reads at the publisher's source.

A deposit request carries one document: the article's text blocks and, optionally, its files (images, PDF of the page, video).

2. Authentication

The publisher key that we handed you has the form evk_ followed by sixteen hexadecimal digits, then _, then sixty-four more hexadecimal digits. It is presented in the Authorization header, under the Bearer scheme, and nowhere else:

Authorization: Bearer evk_0123456789abcdef_<64 hexadecimal digits>

3. The request

POST /deposits
Authorization: Bearer evk_0123456789abcdef_<64 hexadecimal digits>
Content-Type: multipart/form-data; boundary=…
X-Correlation-ID: <optional>

POST /deposits to deposit, and POST /presence-checks for the presence check (section 9): these are the only two routes. Any other path, or any other method, receives 404 (not-found).

The body is in several parts (multipart/form-data):

Part Content
native_document the native document, in JSON required
supplementary_metadata the supplementary metadata, a free JSON object (CMS keywords, alternative headlines) optional
one part per file the bytes of the file, as they are; its name is the one the native document gives it as many as there are files

The names native_document and supplementary_metadata are reserved: no file takes them. Each part that the request carries must be designated by the native document, and each part that it designates must be present. A part that declares a Content-Transfer-Encoding other than 7bit, 8bit or binary is refused.

The correlation identifier. You may send X-Correlation-ID; otherwise the interface derives one. It accompanies the request as far as the vault, and returns in the response header and in the body of any refusal: keep it for any support request.

The limits (starting values, adjusted by Eiger Vault before going into service):

What is limited Limit
the entire request 16 MiB
the native_document part 1 MiB
each of the other parts 16 MiB
the supplementary_metadata part 64 KiB

Beyond a limit, the interface stops reading and refuses with native.request-too-large (413). With these values, the heaviest deposit that can result, base64 included, fits within the 24 MiB that the vault accepts for the body of a deposit.

4. The native document

This is a JSON text that the interface never rewrites. If there is no explicit original envelope, these bytes are themselves sealed as the original envelope of the deposit: they are kept as they are.

{
  "format": "ev-native/1",
  "source": "ch-src-0192f3a4-5b6c-7d8e-9fa0-b1c2d3e4f5a6",
  "metadata": {
    "document_type": "article",
    "source_reference": "12345",
    "published_at": "2026-10-05T08:30:00+02:00",
    "title": "A headline",
    "authors": ["Anne Exemple"]
  },
  "body_form": "blocks",
  "text_blocks": [
    {"role": "paragraph", "text": "A paragraph."},
    {"role": "caption", "text": "The photo.", "file": "photo-1"}
  ],
  "files": [
    {"name": "photo-1", "role": "image", "media_type": "image/jpeg"}
  ],
  "cms_revision": "48213"
}

The contract is closed at every level. A member that the contract does not name, at whatever depth, causes the document to be refused (native.unknown-member): a misspelt member that passed silently would be sealed for ever without having said anything to anyone.

The top-level members.

Member Value
format ev-native/1 required
source identifier of your source, <region>-src-<UUID version 7>, which we communicate to you required
collection collection identifier, <source>:c<number>; without it, the source's default collection (or that of the latest version, for a known document) optional
metadata the metadata, see below required
body_form blocks, the only form served in v1 required
text_blocks list of at least one block required
files list of files; absent or empty for an article without files optional
rights the rights in the ev-rights/1 format (see section 11) optional
cms_revision the identifier of the article's revision in your CMS; a non-blank string required

The metadata descriptor.

Member Value
document_type article (the only type in v1) required
source_reference the identifier that your CMS gives to the article, as it is, under the reference rule agreed for your source (for example a prefix if your source brings together several editions) required
version_label version label chosen by you; never the CMS revision optional
published_at, updated_at publication date and update date displayed to the reader, see below optional
canonical_url canonical address optional
language BCP 47 tag optional
title, lede headline, standfirst optional
genres IPTC genre codes (Analysis, Interview…); an unknown value passes and is counted optional
category category path, ordered list of labels optional
authors, places lists of strings optional
relations links to other documents: types derived_from, corrects, translates, cites; target: an Eiger Vault document, a (source, reference) pair, an address or a digest optional

The fields that the vault writes itself – the schema type, its version, the resolved collection – are not declared: declaring them causes the document to be refused.

The blocks. Each block has a role – paragraph, subheading, sidebar or caption – and a non-blank text. A sidebar carries a text_blocks list of at least one block, which does not contain a sidebar. A caption can designate a file by its name in file. The text is copied without any modification, tags included: the vault reads no markup in it, and <b> counts in it as four characters.

The files. Each entry of files has a name (unique, not reserved: it is the name of the part that carries the bytes), a role (image, video or page_pdf; never text) and a media_type (of the form type/subtype, in lower case, without parameters). The files are kept in the order of the list, byte for byte. A file's name never goes to the vault and appears in no trace.

The dates. published_at and updated_at are written either as an RFC 3339 instant with its offset, copied as the CMS holds it (2026-10-05T08:30:00+02:00), or as a day YYYY-MM-DD, only when nothing else is known. A date without an offset is not an instant. Do not convert the offset: the vault converts it to UTC. updated_at is the update date displayed to the reader: if your site displays none, omit the member, and never put a technical modification date in it. A missing date stays missing; we never complete it.

Reading the JSON. The JSON part is strict I-JSON (RFC 7493): valid UTF-8, without a byte order mark, without a repeated member, without a lone surrogate or noncharacter, with whole numbers only. A blank string, null, an empty list and an empty object count as “absent”: the member is omitted, and a blank required member counts as absent. No block of text_blocks and no file of files is removed by this rule.

The source reference. This is the identifier that your CMS gives to the article, without any normalisation, and it never changes for the same article. It is not derived from an address. It follows the expected form that we agreed for your source: if it does not, the document is refused (native.reference-rule-mismatch) without the refusal citing the reference.

The CMS revision. For your CMS, it identifies a state of the article. It never becomes the version label. It is used to compute the idempotency key (section 6): the same revision must therefore always produce the same content, and a visible modification must produce a new revision.

5. The response

A deposit that creates a document or a version responds 201. A deposit that creates nothing – retry recognised, content unchanged – responds 200, with the seal of the version recognised. Both responses have the same form:

{
  "document": "ch-0192f3a4-5b6c-7d8e-9fa0-b1c2d3e4f5a6",
  "notices": [],
  "seal": {
    "depositor": "ch-dep-0192f3a4-5b6c-7d8e-9fa0-b1c2d3e4f5a7",
    "format": "ev-seal/1",
    "identity_level": "authenticated",
    "manifest_digest": {"sha-256": "…", "sha3-512": "…"},
    "origin_proofs_digest": {"sha-256": "…", "sha3-512": "…"},
    "received_at": "2026-10-05T06:31:12.123456Z"
  },
  "version_number": 1
}
Member Value
document the document identifier, <region>-<UUID version 7>: it derives neither from the content nor from your reference, and is never reused
version_number the version number, starting from 1
seal the complete version seal; its bytes are the sealed bytes. It carries neither a signature nor an address
notices the notices, an empty list when there are none

The seal returned is copied byte for byte from what the vault wrote; you may keep it as a receipt.

The notices (at most once each, never citing what caused them):

Value Meaning
retry_recognized the deposit is the retry of a deposit already received: nothing was created
content_unchanged the content is that of the latest version: nothing was created
version_label_already_used a version was created under a label already in use
out_of_schema_field_ignored a metadata field does not belong to the schema and was ignored
supplementary_metadata_not_kept the supplementary metadata was accepted, but has been passed to no one for the time being

6. Idempotency

You do not send an idempotency key: the interface computes it, as a keyed digest (HMAC-SHA-256, under your key) of the triple (source, source reference, CMS revision). The key is therefore never readable, and a retry that crosses a deployment on our side presents the same key. The vault keeps it for 30 days, for your depositor only.

Your submission Result
same document, same revision, same content: a retry 200, the seal of the first deposit, retry_recognized
same document, same revision, different content refusal deposit.idempotency-key-conflict (409): one revision cannot carry two contents
same document, new revision, content identical to the latest version 200, content_unchanged; no new version
same document, content changed 201, next version
same document, label already used, content changed 201, next version, with version_label_already_used

First submission, retry, content unchanged, content modified.

After the 30 days, a key is treated as new. Content identical to that of the latest version then returns content_unchanged rather than retry_recognized: there is no extra version in any case.

7. Versions

A document is identified by the pair (source, source reference). A deposit whose pair is known targets the existing document. Versions are numbered 1, 2, 3, without gaps, per document.

What makes a version. The content you declare – the metadata and the parts, structured text included – is compared with that of the latest version. The bytes of the original envelope do not enter into the comparison. If they differ, that alone does not create a version: a regenerated page, or a native document in which an untranslated member changes, creates none. The version label and the collection are part of the metadata: changing them creates a version.

What never changes. The type of a document: a deposit that would change it is refused (deposit.document-type-change, 409). The depositor who may deposit a new version is any depositor authorised for the document's source.

Reclassification. A new version can change collection within the document's source, by designating the new collection.

The publication date of the document is the one declared by its first version.

8. Withdrawals

There is no withdrawal operation in the deposit contract: neither a deletion, nor an “unpublished” status. An article deleted or unpublished in your CMS stays in our register, with all its versions. To withdraw content, the publisher makes the request to Eiger Vault; an operator then takes a decision, which is recorded in the register for ever with its authors, its instant and its reason. Two degrees exist: the block (the content is shown to no one any more, the seal is kept; the decision is reversible) and the erasure (the encryption keys of the parts and of the metadata are destroyed; it cannot be undone). This procedure will be detailed before going into service.

What is never withdrawn, even after an erasure: the identifier, the digests, the seal, and a tombstone that says there was a document, when, on whose behalf, and why it is no longer there.

9. Recovery after an interruption

If you have not received a response – timeout, connection cut, 503 –, the outcome of the deposit is unknown. Send the same request again, with the same CMS revision: you will receive the seal of the first deposit (200, retry_recognized) if it took place, or the deposit will take place (201). A retry never creates a duplicate version.

Recovery after an interruption: resend the same request, or presence check.

If a series of deposits may have been lost, for example after an outage on your side or ours, the presence check tells you what we know, without sending us anything:

POST /presence-checks
Authorization: Bearer evk_0123456789abcdef_<64 hexadecimal digits>
Content-Type: application/json

{"references": [{"source": "ch-src-0192f3a4-5b6c-7d8e-9fa0-b1c2d3e4f5a6", "source_reference": "12345"},
                {"source": "ch-src-0192f3a4-5b6c-7d8e-9fa0-b1c2d3e4f5a6", "source_reference": "12346"}]}

The body is limited to 1000 references and 4 MiB. The 200 response gives one result per pair submitted, in the order submitted:

{"results": [
  {"source": "ch-src-…", "source_reference": "12345", "state": "served",
   "collection": "ch-src-…:c1", "version_label": "evening edition",
   "comparison_digest": {"sha-256": "…", "sha3-512": "…"}},
  {"source": "ch-src-…", "source_reference": "12346", "state": "absent"}
]}
State Meaning
absent the pair is unknown: send the document
served the document has a version: it is already with us
out_of_scope your key is not authorised for this source, or the source is unknown; the other references are not affected

The response never carries content, nor a document identifier, nor a version number. A periodic redeposit consists of listing your recent content (initially the last 48 hours, every hour), submitting it to the presence check, then depositing only the absent ones.

The redeposit mode exists in the deposit contract: a deposit marked in this way never creates a version of an existing document, and returns the seal of its latest version. Its use by the native interface will be clarified: see section 11.

10. Refusals

A refusal always has the same form, with its HTTP status:

{"code": "native.missing-required-field", "message": "<fixed sentence>", "correlation_id": "…"}

The message is a fixed sentence that says where (the name of a member, the rank of a block or a file) and what; it never cites the input: neither a value, nor the name of an unknown member, nor a reference, nor a file name. The code is the only thing your program should rely on. A refusal writes nothing with us.

10.1 The order

The same submission always gives the same refusal, because the checks are made in a fixed order, and the first deviation gives the refusal. There are two stages: the native interface, which checks before any call to the vault, then the vault.

The checks of the native interface, then those of the vault.

Stage 1, the native interface.

  1. before reading the document: a key-shaped string in the address, then the size, then the transfer encoding of the parts;
  2. the I-JSON reading, then the object form of the document;
  3. format: absent or different from ev-native/1;
  4. the member names, at every level;
  5. the members, in the order source, collection, metadata, body_form, text_blocks, files, rights, each one thoroughly;
  6. the explicit original envelope and its proofs, if they are there;
  7. the designations: each file name, then each caption;
  8. cms_revision;
  9. the matching of the request's parts with what the document designates; then the source in the interface's configuration, and the reference against its source's rule.

The exact position of the matching of the request's parts relative to steps 3 to 8 will be clarified.

Stage 2, the vault, according to the deposit contract v1:

  1. the key;
  2. the attribution to a source or a collection, and the scope: source or collection not authorised, deactivated, unknown, not designated, and, in our trial environment only, source not admitted;
  3. the size of the body and of each part;
  4. the format of the request, the metadata, the parts and the structured text;
  5. the origin proofs, if there are any;
  6. an original envelope already linked to another document;
  7. under the document lock, in order: the scope is re-read; then the decision of section 7: an idempotency key already seen, a change of type, the recognition of a retry, of unchanged content, or the creation of a version.

A top-level member that the deposit contract does not know, or a body that is not an object, is refused deposit.malformed-request.

10.2 The native interface codes

Cause Code Status
the JSON part is not strict I-JSON native.invalid-json 400
format version absent or not served native.unsupported-format 400
member that the contract does not name native.unknown-member 400
required member absent native.missing-required-field 400
body form not served native.unsupported-body-form 400
other breach: form of a value, role outside the vocabulary, file with role text or original, badly written date or media type, duplicate or reserved file name, caption pointing to a file absent from the list native.invalid-native-document 400
explicit original envelope without proof, or proof without envelope native.unpaired-original-envelope 400
the document designates a part absent from the request native.missing-form-part 400
the request carries a part that the document does not designate, or two parts of the same name native.undesignated-form-part 400
transfer encoding of a part other than 7bit, 8bit, binary native.unsupported-transfer-encoding 400
a key-shaped string in the address, the JSON part or the body native.key-outside-header 400
a part, the request or the body of a presence check exceeds its limit native.request-too-large 413
supplementary metadata that is not an I-JSON object, or too large native.invalid-supplementary-metadata 400
the reference does not follow the expected form of its source native.reference-rule-mismatch 400
the designated source is not configured in the interface native.unconfigured-source 403
other path or other method not-found 404
vault unreachable, or its response unreadable unavailable 503

A key written with \u escapes in the JSON part is discovered only when reading it: native.invalid-json then takes precedence over native.key-outside-header.

10.3 The vault codes, returned as they are

Cause Code Status
key unknown, revoked, absent, malformed, or belonging to a deactivated depositor deposit.unknown-or-revoked-key 401
malformed request: unknown member, body that is not an object… deposit.malformed-request 400
designated source unknown or not authorised, authorisation withdrawn, or depositor authorised for no source deposit.source-not-authorized 403
the source is deactivated deposit.source-deactivated 403
the collection is deactivated deposit.collection-deactivated 403
(trial environment) source not admitted deposit.source-not-staging-admitted 403
collection unknown, or not that of the designated source deposit.unknown-collection 404
several sources authorised, and none designated deposit.source-not-designated 400
part or body too large (part > 16 MiB, body > 24 MiB) deposit.part-or-body-too-large 413
original envelope already linked to another document deposit.original-envelope-of-another-document 409
idempotency key seen, with other content or for another document deposit.idempotency-key-conflict 409
change of a document's type deposit.document-type-change 409
format refusal: non-conforming JSON, number, date, digest, part or reference missing, media type, structured text, metadata, part, text, origin proof format.invalid-json, format.invalid-number, format.malformed-date, format.malformed-digest, format.missing-required-field, format.malformed-media-type, format.invalid-structured-text, format.invalid-text-part, format.invalid-metadata, format.invalid-part, format.incomplete-digest-only, format.unsupported-origin-proof-type, format.invalid-origin-proof 400
vault keys not yet unlocked, or parts compartment not responding unavailable 503

The codes specific to a fetch (deposit.attested-fetch-missing, deposit.invalid-attested-fetch, deposit.domain-outside-source-domains, deposit.original-envelope-not-attested) do not concern a push with a publisher key. Whether a code is final or transient is read from its status: any 4xx is final as long as the request does not change; 503 is retried, with the same request.

11. What will be clarified

Certain points of the contract will be clarified or completed. We will announce them to you, and an addition will not change what this page already describes.

12. The vectors

The native document contract is delivered with reference vectors: admitted native documents, with the deposit request that they produce, and refused documents, with their code. They never change and can be replayed against your own code. We hand them to you with the contract.