veil

Make a signed request

Use an authorised device key for changes to cards and organisations.

Public reads do not require a signature. Changes use an active authorised key and a signed request. These endpoints are for local or server clients. CORS does not open signed writes to another website. Never send a private key to Veil.

Sign the request bytes#

text
veil-rubrica:request:v1
POST /v1/identities/1/versions
<exact UTF-8 JSON body>

Put the public key in X-Public-Key and the request signature in X-Signature. The JSON body includes a fresh nonce and a timestamp in Unix milliseconds within two minutes of the server clock. A nonce is used once. The signature covers the method, the exact path and the exact body.

Ed25519 keys and signatures use hexadecimal. Composite keys and signatures use base64 and X-Signature-Alg set to mldsa65-ed25519. For signed GET requests, send X-Nonce and X-Timestamp and sign the exact path with its query, nonce and timestamp separated by newlines.

text
veil-rubrica:request:v1
GET /v1/device-requests/PAIRING_CODE
<nonce>
<Unix milliseconds>

Publish a version in format 2#

Format 2 is the default since 28 October 2026 and the live log uses it. The four publishing requests, POST /v1/identities, POST /v1/identities/{n}/versions, POST /v1/organizations and POST /v1/organizations/{n}/versions, send label, body, signature and, for a new picture, pictureData, with the usual nonce and timestamp. Version 1 also sends clientSalt and saltTicket. A format 1 record sent to a format 2 log gets 400 wrong_format.

json
{
  "label": "{\"auth\":[],\"body\":\"BODY_HASH\",\"identity\":\"CARD_TOKEN\",\"n\":2,\"prev\":\"PREVIOUS_LABEL_HASH\",\"state\":\"STATE_ROOT\",\"time\":1792960060,\"v\":2}",
  "body": "{\"ops\":[{\"name\":\"Ada Brennan\",\"op\":\"name\"}],\"salt\":\"BODY_SALT\",\"v\":2}",
  "signature": "LABEL_SIGNATURE",
  "nonce": "FRESH_NONCE",
  "timestamp": 1792960060000
}
text
veil-rubrica:identity-version:v2
<canonical label>

label and body are canonical JSON texts, the exact bytes that are hashed and signed. The server encodes them again and compares, and it never rewrites them. signature covers the prefix above followed by the label, in hex for Ed25519 and in base64 for a composite key. pictureData carries the bytes of a new picture that the body names by hash, in standard padded base64.

  • label.n is the version number and label.prev the hash of the previous label, null for version 1. label.body is the hash of the body. label.state is the root of the state after the operations, salted with the body’s salt. label.time is Unix seconds within five minutes of the server clock.
  • label.auth lists, in order, the operations that change who can sign: addKey and revokeKey for a person, and invite, role and remove of an owner or admin for an organisation. In the Veil organisation every staff role counts. Every other operation goes in body.ops, and an operation in the wrong place gets 422 invalid_op.
  • For an organisation, label.signer names the member card that signs and its current version as {identity, n}, and the key is an active key of that version. The body never carries signer.
  • Each key that label.auth adds needs its name in a device operation of the same body.
  • A person holds at most 64 active keys (keys_limit) and an organisation at most 1,024 members in its authority (authority_limit). While an erasure of the card waits, the card does not publish (erase_pending).

Request examples with request.json assume your client has already built and signed the body. A publication is ready only after its commitment appears in a witnessed checkpoint. If a request times out, check the current versions before sending another change.

An erasure request on a format 2 log also carries closing, the closing entry signed with the same key over the prefix veil-rubrica:close-entry:v2 and a line break followed by the entry without its signature. Without it the answer is 422 closing_required.

Canonical JSON, for a label, a body or a format 1 record, sorts object keys by UTF-16 code units, preserves array order and has no whitespace outside strings. It allows only integers in the JavaScript safe range, UTF-8 strings, booleans and null. Do not normalise or trim signed text. The outer request body need not be canonical, but its request signature must cover the exact bytes sent.

Format 1 records on test-1#

Format 1 is frozen on log.veil.rip/test-1, which takes no new versions. Only a server whose GET /v1/log has no format takes the signed record below, as record, signature and an optional logoData. Never sign a format 1 record for a card you know in format 2.

The record has its own signature over the identity-version prefix and canonical JSON. The request signature and record signature serve different checks. record.n is the version number. The permanent card number stays in the endpoint path. A new version keeps the previous commitment in prev and preserves the history of keys and links.

  • v is 1. identity is base64url of a fresh 16-byte token for the first version and stays the same thereafter. n starts at 1 and increases by one. prev is null for version 1 and the previous commitment in standard base64 for later versions.
  • name is the display name. time is Unix seconds within five minutes of the server clock. type is person or organization, and a record without it is a person. alias is optional, lowercase and governed by the alias availability endpoint.
  • keys is the full key history. Each item carries key, alg, device, role, since and revoked. role is device or recovery. since and revoked are version numbers, and revoked is null while the key is active.
  • links is the full account history. Each item carries platform, subject, display, since, removed and its grant when added. Removed entries remain in later records. Use the linking APIs to get the grant before publishing.
  • A person can carry orgs for accepted memberships, and an organisation carries members with their roles. Keep their history and the acceptance grants. Publishing an invitation alone does not make it an accepted membership.
  • logo is optional for either card type. A new version names the image by hash: type, sha256 (its SHA-256 in 64 lowercase hex digits) and size (its length in bytes, up to 2 MiB). When the image is new, send it beside the record as logoData, in standard padded base64. Use image/svg+xml, image/png or image/gif. Versions published before 27 October 2026 carry type and data instead, and stay valid. The preview API checks the image before publication.