Documentation
REST API
TypeScript SDK
If you are calling this from TypeScript or JavaScript, use the SDK rather than raw HTTP. It is generated from the same spec this page documents, so it cannot drift from the API, and it handles the fiddly parts for you: idempotency keys on creates, ETag round-tripping on updates, cursor pagination, and typed errors you can branch on.
npm install @agentlefs/sdkimport { AgentlefsApiClient } from "@agentlefs/sdk";
const client = new AgentlefsApiClient({ token: process.env.CH_KEY! });
// List what your key can reach
const folders = await client.folders.listFolders();
// Read a document
const doc = await client.documents.retrieveDocument({
path: "onboarding/day-one.md",
folder: "handbook",
});Source and issues: agentlefs-sdk-typescript. TypeScript is the only language published so far; others are generated from the same spec when there is demand.
Published on npm as @agentlefs/sdk, built by CI from the spec above with npm provenance. Works with both import and require, and has no runtime dependencies.
The REST API
/v1 is for your own application code: a backend service, a scheduled job, an internal tool. It speaks plain HTTP and JSON, so any language can call it.
This is a different door from the one on the Connect tab. An agent connects over MCP and decides for itself which tool to call. Application code decides that ahead of time, so this API is built to be predictable instead: stable error codes you can branch on, retries that cannot duplicate, and updates that refuse rather than clobber.
Full reference, including every route and field, is the OpenAPI spec this API serves about itself at /v1/openapi.yaml. It needs no credential, so a client generator can read it directly:
https://agentlefs.com/v1/openapi.yamldocs/api.md in the repository has the same tables as below with worked examples.
| route | does | requires |
|---|---|---|
| GET /v1/documents | list documents in a folder | ?location= |
| POST /v1/documents | create a document | Idempotency-Key |
| GET /v1/documents/{location} | read a document | |
| PUT /v1/documents/{location} | update a document | If-Match |
| DELETE /v1/documents/{location} | delete a document | If-Match |
| GET /v1/folders | list folders, one level | |
| GET /v1/folders/export | download a folder as a zip | ?location= |
| POST /v1/folders | create a folder | Idempotency-Key |
| POST /v1/renames | rename in place | Idempotency-Key |
| POST /v1/moves | move to another folder | Idempotency-Key |
| POST /v1/undeletes | undo the last delete at a location | Idempotency-Key |
| GET /v1/search | search reachable content | ?q= |
| GET /v1/grants | who can reach a scope | ?scope_type=, ?scope_id= |
| POST /v1/grants | grant a role on a scope | Idempotency-Key |
| DELETE /v1/grants | revoke a grant | scope + subject + role |
| GET /v1/health | liveness, no credential needed | |
| GET /v1/openapi.yaml | this API's OpenAPI spec |
Content is addressed by ONE location — the whole path from the workspace root, filename included. A folder is a path prefix rather than a container, so handbook and handbook/policies are both folders, and a grant on the first covers the second.
Get an API key
An existing agent token will not work here. A key has to be created for the API deliberately, so that a credential handed to an agent does not silently become a customer-facing one.
- Go to Access and open the principal the integration should act as. Create one if you want its access separate from any person or agent, which is usually what you want.
- In its Tokens card, choose Mint.
- Tick enable for the REST API. The key is shown once, so store it where your application reads its secrets.
API keys do not expire. Paste one into your deployment config and it keeps working, which is the point of a key rather than a session. Set an expiry when minting if you want one; there is no maximum.
Revoking takes effect on the next request, with no cache to wait out, so revocation rather than expiry is the control to reach for when a key should stop working. Rotate to replace a key’s secret without changing anything else about it.
Your first request, which lists what the key can reach:
curl -s "https://agentlefs.com/v1/folders" \
-H "Authorization: Bearer afs_your_api_key"An empty list is a real answer, not a failure. The key reaches only what its principal has been granted, so grant it access in Permissions first.
A 404 does not mean you got the path wrong
If your key is not permitted to read something, the API returns 404 with exactly the response it would give for something that does not exist. The two are byte-identical, on purpose.
That is a security property rather than an oversight. If denied and missing looked different, any key holder could map content they cannot read, one request at a time, just by watching which paths answer differently.
So there is no 403 anywhere on this API and no error code meaning “forbidden”. When you get a 404 you did not expect, the question to ask is whether the principal has been granted access, not whether the path is right.
The same reasoning removes result totals and hidden-item counts. A count of things you cannot see is still information about them.
Every error has this shape:
{
"error": {
"code": "NOT_FOUND",
"message": "not found",
"request_id": "4f8c1e02-9d3a-4a1b-8e77-0b5b2c9a1f34"
}
}Branch on code, never on the message. Messages are for people and are not stable. request_id is also returned as X-Request-Id on every response, so quote it if you need to ask us about a specific call.
| code | status | means |
|---|---|---|
| NOT_FOUND | 404 | absent, OR present and not yours. Deliberately the same. |
| INVALID_REQUEST | 400 | missing field, bad path, bad cursor, missing precondition |
| CONFLICT | 409 | stale If-Match, name collision, key reused, access would widen |
| RATE_LIMITED | 429 | too many requests; honour Retry-After |
| INTERNAL | 500 | ours, not yours. Quote the request_id. |
| UNAUTHENTICATED | 401 | missing, invalid, revoked, expired, or not API-enabled |
Writing safely
Two headers are required on writes, and both exist so that a retry or a race cannot quietly cost you data.
Creating needs Idempotency-Key. Networks fail after the server has already acted, so a retry without a key makes a second document. With one, the retry returns the original result and tells you so.
curl -s -X POST "https://agentlefs.com/v1/documents" \
-H "Authorization: Bearer afs_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"location": "handbook/onboarding/day-one.md",
"content": "# Day one\n\nWelcome."
}'Updating and deleting need If-Match, carrying the ETag you got when you read the document. If someone changed it in between you get a CONFLICT instead of silently overwriting their edit. Send * to overwrite regardless.
# Read first: the ETag is the version you are updating.
ETAG=$(curl -sD- -o /dev/null "https://agentlefs.com/v1/documents/handbook/onboarding/day-one.md" \
-H "Authorization: Bearer afs_your_api_key" | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')
curl -s -X PUT "https://agentlefs.com/v1/documents/handbook/onboarding/day-one.md" \
-H "Authorization: Bearer afs_your_api_key" \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
-d '{"content": "# Day one\n\nUpdated."}'Lists are paginated with an opaque next_cursor. Pass it back to get the next page, and stop when it comes back null. A cursor belongs to the query and the key that produced it, so it cannot be edited or reused elsewhere.
Moving content can change who reaches it, since access follows the path. A move that would widen access is refused with a report of what changes; re-send with acknowledge_access_change: true once that has been reviewed.
Requests are limited per key. Watch X-RateLimit-Remaining, and honour Retry-After on a 429.

