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.
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>
- A string that has the form of a key, found in the address or in the JSON part, causes the request to be refused (
native.key-outside-header) before any call to the vault. No address of the interface ever carries a key, a reference or a file name. - The interface passes the header on as it is, even if it is absent or malformed: only the vault judges a key. A key that is absent, malformed, unknown, revoked, or belonging to a deactivated depositor is refused with the same code,
deposit.unknown-or-revoked-key(401), which never says which of these causes applies. - The key is judged when the deposit arrives: a deposit begun with a valid key runs to completion, even if the key is revoked in the meantime.
- The seal's identity level follows from the key: a publisher key gives the “authenticated” level.
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 |
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.
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.
Stage 1, the native interface.
- before reading the document: a key-shaped string in the address, then the size, then the transfer encoding of the parts;
- the I-JSON reading, then the object form of the document;
format: absent or different fromev-native/1;- the member names, at every level;
- the members, in the order
source,collection,metadata,body_form,text_blocks,files,rights, each one thoroughly; - the explicit original envelope and its proofs, if they are there;
- the designations: each file name, then each caption;
cms_revision;- 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:
- the key;
- 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;
- the size of the body and of each part;
- the format of the request, the metadata, the parts and the structured text;
- the origin proofs, if there are any;
- an original envelope already linked to another document;
- 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.
- The order of the refusals when several apply to the same deposit will be written out in full in the contract. Until then, rely on the code of the refusal, not on its position.
- A deposit that does not complete within the time limit will receive a response that says to retry. Until then, treat a connection cut or a timeout as an unknown outcome, and send the same request again (section 9).
- Rights in a native document. The native document provides for a
rightssection; its use in a deposit will be clarified. Until then, do not send any. - The redeposit mode, and the modification of the rights of an existing document, through the native interface, will be opened with its public opening.
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.