Skip to content

Guest drops

Stable

Generated 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.

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.

ParameterInTypeDescription
opaque_idpathstringRequired. The opaque id.

Request body (required, application/json): CreateDropRequest

StatusBodyDescription
201CreateDropResponse
application/json
A drop session is open and accepting chunks.
400CodedProblem
application/problem+json
Bad Request.
403CodedProblem
application/problem+json
Passphrase required.
404CodedProblem
application/problem+json
Not found.
409CodedProblem
application/problem+json
Link capacity exhausted.
413FileTooLargeProblem
application/problem+json
File too large.
415CodedProblem
application/problem+json
Unsupported Media Type.
422CodedProblem
application/problem+json
Unprocessable Entity.
429CodedProblem
application/problem+json
Too many requests.
500CodedProblem
application/problem+json
Internal server error.

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.

ParameterInTypeDescription
X-Capsule-Checksumheaderstring | nullThe chunk’s SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined without it.
X-Capsule-Offsetheaderstring | nullWhere in the blob this chunk starts.
opaque_idpathstringRequired. The opaque id of the link the session belongs to.
upload_idpathstringRequired. The session id.

Request body (required, application/octet-stream): string

StatusBodyDescription
204the request succeeded and there is no content to send. Headers: X-Capsule-Offset.
400CodedProblem
application/problem+json
Bad Request.
404CodedProblem
application/problem+json
Not found.
409CodedProblem
application/problem+json
Chunk refused.
413the request body exceeds the configured limit.
415CodedProblem
application/problem+json
Unsupported media type.
500CodedProblem
application/problem+json
Internal server error.

The caller’s pending drops.

Authentication: required — bearer.

StatusBodyDescription
200InboxResponse
application/json
OK.
401CodedProblem
application/problem+json
Unauthorized. Headers: WWW-Authenticate.
403CodedProblem
application/problem+json
Forbidden.
413the request body exceeds the configured limit.
500CodedProblem
application/problem+json
Internal server error.

Provision an upload link.

Authentication: required — bearer.

Request body (required, application/json): ProvisionLinkRequest

StatusBodyDescription
201ProvisionLinkResponse
application/json
The upload link is provisioned and accepting drops.
400CodedProblem
application/problem+json
Bad Request.
401CodedProblem
application/problem+json
Unauthorized. Headers: WWW-Authenticate.
403CodedProblem
application/problem+json
Forbidden.
413the request body exceeds the configured limit.
415CodedProblem
application/problem+json
Unsupported Media Type.
422CodedProblem
application/problem+json
Unprocessable Entity.
500CodedProblem
application/problem+json
Internal server error.

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.

ParameterInTypeDescription
opaque_idpathstringRequired. The opaque id.
StatusBodyDescription
204the request succeeded and there is no content to send.
400CodedProblem
application/problem+json
Bad Request.
401CodedProblem
application/problem+json
Unauthorized. Headers: WWW-Authenticate.
403CodedProblem
application/problem+json
Forbidden.
413the request body exceeds the configured limit.
500CodedProblem
application/problem+json
Internal server error.

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.

ParameterInTypeDescription
drop_idpathstringRequired. The drop’s identifier.
StatusBodyDescription
204the request succeeded and there is no content to send.
400CodedProblem
application/problem+json
Bad Request.
401CodedProblem
application/problem+json
Unauthorized. Headers: WWW-Authenticate.
403CodedProblem
application/problem+json
Forbidden.
404CodedProblem
application/problem+json
Not found.
413the request body exceeds the configured limit.
500CodedProblem
application/problem+json
Internal server error.

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.

ParameterInTypeDescription
drop_idpathstringRequired. The drop’s identifier.

Request body (required, application/json): AdoptRequest

StatusBodyDescription
200AdoptResponse
application/json
OK.
400CodedProblem
application/problem+json
Bad Request.
401CodedProblem
application/problem+json
Unauthorized. Headers: WWW-Authenticate.
403CodedProblem
application/problem+json
Forbidden.
404CodedProblem
application/problem+json
Not found.
413the request body exceeds the configured limit.
415CodedProblem
application/problem+json
Unsupported Media Type.
422CodedProblem
application/problem+json
Unprocessable Entity.
500CodedProblem
application/problem+json
Internal server error.

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.

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.

FieldTypeDescription
album_idstringRequired. The album to adopt into.
asset_idstringRequired. The asset the drop becomes.
sizeintegerRequired. The blob’s declared size — the inbox row’s, restated and checked against it.
hashstringRequired. The ciphertext hash, which must name this drop’s blob.
content_typestringRequired. The declared content type.
crypto_suite_idintegerRequired. The crypto suite.
protocol_versionstringRequired. The protocol version the manifest is written against.
key_modestringRequired. How the asset’s key is carried. derived or wrapped (invariant 32).
manifest_envelopeManifestEnvelopeRequired. The signed manifest envelope, verbatim.

