Skip to main content
The Write API stores a record your app produced — a summary, an enrichment, an imported record — in the user’s own Personal Server, where the user can see it, grant it to someone else, and delete it. Your app can write a scope without being able to read it, and without holding the user’s key:
  • The user’s Personal Server registers the record on chain as the owner. Your app never holds the owner key or the scope’s encryption key.
  • A write grant carries a write: prefix and confers no read. write:notes.entries lets an app write that scope and nothing else. Reading it back needs a separate bare entry in the same grant. See Scope grammar.
  • Every record carries who wrote it. Your app signs the exact bytes it stored, and the Personal Server keeps that signature with the record, so authorship is verifiable from the record alone.

The flow

You prove control of your key once per session, then write with a bearer token plus a per-write signature. The token authorizes the write; the signature attributes it.

Prerequisites

Step 1 — Open a write session

The payload is the standard Personal Server request-signing envelope — aud, method, uri, bodyHash, iat, exp — and it must carry a grantId claim naming the write grant. Sign it EIP-191 with your builder key. A bearer token cannot open a write session.
scope lists the grant’s write patterns with the write: prefix stripped — exactly what POST /v1/data/:scope accepts under this token. The handshake checks that your builder address is registered, the grant exists and is not revoked, the grant carries at least one write: entry, the grantee is the handshake signer, and the grantor is this server’s owner. Scope coverage and revocation are checked again on every write, so revoking the grant stops the next write even inside an open session.
Session tokens do not survive a Personal Server restart, or the user closing Vana in the browser. Treat a 401 on a write as “open a new session and retry once”. The SDK does this for you.
A handshake proof is single-use: a proof that already minted a token is refused (WRITE_SESSION_PROOF_REPLAY). Sign a fresh one per handshake.

Step 2 — Write the record

The bearer token authorizes the write. The X-Vana-Write-Signature proof attributes it: your builder key signs aud, method, uri, bodyHash and grantId, so a stored record cannot be moved to another scope or relabelled with a different grant and still verify. The proof imposes three rules:
  • The body must be compact JSON — what JSON.stringify emits, with no insignificant whitespace. Your app signs the bytes that will be stored, so a pretty-printed body is refused with WRITE_BODY_NOT_CANONICAL.
  • The signed uri covers the query string, with parameters sorted by name and then value. Parameter order does not matter; the parameters themselves must match.
  • Repeat requests need a nonce claim. Two identical writes signed in the same second are byte-identical, and the second is refused as a replay (WRITE_ATTRIBUTION_REPLAY). Add a nonce — any unique string up to 128 characters, such as a UUID. Each nonce is single-use.
On success the Personal Server encrypts, uploads and registers the record, and answers 201:
status: "syncing" means the record is stored and its on-chain registration is in progress. See Provenance & verifiability for what lands on chain.

Writing bytes instead of JSON

Any Content-Type other than JSON stores the payload as an unstructured record:
The signature covers the stored $binary record — media type, filename, your metadata, size, content hash, content — not the raw bytes, so the same bytes sent as a JSON write and as a binary write do not share a signature. The maximum body size is 50 MB.

What the Personal Server stamps on the record

The Personal Server adds two reserved keys inside the record’s data: A request body that includes its own $writtenBy or $lineage is rejected with 400 INVALID_BODY. Both keys are excluded when the stored bytes are hashed, so a record with lineage verifies the same way as one without. A third reserved key, $binary, holds the stored form of an unstructured write: media type, filename, your metadata, size, content hash and content. It is returned on a read like any other content.
$writtenBy and $lineage are not returned on a read under a grant, because they would reveal which other sources the user has connected. The owner sees them in their own storage. A grantee reads the derivation through the lineage view, where sources outside the grant are redacted.

Using the SDK

The SDK signs the handshake, serializes the body compactly, adds the per-write proof and its nonce, and opens a new session once on a 401. sessionCoversScope(session, scope) tells you whether a scope is writable under the session before you send anything.

Errors

Limits to design around

  • Append-only. A write registers a new version; there is no edit in place. Model a correction as a new version.
  • Sessions do not survive a restart. Open a new session on 401.
  • A write grant is not a read grant. If your app needs to read what it wrote, the same grant must also carry a bare entry for that scope.
  • The Personal Server must be reachable. For a user on the web app, that means Vana is open in their browser. Writes to an offline server are not queued.