Skip to content

Hand Permafrost a document to hold.

POST
/v1/api/custody/store
curl --request POST \
--url https://testnet.permafrost.live/v1/api/custody/store \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "file_name": "offer-letter.txt", "text": "The document to be held." }'

The document is encrypted under a key generated for this object alone, the ciphertext is stored, and the key is held on the instance. From here the document can be served back while that key lives, and destroyed by destroying it.

The acknowledgement carries both hashes, because they answer different questions and are not interchangeable:

Field Hash of Present it to
content_hash the plaintext you supplied the certificate, after deletion
stored_hash the ciphertext that was stored GET /v1/api/verify?hash=

stored_hash is null when the storage row is not readable yet. A null means “not recorded” — it is never the other hash in disguise.

The request is size-checked against the tenant’s ceiling before the body is read, so an oversized document is refused on its declared length rather than after being buffered.

Media typeapplication/json

Supply exactly one of text or file_base64. An empty document is refused.

object
file_name

A label for the document. Optional.

string
text

The document as text.

string
file_base64

The document as base64-encoded bytes.

string
Examples

A text document.

{
"file_name": "offer-letter.txt",
"text": "The document to be held."
}

Held. The id is what every other custody route takes.

Media typeapplication/json

The acknowledgement of a stored document. Both hashes are here because they answer different questions.

object
id
required

The custody object identifier. Every other custody route takes it.

string format: uuid
content_hash
required

Sha256 of the plaintext. This is what the certificate names.

string | null
stored_hash
required

Sha256 of the ciphertext as the storage layer recorded it. This is the hash GET /v1/api/verify?hash= indexes. null means the storage row is not readable yet — never a substitute hash.

string | null
Example
{
"id": "3f2a9c18-5b7e-4d61-9a0c-8e2f1d4b6a37",
"content_hash": "7d1a2b3c4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9",
"stored_hash": "0c9b8a7d6e5f40312213a4b5c6d7e8f90a1b2c3d4e5f6071829304b5c6d7e8fa"
}

Neither text nor file_base64 was supplied, or the document was empty. The body carries a code and never a stack or a path.

Media typeapplication/json

The refusal shape. detail is present on a few routes and deliberately absent from the custody surface, where a detail string could carry an internal path.

object
error
required

The code, or a short fixed sentence.

string
detail

A human-readable note, where a route carries one.

string
Example
{
"error": "empty document"
}

No tenant on the request. Through the public edge this is the edge’s own refusal of the credential; the custody surface’s own generic refusal has the same status and carries no hint about which header, which surface, or whether the id exists.

Media typeapplication/json

The refusal shape. detail is present on a few routes and deliberately absent from the custody surface, where a detail string could carry an internal path.

object
error
required

The code, or a short fixed sentence.

string
detail

A human-readable note, where a route carries one.

string
Example
{
"error": "unauthorized"
}

The declared size would take the tenant past its ceiling. This is a size check against the ceiling on this one request, not a running total of everything held on this surface.

Media typeapplication/json
object
error
required
string
limit_mib

The tenant’s ceiling, in MiB.

integer
used_mib

What the tenant is already recorded as holding, in MiB.

integer
Example
{
"error": "quota_exceeded",
"limit_mib": 1024,
"used_mib": 1024
}

The tenant is over an allowance. Retry-After carries whole seconds, rounded up and never zero. The per-tenant refusal also carries retry_after_ms; the custody surface’s own local limiter carries the code alone. Both are per tenant, keyed by the tenant address — one tenant spending its allowance does not consume another’s.

Media typeapplication/json
object
error
required
string
retry_after_ms

How long until one token is available, in milliseconds. Present on the per-tenant refusal.

integer
>= 1
Example
{
"error": "rate_limited",
"retry_after_ms": 240
}
Retry-After
integer
>= 1
Example
1

Whole seconds to wait before retrying.

The document could not be stored. The reason is logged on the instance, not returned.

Media typeapplication/json

The refusal shape. detail is present on a few routes and deliberately absent from the custody surface, where a detail string could carry an internal path.

object
error
required

The code, or a short fixed sentence.

string
detail

A human-readable note, where a route carries one.

string
Example
{
"error": "store failed"
}

Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.