ctx.cat API
ctx.cat exposes a small HTTP API for encrypted private shares, plaintext public and unlisted shares, comments, discovery, signing capabilities, and verification. Hosted private-content operations are zero-knowledge: content, metadata, and comments are encrypted client-side before upload, and decrypt keys stay in URL fragments.
Base URLs
- Web:
https://www.ctx.cat - API:
https://api.ctx.cat - Local default:
http://127.0.0.1:8787
Use CTX_CAT_BASE_URL in local clients when targeting staging or a self-hosted backend.
Critical Fragment Handling
URL fragments are client-side secrets. Browsers do not send fragments to servers, and agents must preserve that boundary manually.
- Correct HTTP request:
GET https://api.ctx.cat/Ab3XyZ9kLm - Wrong HTTP request:
GET https://api.ctx.cat/Ab3XyZ9kLm#key=... - Do not send fragments to an LLM, backend, logs, analytics, search index, or error tracker.
- Strip the fragment before HTTP fetch, then decrypt locally with the fragment key.
- Owner fragments grant mutation rights and must be treated like credentials.
mutateKeyfrom private share creation andownerKeyfrom public/unlisted shares are write capabilities. Treat them like decrypt keys: never send them to an LLM, logs, analytics, backend services that do not require them, or error trackers.- Redact
X-Ctx-Mutate-Key,X-Ctx-Owner-Key,#key, and#ownerbefore logging request context or errors.
Common Mistakes
Do not copy these patterns into agents or integrations:
// WRONG: sends or logs a fragment-bearing URL.
await fetch("https://api.ctx.cat/Ab3XyZ9kLm#key=secret");
console.error("failed to fetch", "https://www.ctx.cat/Ab3XyZ9kLm#key=secret");
// WRONG: stores plaintext in a field that must carry encrypted bytes.
await fetch("https://api.ctx.cat/", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Ctx-Upload-Format": "json" },
body: JSON.stringify({ bodyBase64: btoa(plaintext) }),
});
Safe debugging context is a share id, status code, and generic error message: PATCH Ab3XyZ9kLm returned 403. Full URLs with fragments, capability headers, mutateKey, and ownerKey are not safe debugging context.
Crypto Contract
Use the official TypeScript client or CLI for crypto operations. Only use raw HTTP if implementing the published @synthlabs/ctx-cat-client crypto contract exactly:
- Generate a random base64url decrypt key for the URL fragment.
- Derive an AES-256-GCM key from that fragment key using HKDF-SHA-256 and the per-share
salt. - Encrypt private content with AES-256-GCM and the body
iv. - Encrypt private metadata separately with AES-256-GCM and a metadata IV stored inside
encryptedMeta. - Never send plaintext private content, plaintext private metadata, decrypt keys, owner capabilities, or signing private keys to the hosted backend.
Headers
Security: never log X-Ctx-Mutate-Key, X-Ctx-Owner-Key, or any header containing write capabilities. These are secret credentials equivalent to passwords.
Content-Type: application/octet-stream: encrypted binary private upload body.Content-Type: application/json: public/unlisted JSON payloads, comments, signing requests, verification requests, and the private JSON upload fallback.X-Ctx-Meta: base64 JSON metadata envelope for private uploads. Containssalt,iv, and encrypted metadata.X-Ctx-Upload-Format: json: ctx.cat private upload fallback for large encrypted metadata. The JSON body carriesbodyBase64ciphertext anduploadMeta.X-Ctx-Meta-Location: response header pointing to/<id>/metawhen encrypted metadata is too large to safely return inX-Ctx-Meta.X-Ctx-Expires: expiration timestamp ornever.X-Ctx-Mutate-Key: private owner capability for private edits and deletes.X-Ctx-Owner-Key: public/unlisted owner capability for plaintext edits, deletes, and owner-marked comments.X-Ctx-Mode:publicorunlistedfor plaintext share creation.X-Ctx-Signature-Public-Key: SSH public key used for optional public verification metadata.X-Ctx-Signature-Github-User: claimed GitHub username.X-Ctx-Signature-Created-At: author signature timestamp.X-Ctx-Signature-Content-Sha256: signed content hash.X-Ctx-Signature-Metadata-Sha256: signed metadata hash.If-Match: optional strongETag, comma-separated strong-tag list, or*for conditional public/unlistedPATCHandDELETE.
Public/unlisted GET and successful PATCH responses emit a strong ETag. Write-intake saturation returns 429 with Retry-After: 1.
Legacy X-Mux-* request headers are accepted as temporary aliases for old clients, but new integrations should only emit X-Ctx-*. Responses emit X-Ctx-Meta or X-Ctx-Meta-Location.
Multipart upload v1
Multipart private shares use opaque multipart-gzip-v1 descriptor, part, and outer-manifest bytes. Multipart routes are mounted only when durable multipart support is enabled. <id> is the canonical unpadded base64url encoding of 16 bytes (22 characters), and <index> is a canonical decimal integer in the descriptor's part range. The server never receives the reader/decryption key.
This is the complete multipart route table. “Canonical Content-Length” means one decimal header whose value exactly equals the body bytes; chunked bodies do not satisfy these routes.
| Method | Path | Mandatory headers/body | Authentication, response, and replay behavior | | -------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PUT | /v1/shares/<id> | Content-Type: application/json; canonical Content-Length; Content-Digest; If-None-Match: *; X-Ctx-Owner-Commitment; X-Ctx-Upload-Format: multipart-gzip-v1 | No raw capability. Body is the canonical descriptor (at most 4 KiB). New staging returns 201, Location: /v1/shares/<id>, and {"id":"<id>","state":"staged"}; exact replay returns 200. | | GET | /v1/shares/<id> | X-Ctx-Mutate-Key | Owner-only staged/published inventory reconciliation. Returns 200 with canonical multipart-upload-state JSON, X-Ctx-Expires, and Cache-Control: no-store. | | PUT | /v1/shares/<id>/parts/<index> | X-Ctx-Mutate-Key; Content-Type: application/octet-stream; canonical Content-Length; Content-Digest | Body is one 16-byte-to-9-MiB encrypted part. New durable bytes return 201 and {"id":"<id>","index":<index>}; exact replay returns 200. | | PUT | /v1/shares/<id>/complete | X-Ctx-Mutate-Key; Content-Type: application/json; canonical Content-Length; Content-Digest | Body is the canonical outer manifest (at most 64 KiB). First publication returns 201 and {"id":"<id>","state":"published"}; exact replay returns 200. | | GET | /<id> | None | Public reader route after publication. Streams the exact outer-manifest bytes with Content-Type: application/json, Content-Length, Content-Digest, X-Ctx-Upload-Format, X-Ctx-Expires, and Cache-Control: no-store, no-transform. | | GET | /v1/shares/<id>/parts/<index> | None | Public reader route after publication. Streams exact encrypted part bytes with Content-Type: application/octet-stream, Content-Length, Content-Digest, X-Ctx-Upload-Format, and Cache-Control: no-store, no-transform. | | GET | /<id>/comments | None | Published, unexpired shares return 200 and {"comments":[...]}. The request body must be empty. | | POST | /<id>/comments | Content-Type: application/json; canonical Content-Length; optional X-Ctx-Mutate-Key | Published-share body is {"body":"<encrypted>","iv":"<encrypted-comment-iv>"}. A valid capability marks the comment owner; otherwise it is anonymous. Returns 200 with {"comment":...,"success":true}. This route is not replay-idempotent. | | PATCH | /<id> | X-Ctx-Mutate-Key; X-Ctx-Expires; empty body | Owner-only published-share expiry update. X-Ctx-Expires is never or a canonical timestamp. Returns 200 with {"id":"<id>","success":true}. | | DELETE | /<id> | X-Ctx-Mutate-Key; empty body | Owner-only staged/published deletion. Returns 200 with {"id":"<id>","success":true}; an authorized retry against the durable tombstone also returns 200. |
Content-Digest is required on initialize, part upload, and completion and is the RFC 8941 sha-256=:<standard-base64>: digest of the exact HTTP body. A missing, malformed, duplicate, or mismatched digest is 400; an over-limit body is 413, and an incomplete body can return 400 or deadline 408. Missing If-None-Match on initialize returns 428; a present value other than exactly * returns 400.
Exact initialize replay of the same descriptor and owner commitment returns 200; any occupied, deleted, expired, or different durable identity returns 412 and is never replaced. Exact part and completion replays return 200. Different durable part bytes return 409; a different durable manifest returns 409. Completion also validates the manifest against the descriptor and every durable part before publishing.
Missing or invalid X-Ctx-Mutate-Key on owner-only routes is deliberately indistinguishable from absence (404). Authorized inventory of a tombstone is 410; anonymous public reads of staged, expired, deleted, or missing objects are 404. Encoded aliases of share routes are 400. Process/storage admission returns 429 with Retry-After; unavailable multipart storage returns 503 with Retry-After: 1. Mutation responses are durable before success is sent, but a transport failure before a response still requires reconciliation with the same ID and capability.
Client-authoritative create v1
New clients create retry-safe shares with PUT /v1/shares/<id>. The client generates <id> as the canonical, unpadded base64url encoding of 16 random bytes. It also generates a 32-byte owner capability and sends only its commitment on create:
base64url(sha256(UTF8("ctx.cat owner commitment v1\0") || ownerCapabilityBytes))
The owner capability itself is not accepted by this route. Private reader keys remain client-only URL-fragment material as before.
Required request headers are:
If-None-Match: *Content-Digest: sha-256=:<standard-base64 SHA-256 of the exact HTTP body>:X-Ctx-Owner-Commitment: <canonical 32-byte SHA-256 value in base64url>X-Ctx-Mode: public|unlistedfor plaintext shares; omit it or sendprivatefor private shares
The body uses the existing bounded create formats: public/unlisted JSON, private ciphertext plus X-Ctx-Meta, or the private JSON upload envelope. Public and unlisted clients include their immutable createManifest in the JSON body; private clients place it inside encrypted metadata.
Content-Digest is parsed as a bounded RFC 8941 dictionary. A valid field may contain other byte-sequence digest members in any order; the service selects exactly one sha-256 member. Duplicate keys, malformed members, invalid base64, missing SHA-256, or a digest mismatch reject the request.
A successful create returns 201, Location: /v1/shares/<id>, and a body that contains no capabilities:
{ "created": true, "id": "AbCdEf0123456789_-AbCw", "mode": "unlisted" }
Any physical record at that ID, including an expired record or tombstone, returns the same 412 response and is never replaced. A generic 5xx after publication means the outcome is unknown; reconcile before deciding what to do. Do not automatically retry with a new ID. Concurrent same-ID requests are admitted before waiting under process-wide and per-ID count/byte limits. Requests beyond those limits receive 429; a declared body beyond the endpoint limit receives 413.
GET /v1/shares/<id> is the bounded reconciliation read. Supply the raw 32-byte owner capability only for this authenticated request, using X-Ctx-Mutate-Key for private records or X-Ctx-Owner-Key for public/unlisted records. Public/unlisted success returns the normalized stored create fields as JSON. Private success streams raw ciphertext with Content-Length, X-Ctx-Mode: private, X-Ctx-Size, optional epoch-millisecond X-Ctx-Expires, and X-Ctx-Upload-Meta-Sha256. That digest covers UTF-8 JSON.stringify({encryptedMeta,iv,salt}) in the displayed property order, so a client can compare prepared upload metadata without returning it. Every outcome uses Cache-Control: no-store; reconciliation never returns the commitment or a capability. Missing or invalid authorization is indistinguishable from a missing record (404). An authorized tombstone returns 410.
Deleting a v1 record through the existing authenticated DELETE /<id> route writes a durable tombstone. A later PUT cannot resurrect that ID. Legacy POST / remains supported and unchanged, but its server-generated ID and capability response make it non-retryable after an ambiguous transport failure.
### Multipart initialize availability
Multipart storage availability and new-initialize admission are separate. When durable multipart support is enabled but new initialize is paused, a multipart PUT /v1/shares/<id> returns 503, Retry-After: 1, and:
{ "error": "Multipart initialization temporarily unavailable" }
This response means the client should retain its prepared share ID, capabilities, descriptor, and encrypted parts and retry later. It does not mean multipart storage is disabled. Existing multipart occupants remain addressable: published manifests and parts remain readable, staged uploads can continue part upload and owner inventory reconciliation, and completion, expiry, comments, and deletion remain available.
Health reports multipart.enabled: true whenever durable multipart routes are mounted, multipart.initializeEnabled: false when only new initialize is paused, and multipart storage readiness separately. A paused initialize gate does not by itself change health from 200 to 503.
Capability Headers
All write capabilities are secrets.
Share type | Response field | HTTP header | Security level
Private | mutateKey | X-Ctx-Mutate-Key | secret write capability
Public | ownerKey | X-Ctx-Owner-Key | secret write capability
Unlisted | ownerKey | X-Ctx-Owner-Key | secret write capability
It is safe to log a share id such as Ab3XyZ9kLm or a base URL without a fragment. It is not safe to log fragments, full fragment URLs, capability headers, mutateKey, or ownerKey.
Private Shares
Critical: private means you encrypt before upload. The server stores exactly the bytes you send. Never send plaintext as a private share body.
POST /
Creates a private encrypted share when the request omits X-Ctx-Mode.
Request body is ciphertext bytes with X-Ctx-Meta, or a JSON envelope when X-Ctx-Upload-Format: json is set. The server stores ciphertext, encrypted metadata, hashed owner capability, size, expiration, optional public verification metadata, and optional provenance. It never receives the decrypt key.
Agents should use the official client path unless they have implemented the crypto contract above.
Response:
{
"id": "Ab3XyZ9kLm",
"mutateKey": "owner-capability"
}
GET /<id>
Returns ciphertext bytes. Small encrypted metadata is returned in X-Ctx-Meta. Large encrypted metadata is retrieved from GET /<id>/meta when X-Ctx-Meta-Location is present.
GET /<id>/meta
Returns encrypted metadata fields and non-sensitive storage metadata:
{
"encryptedMeta": "...",
"iv": "...",
"salt": "...",
"size": 1234,
"publicVerification": {},
"provenance": {}
}
PATCH /<id>
With X-Ctx-Mutate-Key, updates encrypted content, encrypted metadata, or expiration. The body follows the same private upload format as POST /.
DELETE /<id>
With X-Ctx-Mutate-Key, deletes a private share.
Public And Unlisted Shares
POST /
Creates a plaintext public or unlisted share when X-Ctx-Mode is public or unlisted.
Security: ownerKey grants edit/delete/comment ownership. Treat it as a secret capability. Do not log it, send it to an LLM, send it to analytics, or include it in user-visible errors.
Private mutateKey grants the same class of write access for private shares and must be handled with the same secrecy.
Request:
{
"content": "# notes",
"name": "notes.md",
"signature": {}
}
Response includes an ownerKey. The owner key belongs in the URL fragment and is never needed for normal reading.
GET /<id>
Returns the plaintext record as JSON with a deterministic strong ETag.
PATCH /<id>
With X-Ctx-Owner-Key or X-Ctx-Mutate-Key, updates content, name, expiration, mode, or signature. Send the most recently read ETag in If-Match to prevent a stale writer from overwriting intervening changes. Successful updates return the new ETag; a non-matching or weak tag returns 412 and leaves the record unchanged.
DELETE /<id>
With owner capability, deletes the share. If-Match provides the same optional stale-write protection as PATCH.
Comments
GET /<id>/comments
Returns public/unlisted comments or encrypted private comment records.
POST /<id>/comments
For private shares, send encrypted comment body and iv. For public/unlisted shares, send plaintext content. Owner capability marks the comment as owner; otherwise it is anonymous. Plaintext or encrypted comment content is limited to 64 KiB. Saturated mutation intake returns 429 with Retry-After.
Private comment encryption uses a key derived locally from the private share key and share id. Agents should not implement this crypto manually unless they are matching the TypeScript client exactly. Prefer addPrivateComment from the TypeScript client or CLI. A private comment request shape is:
{
"body": "encrypted-comment-base64",
"iv": "comment-iv-base64"
}
Discovery
GET /public.json
Returns recent public shares. Private and unlisted shares are excluded.
GET /health and GET /healthz
Return service health without exposing secrets:
{
"build": {
"assertion": "environment-supplied",
"sha": "0123456789abcdef0123456789abcdef01234567",
"source": "CTX_BUILD_SHA"
},
"instanceHash": "<64 lowercase hex characters>",
"ok": true,
"service": "ctx-cat-api",
"source": {
"algorithm": "sha256",
"digest": "<64 lowercase hex characters>",
"fileCount": 40,
"format": "ctx-cat-source-archive-v2",
"status": "recorded"
},
"storage": {
"readiness": "ready",
"reasonCodes": []
},
"timestamp": "2026-05-14T00:00:00.000Z"
}
The build SHA is explicitly an environment-supplied assertion. recorded means only that runtime loaded a schema-valid build manifest; it does not claim that the digest matches an external reviewed tree. Deployment verification compares this digest exactly with CTX_EXPECT_SOURCE_DIGEST through bun run healthcheck. instanceHash is an opaque process-lifetime value: it stays stable for one server process and changes when Railway replaces that process, without exposing raw Railway identifiers. Record storage reports starting, ready, or unavailable; unavailable responses include only bounded reasonCodes, never paths or raw filesystem errors. The endpoint fails closed with 503 and ok: false when record or multipart readiness is unavailable, or when the source/client artifact manifest is missing or malformed. Legacy record permission repair reports only the bounded mode-migration-errors, mode-migration-incomplete, or mode-migration-unsafe-record codes. A configured complete-cleanup bound uses the existing cleanup-removal-exhausted or cleanup-scan-exhausted code; an incomplete maintenance sweep reports maintenance-incomplete. Runtime bounds cover scan checkpoints and queued per-record serializer admission; Node cannot cancel an in-flight filesystem syscall. Aggregate server logs may include scan, candidate, hardened, rejected, error, slice, duration, and limit counters, but never record IDs, paths, contents, or raw filesystem exceptions.
The source digest covers exported static documentation but deliberately omits generated public/config.js, whose API and frontend origins are supplied by the deployment environment. Validate /config.js separately.
Signing And Verification
GET /signing/capabilities
Compatibility response that always reports remoteSigningEnabled: false and does not inspect local keys, SSH agents, or GitHub CLI state.
POST /signing/sign
This removed endpoint returns 410. Author signing is supported only by the local CLI and TypeScript client, so plaintext and private signing authority do not cross the API boundary. The former /signing/clear endpoint is removed.
POST /signing/verify-github-key
Checks whether a signing SSH public key is published for a claimed GitHub user at https://github.com/<user>.keys. This public authority is key-only: it never calls GitHub organization APIs, reads GitHub tokens, or reports organization membership. The compatibility response always includes orgVerified: false and orgs: [].
The verifier accepts at most 16 KiB of canonical JSON, admits four concurrent requests, and applies one five-second deadline to each request body and joined verification wait. It single-flights identical inputs, retains at most 256 results, caches verified results for five minutes and negative results for 30 seconds, admits 64 requests per 10-second ingress window, and starts at most 20 GitHub fetches per 10-second server-wide egress window. Overload returns 429 with Retry-After; ingress overload is rejected before reading the body.
Browser CORS is fail-closed. Exact HTTPS viewer origins come from CTX_CAT_GITHUB_VERIFICATION_VIEWER_ORIGINS (comma separated) and CTX_CAT_PUBLIC_BASE_URL; HTTP is accepted only for exact loopback origins. Allowed browser responses echo the exact origin and Vary: Origin; other origin-bearing requests return 403 without an allow-origin header. Server callers without an Origin header remain supported.
Request:
{
"githubUser": "octocat",
"publicKey": "ssh-ed25519 AAAA..."
}
Response:
{ "verified": true, "orgVerified": false, "orgs": [] }
Limits
Default request body limit is 50 MiB and can be changed with CTX_CAT_MAX_BYTES. Large agent traces should use private encrypted uploads. Large encrypted metadata automatically uses the X-Ctx-Upload-Format: json fallback to avoid unsafe HTTP header sizes.
Comment content has a separate fixed 64 KiB limit. Across all server instances in one process, write admission allows at most eight in-flight write requests and 128 MiB of reserved request-body bytes. Anonymous creates and comments share lower ceilings of six requests and 64 MiB, preserving two request slots and 64 MiB for authenticated owner mutations. A missing or invalid Content-Length reserves the endpoint's full configured body limit. Overload is rejected before full body parsing with 429 rather than queued without bound.
Plain-share names are limited to 1 KiB on create and update. Older records with larger names remain directly readable and lifecycle-eligible, but are omitted from /public.json so discovery responses cannot retain upload-sized strings after releasing their file-read admission.
Admission counters are process-local. They do not coordinate multiple OS processes or replicas; the supported deployment boundary remains one API process and one replica until shared admission and storage coordination exist.
Stored-record reads have a separate eight-request limit and charge twice the opened file size before allocating it. Anonymous reads default to a 136 MiB sublimit; reads and writes share a 336 MiB process budget. The server refuses to start if the configured upload limit would prevent one maximum anonymous read or two maximum reads plus one maximum upload. The byte limits can be raised with CTX_CAT_ANONYMOUS_READ_ADMISSION_BYTES and CTX_CAT_PROCESS_ADMISSION_BYTES, but the startup invariants still apply. Rejected reads return 429 with Retry-After: 1. Accepted read responses have a 120-second inactivity timeout, and large private bodies are decoded and sent in bounded backpressure-aware chunks.
Owner verification is separate from large-record admission. Each record has a small immutable authorization sidecar containing only the verifier scheme, verifier, record kind, and (for client-authoritative creates) an idempotent create-replay digest. It never contains a raw owner or mutate capability. Startup creates and privately attests the sidecar namespace, then incrementally indexes existing records before storage readiness can become healthy. Retained scan slices resume every 10 milliseconds for at most 58 seconds during startup. A corrupt, mismatched, incomplete, or unreadable index keeps readiness unavailable. Exact dead-writer publication remnants are restart-reconciled; active or ambiguous remnants remain fail-closed.
At most 16 supplied capabilities can be validated concurrently. A missing owner capability on canonical owner GET, PATCH, and DELETE routes does not enter that lane or read a sidecar: canonical GET /v1/shares/:id returns 404, while PATCH and DELETE return 403. Anonymous comments still validate the sidecar after anonymous record admission. Supplied but invalid capabilities use only the bounded credential-validation lane and sidecar read; canonical reads remain hidden as 404, mutations return 403 for a physically present live record and 404 for a tombstone before and after restart, and an invalid comment capability is treated as anonymous. Credential-lane overload returns 429 with Retry-After: 1.
Only a verified capability may reserve privileged large-record headroom. The opened record must match its immutable sidecar, and every mutation rechecks the same authorization under the per-record serialization lock immediately before publication. Anonymous reads and comments remain charged to anonymous admission.
JSON Upload Fallback
Critical: bodyBase64 must be encrypted ciphertext, not plaintext. Encrypt content first with AES-256-GCM, then base64 encode the ciphertext.
Use the JSON fallback when encrypted metadata would exceed safe header size limits:
{
"bodyBase64": "encrypted-body-base64",
"uploadMeta": {
"encryptedMeta": "encrypted-metadata-base64",
"iv": "body-iv-base64",
"salt": "hkdf-salt-base64"
}
}
Safe Error Handling
- If decryption fails, log only the share id and a generic failure reason.
- If owner or mutate capability validation fails, log only the share id and status code.
- Never log fragment keys,
mutateKey,ownerKey,X-Ctx-Mutate-Key, orX-Ctx-Owner-Key. - Never include full URLs with
#fragments in stack traces, support tickets, issue text, analytics, or prompts to an LLM. - If asking an LLM to help debug an integration, redact all capabilities first. Safe example:
Failed to PATCH share Ab3XyZ9kLm with HTTP 403. Unsafe example: a full ctx.cat URL with#key, anownerKey, amutateKey, or an owner/mutate header value.
Rate Limiting And Retries
- Use exponential backoff for transient
5xxerrors. - Do not retry
4xxerrors except429when the operation is independently safe to repeat. - Respect
Retry-Afterwhen present. - Do not automatically retry share-creation
POSTrequests. A lost success response can create duplicates. For v1PUT, retain the same client-chosen ID and reconcile an ambiguous5xx; do not create a new ID automatically. - When logging retry attempts, redact all capabilities and fragments from context. Safe example:
Retry 2/5 for PATCH Ab3XyZ9kLm after 429.
Errors
Errors are JSON on the API host:
{
"error": "Invalid mutate key"
}
Common statuses:
400: invalid JSON, invalid metadata, malformed expiration, or missing required fields.403: invalid owner or mutate capability.404: share or route not found.412: optionalIf-Matchprecondition did not match the current plaintext record.413: payload larger than the configured body limit.429: write concurrency/byte budget is saturated; honorRetry-Afteronly when repeating the operation is safe.500: unexpected server error.