What adoption produced.

FieldTypeDescription
asset_idstringRequired. The asset the drop became.

An RFC 9457 problem detail.

FieldTypeDescription
typestringRequired.
titlestring
statusintegerRequired.
detailstring
instancestring
codestringRequired. The stable error.* catalog code. The client localizes this; detail stays English. Present on every problem this server renders.

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.

FieldTypeDescription
content_typestringRequired. The declared content type, from the link’s pinned protocol enum (invariant 27).
sizeintegerRequired. The ciphertext’s total length.
ciphertext_hashstringRequired. The lowercase-hex SHA-256 finalization verifies against.
kem_ctstringRequired. K encapsulated to the link’s Drop Key, base64. Length fixed by the suite (invariant 30).
passphrase_proofstring | nullThe 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_filenamestring | nullGuest-supplied and unverified. Advisory only.

The session a guest uploads into.

FieldTypeDescription
upload_idstringRequired. The session id, and the last path segment of the chunk endpoint.
suggested_chunk_sizeintegerRequired. The chunk size to start with.

An RFC 9457 problem detail.

FieldTypeDescription
typestringRequired.
titlestring
statusintegerRequired.
detailstring
instancestring
codestringRequired. The stable error.* catalog code. The client localizes this; detail stays English. Present on every problem this server renders.
limitintegerThe largest file this drop link accepts, in bytes.

One drop waiting for the owner.

FieldTypeDescription
drop_idstringRequired. The drop’s identifier, which adoption and discard name.
opaque_idstringRequired. The link it arrived through.
ciphertext_hashstringRequired. The ciphertext’s content address.
sizeintegerRequired. How many bytes.
content_typestringRequired. The guest’s declared content type.
kem_ctstringRequired. K encapsulated to the link’s Drop Key, base64. The owner decapsulates.
suggested_filenamestring | nullGuest-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_atstringRequired. When it landed, RFC 3339.
adoptingbooleanRequired. 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.

The owner’s pending drops.

FieldTypeDescription
dropsInboxEntryResponse[]Required. Everything waiting, oldest first.

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.

FieldTypeDescription
crypto_suite_idintegerRequired. The crypto suite the blob was sealed under. Must equal the top-level declaration.
protocol_versionstringRequired. The protocol date the manifest was written under (YYYY-MM-DD).
album_idstring | nullThe album the asset belongs to. Must equal the top-level declaration.
file_idstringRequired. The asset this blob belongs to — the same id across the bundle’s members.
amk_versionintegerRequired. The album-key epoch the manifest was written under.
ciphertext_hashstringRequired. 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_sizeintegerRequired. The plaintext length the manifest commits to.
chunk_sizeintegerRequired. The STREAM plaintext chunk size.
key_modestringRequired. derived or wrapped.
metadata_blob_hashstring | nullThe content hash of the bundle’s metadata blob, when the manifest commits to one.
original_blob_hashstring | nullThe 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_userstringRequired. The account that created the asset.
created_by_devicestringRequired. The device that created it, as a UUID — invariant 7’s subject.
client_versionstringRequired. The client build that wrote the manifest.
timestampstringRequired. The manifest’s self-asserted RFC3339 timestamp — invariants 7 and 8’s subject.
actionstringRequired. The lifecycle action. create or replace on this surface — the two that move blob bytes — and see [GateReject::ActionNotAllowed] for the rest.
prior_provenance_hashstring | nullThe provenance chain position this write continues from.
retention_untilstring | nullThe retention floor the manifest carries, when it carries one.

A link the owner is provisioning.

FieldTypeDescription
opaque_idstringRequired. The 128-bit opaque id, 32 lowercase hex characters, from the client’s CSPRNG.
drop_pubkeystringRequired. The Drop Key’s public half, base64. Opaque here — the server never decapsulates.
crypto_suite_idintegerRequired. The suite a drop must be sealed under.
expires_atstring | nullWhen the link stops accepting drops, RFC 3339.
max_total_bytesinteger | nullCumulative bytes across every drop on this link.
max_file_countinteger | nullHow many files the link may deposit.
max_file_sizeinteger | nullThe largest single file.
single_usebooleanWhether the link dies after its first successful drop.
passphrase_verifierstring | nullAn 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.

Confirmation that a link is live.

FieldTypeDescription
opaque_idstringRequired. The opaque id, echoed.