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.
error tracker.
key.
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.
logging request context or errors.
- 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
- Strip the fragment before HTTP fetch, then decrypt locally with the fragment
- Owner fragments grant mutation rights and must be treated like credentials.
mutateKeyfrom private share creation andownerKeyfrom public/unlisted- Redact
X-Ctx-Mutate-Key,X-Ctx-Owner-Key,#key, and#ownerbefore
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, CLI, or MCP server for crypto operations. Only use raw HTTP if implementing the crypto contract in public/client.ts exactly:
per-share salt.
inside encryptedMeta.
keys, owner capabilities, or signing private keys to the hosted backend.
- 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
- Encrypt private content with AES-256-GCM and the body
iv. - Encrypt private metadata separately with AES-256-GCM and a metadata IV stored
- Never send plaintext private content, plaintext private metadata, decrypt
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.
signing requests, verification requests, and the private JSON upload fallback.
salt, iv, and encrypted metadata.
encrypted metadata. The JSON body carries bodyBase64 ciphertext and uploadMeta.
encrypted metadata is too large to safely return in X-Ctx-Meta.
deletes, and owner-marked comments.
verification metadata.
for conditional public/unlisted PATCH and DELETE.
Content-Type: application/octet-stream: encrypted binary private upload body.Content-Type: application/json: public/unlisted JSON payloads, comments,X-Ctx-Meta: base64 JSON metadata envelope for private uploads. ContainsX-Ctx-Upload-Format: json: ctx.cat private upload fallback for largeX-Ctx-Meta-Location: response header pointing to/<id>/metawhenX-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,X-Ctx-Mode:publicorunlistedfor plaintext share creation.X-Ctx-Signature-Public-Key: SSH public key used for optional publicX-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*
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.
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:
for private shares
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 sendprivate
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.
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, CLI, or MCP tools. 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"
},
"ok": true,
"service": "ctx-cat-api",
"source": {
"algorithm": "sha256",
"digest": "<64 lowercase hex characters>",
"fileCount": 28,
"format": "ctx-cat-source-archive-v1",
"status": "recorded"
},
"storage": "configured",
"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. The endpoint fails closed with 503, ok: false, and source.status: "invalid" if the manifest is missing or malformed.
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
Reports whether remote signing is enabled and whether a local signing key is available. Hosted ctx.cat defaults to remote signing disabled.
POST /signing/sign
This endpoint is disabled on hosted ctx.cat to prevent plaintext exposure and returns 403. It creates a server-side signature envelope only when CTX_CAT_ENABLE_REMOTE_SIGNING=true in local, self-hosted, or explicitly trusted deployments. Hosted clients should use local signing instead so plaintext does not leave the user environment.
POST /signing/verify-github-key
Checks whether a signing SSH public key is published for a claimed GitHub user and optionally whether the user belongs to an allowed org.
Request:
{
"githubUser": "octocat",
"publicKey": "ssh-ed25519 AAAA..."
}
Response includes a verification state, key fingerprint, and optional org state.
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. A supplied owner-capability header selects owner headroom before verification; invalid capabilities still count against the combined pool while they are checked.
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
status code.
X-Ctx-Owner-Key.
issue text, analytics, or prompts to an LLM.
Safe example: Failed to PATCH share Ab3XyZ9kLm with HTTP 403. Unsafe example: a full ctx.cat URL with #key, an ownerKey, a mutateKey, or an owner/mutate header value.
- 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
- Never log fragment keys,
mutateKey,ownerKey,X-Ctx-Mutate-Key, or - Never include full URLs with
#fragments in stack traces, support tickets, - If asking an LLM to help debug an integration, redact all capabilities first.
Rate Limiting And Retries
safe to repeat.
response can create duplicates. For v1 PUT, retain the same client-chosen ID and reconcile an ambiguous 5xx; do not create a new ID automatically.
context. Safe example: Retry 2/5 for PATCH Ab3XyZ9kLm after 429.
- Use exponential backoff for transient
5xxerrors. - Do not retry
4xxerrors except429when the operation is independently - Respect
Retry-Afterwhen present. - Do not automatically retry share-creation
POSTrequests. A lost success - When logging retry attempts, redact all capabilities and fragments from
Errors
Errors are JSON on the API host:
{
"error": "Invalid mutate key"
}
Common statuses:
required fields.
record.
when repeating the operation is safe.
400: invalid JSON, invalid metadata, malformed expiration, or missing403: invalid owner or mutate capability.404: share or route not found.412: optionalIf-Matchprecondition did not match the current plaintext413: payload larger than the configured body limit.429: write concurrency/byte budget is saturated; honorRetry-Afteronly500: unexpected server error.