---
title: "Presign reads for stored objects"
method: POST
path: "/vaults/v1/{vault_id}/object-reads"
tags: ["vault"]
---

# Presign reads for stored objects

`POST /vaults/v1/{vault_id}/object-reads`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/vaults/v1/{vault_id}/object-reads`
- Auth: one of:
  - API key in header `SIGN-IN-WITH-X`
  - HTTP bearer
  - HTTP bearer

Ciphertext never streams through the gateway — reads are presigned GETs straight to storage, each returned with the ledger's recorded `stored_bytes_sha256` and `size_bytes` so the client verifies what it downloaded. `key_envelope` reads are RECIPIENT-ONLY: a fingerprint that is not one of the caller's own registered encryption keys is refused `403 VAULT_ACCESS_DENIED`, and that check runs BEFORE the ledger lookup, so the refusal never reveals whether such an envelope exists. An object that is absent, of a different kind, or already tombstoned (`DELETING` / `DELETED` / `PRUNE_AUTHORIZED_PRESENT`) is `404 RESOURCE_NOT_FOUND`. Never lifecycle-gated. Five immutable ciphertext kinds (`wal_pack`, `ref_state`, `retention_roots`, `checkpoint_manifest`, `checkpoint_pack`) additionally carry `edge_url` — a CloudFront-signed URL for the SAME bytes at the SAME `expires_at` — whenever the platform's optional edge cache is configured; every other kind, and every deployment without edge caching configured, simply omits the field. `edge_url` is ADDITIVE, never a replacement for `url`: `url` is always present and unchanged (the S3 presign), so an existing client that ignores `edge_url` keeps working byte-for-byte. A client that prefers the edge tries `edge_url` first and falls back to `url` on any failure (4xx/5xx/timeout), silently, since both name the identical hash-verified bytes. Edge-served bytes are ciphertext only, verified client-side exactly like the S3 presign — the edge is never part of the trust model. A pruned or purged object stops being served from BOTH paths within a bounded window: the origin 404s immediately post-deletion, and the edge cache's own TTL (1 hour) is the declared worst-case bound on a stale edge hit. Every entry additionally MAY carry `inline` — base64url of the object's EXACT stored bytes — present only when `size_bytes` is at or under a fixed per-object cap AND the response's running total inline budget is not yet spent (decided in request order, so earlier entries win the budget); `url`/`edge_url` stay present and unchanged on every entry regardless, and a gateway-side storage read failure for one entry just omits its `inline`, never fails the batch — size-gated, never kind-gated, so `key_envelope` inlines too once past its recipient-only check above.

## Path parameters

- `vault_id` string, required

## Headers

- `SIGN-IN-WITH-X` string, required

## Request body

- object
  - `objects` object[], required
    - `object_kind` string, required
    - `object_id` string
    - `epoch` string
    - `recipient_fingerprint` string

## Response `200`

Presigned reads, in request order

- object
  - `reads` object[]
    - `object_kind` string
    - `object_id` string
    - `epoch` string
    - `recipient_fingerprint` string
    - `url` string: The S3 presign. Absent only on a BYO payload entry, which carries byo_key instead.
    - `edge_url` string: Additive, optional. A CloudFront-signed URL for the SAME bytes as `url`, expiring at the SAME `expires_at`. Present only when the object's kind is edge-eligible (`wal_pack`/`ref_state`/`retention_roots`/`checkpoint_manifest`/`checkpoint_pack`) AND the platform's optional edge cache is configured — omitted (never null) otherwise. Prefer this when present; fall back to `url` on any failure.
    - `expires_at` string, date-time
    - `stored_bytes_sha256` string
    - `size_bytes` string: Decimal byte count as a string (values exceed 2^53).
    - `inline` string: Additive, optional. Base64url of the object's EXACT stored bytes — the identical bytes `url` serves, never re-serialized. Present only when `size_bytes` is at or under a fixed per-object cap AND this response's running total inline budget is not yet spent (decided in request order: earlier entries win the budget). Omitted, never truncated, on an over-cap entry, a budget-spent entry, or one where a gateway-side storage read failed — indistinguishable from an older gateway that predates this field. `url`/`edge_url` remain present and authoritative on every entry regardless.
    - `byo_key` string: storage_profile "byo" vaults only, on the six payload kinds (wal_pack, ref_state, retention_roots, checkpoint_manifest, checkpoint_pack, key_envelope): the object's relative key under the vault's byo_destination — the same key the upload session handed the writer — in place of url/expires_at/edge_url/inline (run402 holds no payload copy to presign). The client reads it from the customer bucket with its own locally configured credential. Absent on managed vaults and on non-payload kinds.

## Other responses

- `403`: Not authorized (VAULT_ACCESS_DENIED)
- `404`: An object is not readable on this vault
- `503`: STORAGE_UNAVAILABLE

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
