Hand Permafrost a document to hold.
const url = 'https://testnet.permafrost.live/v1/api/custody/store';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"file_name":"offer-letter.txt","text":"The document to be held."}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”Supply exactly one of text or file_base64. An empty document is refused.
object
A label for the document. Optional.
The document as text.
The document as base64-encoded bytes.
Examples
A text document.
{ "file_name": "offer-letter.txt", "text": "The document to be held."}Bytes, base64-encoded.
{ "file_name": "scan.pdf", "file_base64": "JVBERi0xLjQKJcfs…"}Responses
Section titled “Responses”Held. The id is what every other custody route takes.
The acknowledgement of a stored document. Both hashes are here because they answer different questions.
object
The custody object identifier. Every other custody route takes it.
Sha256 of the plaintext. This is what the certificate names.
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.
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.
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
The code, or a short fixed sentence.
A human-readable note, where a route carries one.
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.
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
The code, or a short fixed sentence.
A human-readable note, where a route carries one.
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.
object
The tenant’s ceiling, in MiB.
What the tenant is already recorded as holding, in MiB.
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.
object
How long until one token is available, in milliseconds. Present on the per-tenant refusal.
Example
{ "error": "rate_limited", "retry_after_ms": 240}Headers
Section titled “Headers”Example
1Whole seconds to wait before retrying.
The document could not be stored. The reason is logged on the instance, not returned.
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
The code, or a short fixed sentence.
A human-readable note, where a route carries one.
Example
{ "error": "store failed"}Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.