Guest drops
StableGenerated from capsule-server/openapi.json, the OpenAPI 3.2 document
capsule-server emits and mise run openapi-check-kynos keeps current. To change a
description on this page, change the annotation on the handler or model it comes from
and regenerate — this file is build output. The auth model, error contract, and
conventions common to every endpoint are on the REST API overview.
POST /d/{opaque_id}
Section titled “POST /d/{opaque_id}”Open a drop session through a link.
Invariants 26–30 in order: the link admits the file and reserves its caps in one store operation, then the owner’s quota is charged, then the declaration is checked.
Authentication: none.
| Parameter | In | Type | Description |
|---|---|---|---|
opaque_id | path | string | Required. The opaque id. |
Request body (required, application/json): CreateDropRequest
| Status | Body | Description |
|---|---|---|
201 | CreateDropResponse application/json | A drop session is open and accepting chunks. |
400 | CodedProblem application/problem+json | Bad Request. |
403 | CodedProblem application/problem+json | Passphrase required. |
404 | CodedProblem application/problem+json | Not found. |
409 | CodedProblem application/problem+json | Link capacity exhausted. |
413 | FileTooLargeProblem application/problem+json | File too large. |
415 | CodedProblem application/problem+json | Unsupported Media Type. |
422 | CodedProblem application/problem+json | Unprocessable Entity. |
429 | CodedProblem application/problem+json | Too many requests. |
500 | CodedProblem application/problem+json | Internal server error. |
PATCH /d/{opaque_id}/{upload_id}
Section titled “PATCH /d/{opaque_id}/{upload_id}”Append one chunk to a drop session.
The link is the credential: possession of the opaque id, plus a session that belongs to it.
Everything after that is [crate::upload::chunk::append] — the album path’s own function.
Authentication: none.
| Parameter | In | Type | Description |
|---|---|---|---|
X-Capsule-Checksum | header | string | null | The chunk’s SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined without it. |
X-Capsule-Offset | header | string | null | Where in the blob this chunk starts. |
opaque_id | path | string | Required. The opaque id of the link the session belongs to. |
upload_id | path | string | Required. The session id. |
Request body (required, application/octet-stream): string
| Status | Body | Description |
|---|---|---|
204 | — | the request succeeded and there is no content to send. Headers: X-Capsule-Offset. |
400 | CodedProblem application/problem+json | Bad Request. |
404 | CodedProblem application/problem+json | Not found. |
409 | CodedProblem application/problem+json | Chunk refused. |
413 | — | the request body exceeds the configured limit. |
415 | CodedProblem application/problem+json | Unsupported media type. |
500 | CodedProblem application/problem+json | Internal server error. |
GET /v1/drops
Section titled “GET /v1/drops”The caller’s pending drops.
Authentication: required — bearer.
| Status | Body | Description |
|---|---|---|
200 | InboxResponse application/json | OK. |
401 | CodedProblem application/problem+json | Unauthorized. Headers: WWW-Authenticate. |
403 | CodedProblem application/problem+json | Forbidden. |
413 | — | the request body exceeds the configured limit. |
500 | CodedProblem application/problem+json | Internal server error. |
POST /v1/drops/links
Section titled “POST /v1/drops/links”Provision an upload link.
Authentication: required — bearer.
Request body (required, application/json): ProvisionLinkRequest
| Status | Body | Description |
|---|---|---|
201 | ProvisionLinkResponse application/json | The upload link is provisioned and accepting drops. |
400 | CodedProblem application/problem+json | Bad Request. |
401 | CodedProblem application/problem+json | Unauthorized. Headers: WWW-Authenticate. |
403 | CodedProblem application/problem+json | Forbidden. |
413 | — | the request body exceeds the configured limit. |
415 | CodedProblem application/problem+json | Unsupported Media Type. |
422 | CodedProblem application/problem+json | Unprocessable Entity. |
500 | CodedProblem application/problem+json | Internal server error. |
DELETE /v1/drops/links/{opaque_id}
Section titled “DELETE /v1/drops/links/{opaque_id}”Revoke one of the caller’s links.
Indistinguishable and idempotent, for the same reason a share revocation is: saying “there was nothing to revoke” would be a lookup.
Authentication: required — bearer.
| Parameter | In | Type | Description |
|---|---|---|---|
opaque_id | path | string | Required. The opaque id. |
| Status | Body | Description |
|---|---|---|
204 | — | the request succeeded and there is no content to send. |
400 | CodedProblem application/problem+json | Bad Request. |
401 | CodedProblem application/problem+json | Unauthorized. Headers: WWW-Authenticate. |
403 | CodedProblem application/problem+json | Forbidden. |
413 | — | the request body exceeds the configured limit. |
500 | CodedProblem application/problem+json | Internal server error. |
DELETE /v1/drops/{drop_id}
Section titled “DELETE /v1/drops/{drop_id}”Discard a pending drop.
The bytes become unreferenced and the collector reclaims them; the link’s cap is not refunded, because the drop did happen — a guest deposited a file and the owner chose not to keep it, which is not the same as a link slot never having been used.
Authentication: required — bearer.
| Parameter | In | Type | Description |
|---|---|---|---|
drop_id | path | string | Required. The drop’s identifier. |
| Status | Body | Description |
|---|---|---|
204 | — | the request succeeded and there is no content to send. |
400 | CodedProblem application/problem+json | Bad Request. |
401 | CodedProblem application/problem+json | Unauthorized. Headers: WWW-Authenticate. |
403 | CodedProblem application/problem+json | Forbidden. |
404 | CodedProblem application/problem+json | Not found. |
413 | — | the request body exceeds the configured limit. |
500 | CodedProblem application/problem+json | Internal server error. |
POST /v1/drops/{drop_id}/adopt
Section titled “POST /v1/drops/{drop_id}/adopt”Adopt a pending drop into an album.
Invariant 32. The manifest re-runs the create battery — a drop that skipped it would be the
one write on this server that entered an album unvalidated — and its ciphertext_hash must
name a blob in the caller’s own inbox, which is what stops an adoption from minting an
asset over somebody else’s bytes.
The row is claimed, written, then settled. Across two ports there is no transaction, and
the two failure directions are not equal: writing first and deleting after can duplicate a
photo, taking first and failing to write loses one. A claim leaves a crash visible in the
owner’s own inbox instead, marked adopting.
Authentication: required — bearer.
| Parameter | In | Type | Description |
|---|---|---|---|
drop_id | path | string | Required. The drop’s identifier. |
Request body (required, application/json): AdoptRequest
| Status | Body | Description |
|---|---|---|
200 | AdoptResponse application/json | OK. |
400 | CodedProblem application/problem+json | Bad Request. |
401 | CodedProblem application/problem+json | Unauthorized. Headers: WWW-Authenticate. |
403 | CodedProblem application/problem+json | Forbidden. |
404 | CodedProblem application/problem+json | Not found. |
413 | — | the request body exceeds the configured limit. |
415 | CodedProblem application/problem+json | Unsupported Media Type. |
422 | CodedProblem application/problem+json | Unprocessable Entity. |
500 | CodedProblem application/problem+json | Internal server error. |
Schemas
Section titled “Schemas”The models these endpoints carry. A field whose type names another model links to it when this page documents that model, which it does when some path from an operation reaches it within 4 references. A model only ever reached deeper than that is named without being expanded.
AdoptRequest
Section titled “AdoptRequest”The owner’s signed create over a drop already in their inbox.
The same shape a POST /v1/upload create carries, minus everything about transferring bytes:
the blob is already committed, so there is no size to negotiate and no session to open. What
remains is the manifest, which is the whole point — a drop becomes an asset only when the
owner signs for it.
| Field | Type | Description |
|---|---|---|
album_id | string | Required. The album to adopt into. |
asset_id | string | Required. The asset the drop becomes. |
size | integer | Required. The blob’s declared size — the inbox row’s, restated and checked against it. |
hash | string | Required. The ciphertext hash, which must name this drop’s blob. |
content_type | string | Required. The declared content type. |
crypto_suite_id | integer | Required. The crypto suite. |
protocol_version | string | Required. The protocol version the manifest is written against. |
key_mode | string | Required. How the asset’s key is carried. derived or wrapped (invariant 32). |
manifest_envelope | ManifestEnvelope | Required. The signed manifest envelope, verbatim. |
AdoptResponse
Section titled “AdoptResponse”What adoption produced.
| Field | Type | Description |
|---|---|---|
asset_id | string | Required. The asset the drop became. |
CodedProblem
Section titled “CodedProblem”An RFC 9457 problem detail.
| Field | Type | Description |
|---|---|---|
type | string | Required. |
title | string | — |
status | integer | Required. |
detail | string | — |
instance | string | — |
code | string | Required. The stable error.* catalog code. The client localizes this; detail stays English. Present on every problem this server renders. |
CreateDropRequest
Section titled “CreateDropRequest”A guest’s declared drop.
No album_id, no amk_version, no manifest, no provenance. deny_unknown_fields is
what enforces invariant 30’s absence clause: a drop that names an album is a 400 rather
than a field the server quietly ignores, because ignoring it would let a guest believe they
had written into an album.
| Field | Type | Description |
|---|---|---|
content_type | string | Required. The declared content type, from the link’s pinned protocol enum (invariant 27). |
size | integer | Required. The ciphertext’s total length. |
ciphertext_hash | string | Required. The lowercase-hex SHA-256 finalization verifies against. |
kem_ct | string | Required. K encapsulated to the link’s Drop Key, base64. Length fixed by the suite (invariant 30). |
passphrase_proof | string | null | The Argon2id proof for a passphrase-gated link, base64. Required exactly when the link carries a verifier, and absent otherwise. The passphrase itself is never transmitted: what travels is the derived proof, and the KDF’s cost is what rate-limits guessing on top of the per-link limiter. It gates a write and adds no confidentiality — the guest already encrypts every asset. What it limits is who may spend the owner’s quota (Web Upload). |
suggested_filename | string | null | Guest-supplied and unverified. Advisory only. |
CreateDropResponse
Section titled “CreateDropResponse”The session a guest uploads into.
| Field | Type | Description |
|---|---|---|
upload_id | string | Required. The session id, and the last path segment of the chunk endpoint. |
suggested_chunk_size | integer | Required. The chunk size to start with. |
FileTooLargeProblem
Section titled “FileTooLargeProblem”An RFC 9457 problem detail.
| Field | Type | Description |
|---|---|---|
type | string | Required. |
title | string | — |
status | integer | Required. |
detail | string | — |
instance | string | — |
code | string | Required. The stable error.* catalog code. The client localizes this; detail stays English. Present on every problem this server renders. |
limit | integer | The largest file this drop link accepts, in bytes. |
InboxEntryResponse
Section titled “InboxEntryResponse”One drop waiting for the owner.
| Field | Type | Description |
|---|---|---|
drop_id | string | Required. The drop’s identifier, which adoption and discard name. |
opaque_id | string | Required. The link it arrived through. |
ciphertext_hash | string | Required. The ciphertext’s content address. |
size | integer | Required. How many bytes. |
content_type | string | Required. The guest’s declared content type. |
kem_ct | string | Required. K encapsulated to the link’s Drop Key, base64. The owner decapsulates. |
suggested_filename | string | null | Guest-supplied and unverified. A guest chose this text. A client rendering it treats it as untrusted input — it is the one field on this surface an anonymous party authored. |
received_at | string | Required. When it landed, RFC 3339. |
adopting | boolean | Required. Whether an adoption currently holds this row. Surfaced rather than hidden: a crash between claim and settle leaves a row here, and an owner who cannot see it cannot act on it. |
InboxResponse
Section titled “InboxResponse”The owner’s pending drops.
| Field | Type | Description |
|---|---|---|
drops | InboxEntryResponse[] | Required. Everything waiting, oldest first. |
ManifestEnvelope
Section titled “ManifestEnvelope”The server-visible mirror of the signed manifest’s envelope fields, as declared at
POST /v1/upload.
Strict (deny_unknown_fields) like the rest of the transport JSON. The Postel asymmetry
the design draws — tolerant inside documents that outlive us, strict on the wire we own —
puts unknown-key tolerance in the signed CBOR interiors, never in this JSON projection.
| Field | Type | Description |
|---|---|---|
crypto_suite_id | integer | Required. The crypto suite the blob was sealed under. Must equal the top-level declaration. |
protocol_version | string | Required. The protocol date the manifest was written under (YYYY-MM-DD). |
album_id | string | null | The album the asset belongs to. Must equal the top-level declaration. |
file_id | string | Required. The asset this blob belongs to — the same id across the bundle’s members. |
amk_version | integer | Required. The album-key epoch the manifest was written under. |
ciphertext_hash | string | Required. The ciphertext content hash, lowercase hex. Must equal the top-level hash. This names the blob this session is uploading, not the manifest’s own ciphertext_hash. For the original the two coincide; for a metadata or provenance session they do not, and the projection reuses the manifest’s field name for a per-blob declaration. Invisible for a create, because the bundle is assembled in a pending row nobody can see and no member has to name another. It is not invisible for a replace, which is why [Self::original_blob_hash] exists (S-C43). |
plaintext_size | integer | Required. The plaintext length the manifest commits to. |
chunk_size | integer | Required. The STREAM plaintext chunk size. |
key_mode | string | Required. derived or wrapped. |
metadata_blob_hash | string | null | The content hash of the bundle’s metadata blob, when the manifest commits to one. |
original_blob_hash | string | null | The content hash of the bundle’s original blob, when the manifest commits to one (S-C43). The manifest’s own ciphertext_hash, under a name that cannot be confused with [Self::ciphertext_hash]‘s per-session meaning. Optional on the wire and required on a replace: a replace re-points roles that already have bytes, so it has to be applied as one act, and the only member of the bundle that can carry the whole change is the manifest — which therefore has to be able to name the original it commits to. A create may omit it. Its bundle is assembled incrementally in a row nobody can see, so no member needs to name another and requiring it would be a wire change for no gain. |
created_by_user | string | Required. The account that created the asset. |
created_by_device | string | Required. The device that created it, as a UUID — invariant 7’s subject. |
client_version | string | Required. The client build that wrote the manifest. |
timestamp | string | Required. The manifest’s self-asserted RFC3339 timestamp — invariants 7 and 8’s subject. |
action | string | Required. The lifecycle action. create or replace on this surface — the two that move blob bytes — and see [GateReject::ActionNotAllowed] for the rest. |
prior_provenance_hash | string | null | The provenance chain position this write continues from. |
retention_until | string | null | The retention floor the manifest carries, when it carries one. |
ProvisionLinkRequest
Section titled “ProvisionLinkRequest”A link the owner is provisioning.
| Field | Type | Description |
|---|---|---|
opaque_id | string | Required. The 128-bit opaque id, 32 lowercase hex characters, from the client’s CSPRNG. |
drop_pubkey | string | Required. The Drop Key’s public half, base64. Opaque here — the server never decapsulates. |
crypto_suite_id | integer | Required. The suite a drop must be sealed under. |
expires_at | string | null | When the link stops accepting drops, RFC 3339. |
max_total_bytes | integer | null | Cumulative bytes across every drop on this link. |
max_file_count | integer | null | How many files the link may deposit. |
max_file_size | integer | null | The largest single file. |
single_use | boolean | Whether the link dies after its first successful drop. |
passphrase_verifier | string | null | An Argon2id verifier, base64, when the link is passphrase-gated. A verifier and never a passphrase: this is an abuse gate the server checks, which is why it is stored here at all — unlike a share link’s passphrase, which protects decryption and which the server never sees in any form. |
ProvisionLinkResponse
Section titled “ProvisionLinkResponse”Confirmation that a link is live.
| Field | Type | Description |
|---|---|---|
opaque_id | string | Required. The opaque id, echoed. |