# Agent skill (/agent-skill) Read the skill at [`https://agentfs.cloud/agent-onboarding/SKILL.md`](https://agentfs.cloud/agent-onboarding/SKILL.md). Give that URL to an agent that can load remote instructions, or fetch it to inspect: ```bash curl -fsSL https://agentfs.cloud/agent-onboarding/SKILL.md ``` The skill tells the agent to: 1. Set `AGENTFS_API_URL` to `https://agentfs.cloud`. 2. Start the device flow with `POST /v1/auth/device`. 3. Show you `verification_uri_complete` and `user_code`. 4. Poll `POST /v1/auth/device/token` every 5 seconds until you approve the request. 5. Store the returned `api_key` as `AGENTFS_KEY`. 6. Upload through `POST /v1/files` and use the returned file URL. An agent can also connect to the hosted MCP endpoint: ```bash claude mcp add --transport http agentfs https://agentfs.cloud/mcp \ --header "Authorization: Bearer $AGENTFS_KEY" ``` To upload to a project path, it uses: ```bash curl -sS -X POST "$AGENTFS_API_URL/v1/files" \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./report.pdf" \ -F "path=default/report.pdf" ``` The skill also lists the file and project endpoints, the resumable upload flow, private-file access links, and the available MCP tools. It tells the agent to check the HTTP status and problem details before retrying, and to keep `AGENTFS_KEY` out of URLs, files, logs, and user-visible replies. ## Working from a path [#working-from-a-path] An agent remembers the path it wrote, not the file ID. Add these three calls to whatever instructions you give it. To update a file it wrote earlier, send the same path with `if_exists=replace`: ```bash curl -sS -X POST "$AGENTFS_API_URL/v1/files" \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./report.pdf" \ -F "path=default/report.pdf" \ -F "if_exists=replace" ``` That is one call and no lookup. The file keeps its ID, path, and hosted URL, so a link the agent already handed over keeps working. The response is `200` instead of `201`. Without `if_exists`, the same path returns `409 path_exists`. To find a file by the path it was written to: ```bash curl -sS -G "$AGENTFS_API_URL/v1/files" \ -H "Authorization: Bearer $AGENTFS_KEY" \ --data-urlencode "path=default/report.pdf" ``` To read the bytes back, follow the redirect from `GET /v1/files/:id/content`: ```bash curl -sSL "$AGENTFS_API_URL/v1/files/f_8f2c/content" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` See [Files](./files.mdx) for the rest of the path-based operations. # Authentication (/authentication) Use an API key for API requests. Send it as a bearer token in the `Authorization` header: ```bash export AGENTFS_KEY="afs_..." curl -sS https://agentfs.cloud/v1/me \ -H "Authorization: Bearer $AGENTFS_KEY" ``` Keep the key in a secret store. Do not put it in a URL, uploaded file, log, browser code, or user-visible response. A revoked or expired key returns `401 unauthorized`. ## Create a key in the dashboard [#create-a-key-in-the-dashboard] 1. Sign in at [agentfs.cloud](https://agentfs.cloud). 2. Open the project that should own the files. 3. Open **API Keys**, then select **Create key**. 4. Give the key a name. 5. Copy the secret immediately. The full value is not shown again. A key made in the dashboard only reaches its project, and uploads without a path go to that project. Revoke a key from the same page; it stops working immediately. ## Identify an agent [#identify-an-agent] Add these optional headers when you want audit entries to identify a caller: ```bash curl -sS https://agentfs.cloud/v1/me \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "X-Agent-ID: report-agent" \ -H "X-Run-ID: run-42" \ -H "X-Session-ID: session-9" ``` Each value must contain 1–128 printable characters. ## Connect with device authorization [#connect-with-device-authorization] Use the device flow when an agent cannot safely ask you for a key. ### 1. Request a device code [#1-request-a-device-code] No key is needed: ```bash curl -sS -X POST https://agentfs.cloud/v1/auth/device ``` The response includes a device code, a user code, and a page for approval: ```json { "device_code": "device-code-example", "user_code": "7K2M-4Q9R", "verification_uri": "https://agentfs.cloud/device", "verification_uri_complete": "https://agentfs.cloud/device?code=7K2M-4Q9R", "expires_in": 900, "interval": 5, "next": "Show the verification URL and code to the user, then poll the token endpoint every 5 seconds." } ``` Show `verification_uri_complete` and `user_code` to the person who owns the account. They open the page, sign in, enter the code, and optionally select a default project. ### 2. Poll for approval [#2-poll-for-approval] Poll no more often than the returned `interval`: ```bash curl -sS -X POST https://agentfs.cloud/v1/auth/device/token \ -H "content-type: application/json" \ -d '{"device_code":"device-code-example"}' ``` Before approval, the endpoint returns `428 authorization_pending` and a `Retry-After` header. Polling too soon returns `429 slow_down`; use its `Retry-After` value. The device code expires after 15 minutes. After approval, save the key from the response at once: ```json { "type": "account", "id": "org_7ab1", "api_key": "afs_...", "default_project": "research", "next": "Store this value as AGENTFS_KEY. It will not be shown again." } ``` Use the returned value as `AGENTFS_KEY` for later requests. A denied, expired, claimed, or unknown code returns a problem response. # Batch uploads (/batch) Use `POST /v1/batch` when you need to upload several files together. Authenticate with a bearer API key and send an `Idempotency-Key` header. ## Create a batch [#create-a-batch] Send a `manifest` form part containing a JSON array. Each entry maps a form part name to a project path: ```bash title="batch upload" curl -sS -X POST https://agentfs.cloud/v1/batch \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Idempotency-Key: report-set-1" \ -F 'manifest=[{"part":"summary","path":"research/summary.md"},{"part":"data","path":"research/data.csv"}]' \ -F "summary=@./summary.md" \ -F "data=@./data.csv" ``` Each `part` is 1–64 characters and may contain letters, numbers, `_`, and `-`. Each `path` must include a project and filename. The manifest must contain 1–25 entries, and part names must be unique. The request body and the summed file bytes are limited to 104,857,600 bytes (100 MiB). The batch runs to completion inside the request. The response has status `200` and the final batch object: ```json { "type": "batch", "id": "b_4d2a", "status": "failed", "total": 2, "completed": 1, "failed": 1, "items": [ { "part": "summary", "path": "research/summary.md", "file": { "type": "file", "id": "f_8f2c", "name": "summary.md", "path": "research/summary.md", "label": null, "content_type": "text/markdown", "content_disposition": "inline", "size_bytes": 8192, "sha256": "0000000000000000000000000000000000000000000000000000000000000000", "etag": "\"demo-etag\"", "revision": 1, "visibility": "unlisted", "expires_at": null, "tags": [], "metadata": {}, "agent_id": null, "run_id": null, "url": "https://agentfsusercontent.com/f/f_8f2c", "markdown": "[summary.md](https://agentfsusercontent.com/f/f_8f2c)", "created_at": "2026-04-01T12:00:00.000Z", "updated_at": "2026-04-01T12:00:00.000Z" } }, { "part": "data", "path": "research/data.csv", "error": { "code": "path_exists", "detail": "The path research/data.csv already exists." } } ], "created_at": "2026-04-01T12:00:00.000Z" } ``` Each item has a `file`, an `error`, or a `status`. A failed item has this shape: ```json { "part": "data", "path": "research/data.csv", "error": { "code": "path_exists", "detail": "The path research/data.csv already exists." } } ``` The batch status is `completed` when every file stored, and `failed` when any file did not. A batch can finish with some files completed and others failed, so check `items` rather than the status alone. Files use the project's defaults for visibility and expiry. ## Retry safely [#retry-safely] The `Idempotency-Key` is required and may contain up to 200 characters. Repeating the same key with the same manifest and file bytes returns the existing batch with status `200`. Reusing it for a different request returns `409 idempotency_conflict`. ## Inspect a batch [#inspect-a-batch] Call `GET /v1/batch/:id` with the same key that created the batch: ```bash curl -sS https://agentfs.cloud/v1/batch/b_4d2a \ -H "Authorization: Bearer $AGENTFS_KEY" ``` Batch records are retained for 24 hours. An expired record returns `404 batch_not_found`. A batch cannot be cancelled: it has already finished by the time `POST /v1/batch` responds. To undo one, delete the files it stored. # Errors (/errors) The API and hosted-file URLs return errors as `application/problem+json`. This example uses the `/v1` response shape. Hosted-file URLs use a short phrase for `title`—such as `Not found`, `Method not allowed`, `Precondition failed`, `Range not satisfiable`, `Region unavailable`, or `Storage unavailable`—while `code` stays snake\_case. The response has this shape: ```json { "type": "https://docs.agentfs.cloud/errors#storage-limit-exceeded", "title": "storage_limit_exceeded", "status": 413, "code": "storage_limit_exceeded", "detail": "This upload would exceed the account storage limit of 2147483648 bytes.", "retryable": false } ``` `detail` is optional. `retryable` is `true` for responses with a status of 500 or higher, and for `429`. Every `429` and `503` also carries a `Retry-After` header in seconds. The server also sends `Cache-Control: no-store`. ## Handle an error [#handle-an-error] Use the response as a machine-readable instruction. An agent should branch on `status` and `code`, then keep `detail` for a user or log message. | Status | Typical cause | What to do | Retry? | | ------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | | `400`, `401`, `403`, `404`, `409`, `412`, `413`, `422`, `451` | Bad input, missing access, a state conflict, or a blocked file. | Correct the request, refresh the file state, or ask for access. | No. | | `408` | The request took too long to arrive. | Retry with exponential backoff. | Yes. | | `429` | A request or upload limit was reached. | Wait for `Retry-After`, then retry the same safe request. | Yes, after waiting. | | `500`–`599` | AgentFS or its storage dependency could not finish the operation. | Use bounded exponential backoff, and wait for `Retry-After` on a `503`. Preserve the idempotency key for writes. | Yes. | For a complete retry policy and safe-write examples, see [Retries and recovery](./retries.mdx). The table lists codes declared by the API, upload handling, and hosted-file responses. Each code has its own section below so you can link directly to it. | Code | Status | Meaning | | ------------------------------------------------------------- | ---------: | -------------------------------------------------------------------------- | | [access\_denied](#access-denied) | 403 | Device access was denied. | | [api\_key\_create\_failed](#api-key-create-failed) | 500 | The dashboard could not create a key. | | [api\_key\_not\_found](#api-key-not-found) | 404 | The key to revoke was not found. | | [api\_key\_required](#api-key-required) | 400 | An upload without a key asked for an option that needs one. | | [authorization\_pending](#authorization-pending) | 428 | The device has not been approved. | | [batch\_create\_failed](#batch-create-failed) | 500 | The batch record could not be created. | | [batch\_item\_create\_failed](#batch-item-create-failed) | 500 | A batch item record could not be created. | | [batch\_not\_found](#batch-not-found) | 404 | The batch does not exist or has expired. | | [batch\_too\_large](#batch-too-large) | 413 | The batch body or file bytes exceed the limit. | | [batch\_update\_failed](#batch-update-failed) | 500 | The batch result could not be saved. | | [billing\_customer\_failed](#billing-customer-failed) | 500 | The billing customer could not be created. | | [billing\_customer\_not\_found](#billing-customer-not-found) | 404 | The account has no billing customer. | | [billing\_unavailable](#billing-unavailable) | 503 | The account storage limit cannot be determined. | | [checkout\_unavailable](#checkout-unavailable) | 502 | A checkout URL could not be created. | | [checksum\_mismatch](#checksum-mismatch) | 422 | The completed object hash does not match the declared hash. | | [csrf\_rejected](#csrf-rejected) | 403 | A dashboard mutation came from the wrong web origin. | | [device\_authorization\_failed](#device-authorization-failed) | 500 | The device request could not be created. | | [expired\_device\_code](#expired-device-code) | 400 | The device code has expired. | | [file\_blocked](#file-blocked) | 451 | The file was blocked for abuse. | | [file\_not\_found](#file-not-found) | 404 | The file is missing, expired, or inaccessible. | | [file\_too\_large](#file-too-large) | 413 | A one-shot upload exceeds its 100 MiB limit. | | [folder\_create\_failed](#folder-create-failed) | 500 | A folder for the path could not be created. | | [forbidden](#forbidden) | 403 | The key or session lacks permission. | | [idempotency\_conflict](#idempotency-conflict) | 409 | A reused idempotency key has different content. | | [internal\_error](#internal-error) | 500 | The request could not be completed. | | [invalid\_agent\_label](#invalid-agent-label) | 400 | An agent, run, or session label is invalid. | | [invalid\_api\_key](#invalid-api-key) | 400 | The API-key request body is invalid. | | [invalid\_billing\_request](#invalid-billing-request) | 400 | The billing request is not valid JSON or has invalid fields. | | [invalid\_device\_code](#invalid-device-code) | 400 or 404 | The device code or user code is invalid. | | [invalid\_expiration](#invalid-expiration) | 400 | An access duration is invalid. | | [invalid\_manifest](#invalid-manifest) | 400 | The batch manifest is invalid. | | [invalid\_metadata](#invalid-metadata) | 400 | An upload metadata value is invalid. | | [invalid\_part](#invalid-part) | 400 | A multipart part number or size is invalid. | | [invalid\_path](#invalid-path) | 400 | A path or file name is invalid. | | [invalid\_project](#invalid-project) | 400 | A project name or project request is invalid. | | [invalid\_project\_settings](#invalid-project-settings) | 400 | Project settings are invalid. | | [invalid\_project\_transfer](#invalid-project-transfer) | 400 | The project transfer request is invalid. | | [invalid\_query](#invalid-query) | 400 | A query parameter is invalid. | | [invalid\_signature](#invalid-signature) | 400 | A billing webhook signature is invalid. | | [invalid\_token](#invalid-token) | 400 or 401 | An upload service token is missing or invalid. | | [invalid\_upload](#invalid-upload) | 400 | The upload-session request is invalid. | | [jurisdiction\_immutable](#jurisdiction-immutable) | 409 | A project's storage region cannot be changed. | | [length\_required](#length-required) | 411 | A raw part is missing `Content-Length`. | | [method\_not\_allowed](#method-not-allowed) | 405 | The hosted-file URL only allows `GET` and `HEAD`. | | [missing\_parts](#missing-parts) | 409 | Not every multipart part is present. | | [missing\_signature](#missing-signature) | 400 | A billing webhook has no signature. | | [no\_active\_organization](#no-active-organization) | 403 | The dashboard session has no active organization. | | [object\_not\_ready](#object-not-ready) | 409 | A direct multipart object is not ready to finalize. | | [part\_upload\_failed](#part-upload-failed) | 502 | A multipart part could not be stored. | | [path\_exists](#path-exists) | 409 | The requested path is already used. | | [plan\_unavailable](#plan-unavailable) | 400 | The plan cannot be bought right now. | | [precondition\_failed](#precondition-failed) | 409 or 412 | A request's `If-Match` or `if_updated_at` did not match. | | [project\_create\_failed](#project-create-failed) | 500 | The project could not be created. | | [project\_exists](#project-exists) | 409 | A project with that name already exists. | | [project\_not\_empty](#project-not-empty) | 409 | The project still contains files. | | [project\_not\_found](#project-not-found) | 404 | The project does not exist or is not visible to the key. | | [project\_update\_failed](#project-update-failed) | 500 | The project could not be updated. | | [range\_not\_satisfiable](#range-not-satisfiable) | 416 | A hosted-file byte range is outside the file. | | [rate\_limited](#rate-limited) | 429 | The caller sent too many requests. | | [region\_unavailable](#region-unavailable) | 422 or 503 | `422` for account/API requests; `503` for hosted-file URLs. | | [size\_mismatch](#size-mismatch) | 409 or 422 | `409` for recorded part sizes; `422` for a completed object size mismatch. | | [slow\_down](#slow-down) | 429 | Poll the device endpoint less often. | | [storage\_delete\_failed](#storage-delete-failed) | 502 | The stored object could not be deleted. | | [storage\_limit\_exceeded](#storage-limit-exceeded) | 413 | The upload would exceed account storage. | | [storage\_unavailable](#storage-unavailable) | 502 or 503 | `503` from `/v1`; `502` from hosted-file URLs. | | [subscription\_exists](#subscription-exists) | 409 | The account already has a subscription. | | [unauthorized](#unauthorized) | 401 | Authentication is missing or invalid. | | [unknown\_bucket](#unknown-bucket) | 400 | The selected storage target is unknown. | | [upload\_abort\_failed](#upload-abort-failed) | 502 | The multipart upload could not be aborted. | | [upload\_complete\_failed](#upload-complete-failed) | 502 | The upload could not be completed. Safe to retry. | | [upload\_completed](#upload-completed) | 409 | The multipart session is already complete. | | [upload\_create\_failed](#upload-create-failed) | 500 or 502 | The multipart session could not be started. | | [upload\_expired](#upload-expired) | 410 | The multipart session has expired. | | [upload\_failed](#upload-failed) | 500 or 502 | The file upload failed. | | [upload\_not\_active](#upload-not-active) | 409 | The session is not ready for this operation. | | [upload\_not\_found](#upload-not-found) | 404 | The upload session does not exist. | | [upload\_not\_pending](#upload-not-pending) | 409 | The upload session is no longer waiting for completion. | | [upload\_not\_started](#upload-not-started) | 409 | The direct multipart upload has not started. | | [upload\_rate\_limited](#upload-rate-limited) | 429 | The account has too many active uploads. | | [upload\_unavailable](#upload-unavailable) | 503 | Upload storage is unavailable right now. Retry shortly. | ## access\_denied [#access-denied] **403** — The device owner denied the request, or the approved device cannot be claimed. ## api\_key\_create\_failed [#api-key-create-failed] **500** — The dashboard could not save a new API key. ## api\_key\_not\_found [#api-key-not-found] **404** — The API key ID to revoke does not exist in the active account. ## api\_key\_required [#api-key-required] **400** — An upload without a key sent an option that needs one, such as `path`, `expires_in`, `visibility=private`, or an `Idempotency-Key`. Send the request with an API key, or drop the option. See [Upload without a key](./uploads.mdx#upload-without-a-key). ## authorization\_pending [#authorization-pending] **428** — The device code is valid but the account owner has not approved it. Wait for `Retry-After` and poll again. ## batch\_create\_failed [#batch-create-failed] **500** — The batch record could not be created. ## batch\_item\_create\_failed [#batch-item-create-failed] **500** — An item record could not be created while the batch was being processed. ## batch\_not\_found [#batch-not-found] **404** — The batch ID is missing or its 24-hour retention period has ended. ## batch\_too\_large [#batch-too-large] **413** — The batch request body or summed file bytes exceed 104,857,600 bytes. ## batch\_update\_failed [#batch-update-failed] **500** — AgentFS could not save the final batch counts. ## billing\_customer\_failed [#billing-customer-failed] **500** — AgentFS could not create the account's billing customer. ## billing\_customer\_not\_found [#billing-customer-not-found] **404** — The account has no billing customer for the requested billing portal action. ## billing\_unavailable [#billing-unavailable] **503** — AgentFS cannot determine the account's plan storage limit. ## checkout\_unavailable [#checkout-unavailable] **502** — AgentFS did not receive a checkout URL. ## checksum\_mismatch [#checksum-mismatch] **422** — The completed object hash does not match the SHA-256 value declared when the session was created. ## cross\_region\_copy\_unsupported [#cross-region-copy-unsupported] **409** — Source and destination are in different storage regions. Bytes are not copied across regions. ## csrf\_rejected [#csrf-rejected] **403** — A dashboard cookie mutation did not come from the AgentFS web origin. ## device\_authorization\_failed [#device-authorization-failed] **500** — AgentFS could not create a device authorization request. ## expired\_device\_code [#expired-device-code] **400** — The device code or user code is past its expiry. Start the flow again. ## file\_create\_failed [#file-create-failed] **500** — The file record could not be created. Nothing was stored. Retry the request. ## file\_blocked [#file-blocked] **451** — The file was blocked for abuse, so its hosted URL no longer serves the bytes. The identifier is in `code`; `title` is `File blocked`. Do not retry. To report a file, use the Report link on its page or write to [abuse@agentfs.cloud](mailto:abuse@agentfs.cloud). ## file\_not\_found [#file-not-found] **404** — The file is missing, not ready, expired, or outside the key's read scope. Public file URLs use the same code when no file is available, and for a private link that was [revoked](/api/files#revoke-private-links). Restoring a file that was purged or permanently deleted also returns this code. ## file\_too\_large [#file-too-large] **413** — A one-shot upload over 104,857,600 bytes (100 MiB) is too large. An upload session declaring more than 5,000,000,000,000 bytes (5 TB) returns `400 invalid_upload` instead. ## folder\_create\_failed [#folder-create-failed] **500** — AgentFS could not create a folder required by the file path. ## forbidden [#forbidden] **403** — The API key or dashboard session does not grant the requested read, write, delete, or share permission, or the action needs a team owner or admin (deleting or transferring a project, billing, revoking someone else's key). ## idempotency\_conflict [#idempotency-conflict] **409** — The idempotency key already belongs to a different request. Use a new key or send the original request unchanged. ## internal\_error [#internal-error] **500** — The request hit an unexpected server error. The response is retryable. ## invalid\_agent\_label [#invalid-agent-label] **400** — `X-Agent-ID`, `X-Run-ID`, or `X-Session-ID` is empty, too long, or contains a control character. ## invalid\_api\_key [#invalid-api-key] **400** — The dashboard API-key request body is invalid JSON or has invalid fields. ## invalid\_billing\_request [#invalid-billing-request] **400** — The billing request is not valid JSON or does not match the accepted schema. ## invalid\_device\_code [#invalid-device-code] **400 or 404** — The device code is unknown, already claimed, or the approval user code is not valid. ## invalid\_expiration [#invalid-expiration] **400** — The signed-link duration is malformed or longer than 7 days. ## invalid\_manifest [#invalid-manifest] **400** — The manifest is not a JSON array, has invalid entries, repeats a part name, omits a file part, or has more than 25 entries. ## invalid\_metadata [#invalid-metadata] **400** — A multipart upload field, label, visibility, disposition, or expiration value is invalid. ## invalid\_part [#invalid-part] **400** — The part number is not a positive integer, is outside the session, or has an invalid size. ## invalid\_path [#invalid-path] **400** — A path has an invalid segment, missing filename, bad encoding, or a filename outside the selected project. ## invalid\_project [#invalid-project] **400** — The project name contains unsupported characters or the project parameter is malformed. ## invalid\_project\_settings [#invalid-project-settings] **400** — Project settings are empty or contain an invalid name, visibility, or expiry. ## invalid\_project\_transfer [#invalid-project-transfer] **400** — A project transfer targets the same organization. ## invalid\_query [#invalid-query] **400** — A list limit or cursor is malformed or outside its accepted range. ## invalid\_signature [#invalid-signature] **400** — A billing webhook signature does not verify. ## invalid\_token [#invalid-token] **400 or 401** — An upload service token is missing, malformed, expired, or does not match the requested operation. ## invalid\_upload [#invalid-upload] **400** — The upload-session body is not valid JSON or does not match its schema. ## jurisdiction\_immutable [#jurisdiction-immutable] **409** — A project's storage region is fixed after creation. ## length\_required [#length-required] **411** — A raw multipart part must include `Content-Length`. ## misconfigured [#misconfigured] **503** — A delivery host is missing required configuration, such as its signing secret. This is on our side. It is `retryable: true`; nothing about your request needs to change. ## method\_not\_allowed [#method-not-allowed] **405** — A hosted-file URL accepts only `GET` and `HEAD` requests. ## missing\_parts [#missing-parts] **409** — Completion requires every part from `1` through `total_parts`. ## missing\_signature [#missing-signature] **400** — A billing webhook request has no signature header. ## no\_active\_organization [#no-active-organization] **403** — Sign in with a session that has an active organization. ## object\_not\_ready [#object-not-ready] **409** — A direct multipart upload has not finished writing its object. Wait for the transfer to finish, then finalize it. ## part\_upload\_failed [#part-upload-failed] **502** — The upload service could not store a multipart part. Retry the part with the same part number. ## path\_exists [#path-exists] **409** — A file or folder already uses the requested path. A [restore](/api/files#restore-a-file) returns it when another live file now holds the deleted file's path. ## plan\_unavailable [#plan-unavailable] **400** — Checkout for this plan is not open yet. Choose another plan. ## precondition\_failed [#precondition-failed] **409 or 412** — A conditional request did not match the stored file. `412` means the precondition you sent was already wrong: `If-Match` did not equal the file's current `etag` on `POST /v1/files` with `if_exists=replace`, or `If-Match` was missing or stale on `POST /v1/files/:id/replace`. A hosted-file URL returns `412` for a failed `If-Match` or `If-Unmodified-Since` too. `409` means the file changed between the read and the write: a replace move whose `If-Match` or `if_updated_at` no longer holds, or a replacement that lost a race with a concurrent one. Read the file again and retry against the new `etag`. ## project\_create\_failed [#project-create-failed] **500** — AgentFS could not save the project. ## project\_exists [#project-exists] **409** — The organization already has a project with this name. ## project\_not\_empty [#project-not-empty] **409** — Delete the project's files first, or pass `recursive=true`. ## project\_not\_found [#project-not-found] **404** — The project name or ID does not exist, or the key cannot read it. ## project\_update\_failed [#project-update-failed] **500** — AgentFS could not save the project update. ## range\_not\_satisfiable [#range-not-satisfiable] **416** — A hosted-file request asked for bytes outside the file. The response includes the available size in `Content-Range`. ## rate\_limited [#rate-limited] **429** — The caller went over a request rate limit: `/v1` calls per API key, uploads without a key per IP address, or device codes per IP address. The response is `retryable: true` and carries a `Retry-After` header in seconds. Wait for it, then send the same request again. See [Limits](./limits.mdx#request-rate) for the numbers. ## region\_unavailable [#region-unavailable] **422 or 503** — An account or `/v1` API request returns `422` when the requested storage region is unavailable. A hosted-file URL returns `503` when the file's region is not bound. ## size\_mismatch [#size-mismatch] **409 or 422** — `409` means the recorded part sizes do not add up to `expected_size_bytes`; `422` means the completed object size does not match it. ## slow\_down [#slow-down] **429** — The device token was polled before its interval elapsed. Wait for `Retry-After` seconds. ## source\_unavailable [#source-unavailable] The `url` given to `POST /v1/files` could not be fetched: it redirected, answered with an error, or did not respond within 60 seconds. Fix the source and send the request again; it is not retried automatically. ## storage\_copy\_failed [#storage-copy-failed] **502** — Storage could not copy the bytes. Nothing was kept. It is `retryable: true`. ## storage\_delete\_failed [#storage-delete-failed] **502** — AgentFS could not remove the stored bytes while deleting a file or project. ## storage\_limit\_exceeded [#storage-limit-exceeded] **413** — The upload would exceed the account's hard storage cap. The detail includes the cap in bytes. For an upload without a key, the IP address already holds 500 MB of unexpired files; wait for some to expire or use a key. ## storage\_unavailable [#storage-unavailable] **502 or 503** — `/v1` returns `503` when AgentFS cannot reach the stored object; a hosted-file URL returns `502` for the same code. ## subscription\_exists [#subscription-exists] **409** — The account already has a subscription. Manage that subscription instead of starting another checkout. ## unauthorized [#unauthorized] **401** — Send a valid bearer key, or sign in for a dashboard request. Revoked and expired keys also use this code. ## unknown\_bucket [#unknown-bucket] **400** — The upload service does not recognize the selected storage target. Check the project's storage region. ## upload\_abort\_failed [#upload-abort-failed] **502** — AgentFS could not abort the multipart upload. ## upload\_complete\_failed [#upload-complete-failed] **502** — The upload could not be completed. `retryable` is `true`: call `POST /v1/uploads/:id/complete` again. If it already finished, the retry succeeds. ## upload\_completed [#upload-completed] **409** — The upload session is already complete and cannot be aborted. ## upload\_create\_failed [#upload-create-failed] **500 or 502** — AgentFS could not create the multipart session or its underlying multipart upload. ## upload\_expired [#upload-expired] **410** — The session expiry passed. Create a new session. ## upload\_failed [#upload-failed] **500 or 502** — A file upload or multipart completion failed. Check `detail` before retrying. ## upload\_not\_active [#upload-not-active] **409** — The session is not in a state that accepts the requested operation. ## upload\_not\_found [#upload-not-found] **404** — The upload session ID does not exist, belongs to another account, or expired before completion. ## upload\_not\_pending [#upload-not-pending] **409** — The file is no longer waiting for the multipart completion operation. ## upload\_not\_started [#upload-not-started] **409** — The direct session has no multipart upload ID yet. Start it first. ## upload\_rate\_limited [#upload-rate-limited] **429** — The account already has 64 active upload sessions. Finish or abort one before creating another. The response carries `Retry-After`. ## upload\_unavailable [#upload-unavailable] **503** — Uploads are temporarily unavailable. `retryable` is `true`: wait for `Retry-After` and retry. # Files (/files) Use a bearer API key for the file API. A path starts with a project name and ends with a filename, for example `research/notes/summary.md`. Paths in JSON responses do not start with `/`. ## Paths and projects [#paths-and-projects] Use letters, numbers, `.`, `_`, and `-` in each path segment. A path can contain at most 16 segments and 2,048 encoded characters. The first segment addresses the project; the remaining segments address folders and the file. A project name starts with a letter or number and is at most 100 characters. Project names are case-insensitive. A file ID starts with `f_` and is stable after a rename, so use the ID for API operations and the returned URL for downloads. ## List and find files [#list-and-find-files] Call `GET /v1/files`. Pass a project name or project ID in `project`, and an optional case-insensitive search in `q`: ```bash curl -sS "https://agentfs.cloud/v1/files?project=research&q=summary&limit=50" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` `limit` defaults to 50 and accepts 1–200. Use the opaque `next_cursor` value as the `cursor` query parameter for the next page. These filters combine, and all of them narrow within what the key is allowed to read: | Filter | Matches | | --------------------- | -------------------------------------------------------------------------------- | | `project` | One project, by name or ID. | | `path` | One exact path, for an agent that remembers where it wrote. | | `prefix` | A path prefix such as `research/2026`, and everything below it. | | `q` | Text in the file name or path. `search` is the same filter under its older name. | | `run_id` / `agent_id` | Files written by requests that sent that `X-Run-ID` or `X-Agent-ID`. | | `status` | `ready` (the default), `uploading`, `failed`, or `trashed`, as a comma list. | ```bash title="everything one run wrote" curl -sS -G https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ --data-urlencode "run_id=run-2026-04-01" ``` The response includes `project` and `project_id` on every item: ```json { "items": [ { "type": "file", "id": "f_8f2c", "name": "summary.md", "path": "research/notes/summary.md", "label": null, "content_type": "text/markdown", "content_disposition": "inline", "size_bytes": 8192, "sha256": "0000000000000000000000000000000000000000000000000000000000000000", "etag": "\"demo-etag\"", "revision": 1, "visibility": "unlisted", "expires_at": null, "agent_id": "research-agent", "run_id": "run-2026-04-01", "url": "https://agentfsusercontent.com/f/f_8f2c", "markdown": "[summary.md](https://agentfsusercontent.com/f/f_8f2c)", "created_at": "2026-04-01T12:00:00.000Z", "updated_at": "2026-04-01T12:00:00.000Z", "project": "research", "project_id": "p_a13b", "status": "ready" } ], "next_cursor": null } ``` ## Get a file [#get-a-file] Use the file ID to get one file object: ```bash curl -sS https://agentfs.cloud/v1/files/f_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` A private file has `url: null` and `markdown: null`. Create a signed URL as described in [Sharing](./sharing.mdx). ## Update a file you wrote before [#update-a-file-you-wrote-before] Send the same `path` again with `if_exists=replace`. One call, no lookup: ```bash curl -sS -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./summary.md" \ -F "path=research/notes/summary.md" \ -F "if_exists=replace" ``` The file keeps its ID, path, and hosted URL, so anything you already shared keeps working. `revision` goes up by one and the response is `200` instead of `201`. Only the bytes and content type change; visibility, label, and expiry stay as they were. Writing the same bytes again is a no-op that returns the current file, so a retry is safe. To make the write conditional on the version you last read, add `-H "If-Match: "`; a file that changed first returns `412 precondition_failed`. Without `if_exists`, a path already in use returns `409 path_exists`. ## Rename a file [#rename-a-file] The file rename endpoint uses the signed-in dashboard session, not a bearer API key: ```http PATCH /v1/dashboard/files/f_8f2c Content-Type: application/json {"name":"final-summary.md"} ``` The name must be 1–255 characters, start with a letter or number, and then contain only letters, numbers, `.`, `_`, and `-`. This changes only the final path segment in the same folder. It does not move the file or replace its bytes. The file ID and hosted URL stay the same. A name already used in that folder returns `409 path_exists`. With a bearer key, use `PATCH /v1/files/:id` with a full `path` to rename or move a file inside its project. Send `If-Match: ` to make the move conditional; a file that changed first returns `409 precondition_failed`. ## Delete a file [#delete-a-file] Delete by ID with a bearer key: ```bash curl -sS -X DELETE https://agentfs.cloud/v1/files/f_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` A successful delete returns `204` with no body. A later get or download returns not found. The file sits in the trash for 30 days before it is purged. It still counts toward your storage while it is there. Add `?permanent=true` to remove it for good and free the space now. ## Restore a deleted file [#restore-a-deleted-file] While a file is in the trash, you can bring it back. List the trash with `status=trashed`, then restore by ID: ```bash curl -sS "https://agentfs.cloud/v1/files?status=trashed" \ -H "Authorization: Bearer $AGENTFS_KEY" curl -sS -X POST https://agentfs.cloud/v1/files/f_8f2c/restore \ -H "Authorization: Bearer $AGENTFS_KEY" ``` The file comes back at the same path, with the same ID and hosted URL, and the response is the file object. Restoring a file that is already live returns it again. If another file now holds the path, the response is `409 path_exists`. A purged or permanently deleted file returns `404 file_not_found`. # AgentFS docs (/) AgentFS gives agents a durable place to upload files, organize them into projects, and return hosted URLs that people or other agents can use. For AI agents: the docs index is at [`/llms.txt`](https://docs.agentfs.cloud/llms.txt) and the full text of every page is at [`/llms-full.txt`](https://docs.agentfs.cloud/llms-full.txt). Any page is also available as Markdown: add `.md` to its URL or send `Accept: text/markdown`. ## The first useful thing [#the-first-useful-thing] Start with the [Quickstart](./quickstart.mdx) to upload a file in one request. It covers guest uploads, API keys, and project paths. # Limits (/limits) Storage belongs to the account and is shared by its projects. Plan sizes use 1 GB = 1,073,741,824 bytes. ## Plans [#plans] | Plan | Price | Included storage | Storage cap | Overage | | --------- | --------: | -----------------------------: | ----------: | ------------------------------: | | Free | $0/month | 2 GB (2,147,483,648 bytes) | 2 GB | None | | Starter | $5/month | 50 GB (53,687,091,200 bytes) | 50 GB | None | | Developer | $11/month | 200 GB (214,748,364,800 bytes) | 200 GB | None | | Pro | $28/month | 500 GB (536,870,912,000 bytes) | None | $0.05 per GB-month above 500 GB | Free, Starter, and Developer uploads stop at their included storage amount. Pro has no storage cap; usage above 500 GB is billed as overage. On Pro, a team admin can set a spend cap on the billing page: the most extra storage, in GB, the team pays for. Uploads that would go past it get `413 storage_limit_exceeded`, the same as on a capped plan. Team owners get an email when storage reaches 70% and again at 90% of the plan's limit. On Pro, that's 70% and 90% of the 500 GB included. `GET /v1/me` returns `usage.remaining_bytes`, and the MCP `get_account` tool returns the same, so an agent can check before a large upload. Files in the trash count toward storage until they are purged after 30 days or [deleted permanently](/api/files#delete-a-file). ## Request rate [#request-rate] | Requests to | Counted per | Limit | | ------------------------------------ | ----------- | -----------------: | | `/v1` with an `Authorization` header | API key | 600 per 60 seconds | | `POST /v1/files` without a key | IP address | 20 per 60 seconds | | `POST /v1/auth/device` | IP address | 10 per 60 seconds | Over a limit, the response is `429 rate_limited` with `retryable: true` and a `Retry-After` header in seconds. Wait for it and send the same request again. Every `429` and `503` response carries `Retry-After`. ## Without a key [#without-a-key] Uploads without an API key, over HTTP or the [keyless MCP](./mcp-keyless.mdx), are counted per IP address: | Limit | Value | | --------------- | --------------------------: | | Uploads | 20 per 60 seconds | | File size | 104,857,600 bytes (100 MiB) | | Unexpired files | 524,288,000 bytes (500 MiB) | | File lifetime | 24 hours, then deleted | Over the upload rate, the response is `429 rate_limited`. Over the size or file limits, it is `413 file_too_large` or `storage_limit_exceeded`. An API key removes these limits; your plan's limits apply instead. ## Upload limits [#upload-limits] | Operation | Limit | | ----------------------------------- | --------------------------------------: | | One-shot `POST /v1/files` | 104,857,600 bytes (100 MiB) per request | | One `PUT` to `upload_url` | 5,000,000,000 bytes (5 GB) | | Upload session file size | 5,000,000,000,000 bytes (5 TB) | | Parts per file | 10,000 | | Active upload sessions | 64 per account | | Batch entries | 25 files per request | | Batch request and summed file bytes | 104,857,600 bytes (100 MiB) | Use `/v1/uploads` for a file over 100 MiB. It sends up to 5 GB in one `PUT`, and larger files in parts. Every part is the session's `part_size`, except the last, which holds the bytes that remain. A session lasts 24 hours, or less when the file's expiry comes first. ## Uploads without a key [#uploads-without-a-key] `POST /v1/files` also works with no API key and no account, for trying AgentFS or sharing a file quickly. See [Upload without a key](./uploads.mdx#upload-without-a-key). | Limit | Value | | ------------------ | ---------------------------------------: | | File size | 104,857,600 bytes (100 MB) per file | | Uploads | 20 per 60 seconds per IP address | | Files kept at once | 500 MB of unexpired files per IP address | | Link lifetime | 24 hours, then the file is deleted | | Visibility | Always unlisted | A key is needed for everything else: larger files, resumable uploads, choosing a path or project, setting visibility or expiry, and listing, renaming, sharing, or deleting files. An upload over the 500 MB allowance returns `413 storage_limit_exceeded` until older files expire. # MCP for agents (/mcp-keyless) Agents can start right away: no API key, no account. Add an API key to keep files and use every tool. ## Try keyless [#try-keyless] Keyless server URL (Streamable HTTP, no credential): `https://mcp.agentfs.cloud/v1/mcp` * Claude Code: `claude mcp add --transport http agentfs https://mcp.agentfs.cloud/v1/mcp` * Codex: `codex mcp add agentfs --url https://mcp.agentfs.cloud/v1/mcp` * Cursor or any JSON-config client: `{"mcpServers": {"agentfs": {"url": "https://mcp.agentfs.cloud/v1/mcp"}}}` * OpenCode (`opencode.json`): `{"mcp": {"agentfs": {"type": "remote", "url": "https://mcp.agentfs.cloud/v1/mcp", "enabled": true}}}` People connecting their own account should sign in instead: see [MCP](./mcp.mdx). Keyless MCP exposes one tool, `upload_file`. It takes `name` and either `text` or `content_base64`, and returns the file with its `url`. Keyless files are unlisted, up to 100 MB each, and deleted 24 hours after upload. Uploads are rate limited per IP address; see [Limits](./limits.mdx#without-a-key). ## Add an API key [#add-an-api-key] [Create an API key](./quickstart.mdx) or run `agentfs login`, then add it as a bearer token on the same URL: ```json { "mcpServers": { "agentfs": { "type": "http", "url": "https://mcp.agentfs.cloud/v1/mcp", "headers": { "Authorization": "Bearer " } } } } ``` With a key, files are kept in your projects, and every tool is available: `upload_file` with paths, projects and visibility, `list_files`, `read_file`, `get_file`, `rename_file`, `delete_file`, `restore_file`, and `create_access_url`. See [Available tools](./mcp.mdx#available-tools). ## Verify your connection [#verify-your-connection] Open your client's MCP status or tool list and check that `agentfs` is connected. A keyless connection shows only `upload_file`. A connection with a key shows every tool. ## Try [#try] ```text Write a short summary of this conversation to summary.md and upload it to AgentFS. Give me the link. ``` If no AgentFS tools appear, restart or reload the client after saving its MCP configuration. # MCP (/mcp) Connect an MCP client to: ```text https://mcp.agentfs.cloud/v1/mcp-oauth ``` No API key or headers are needed. `https://agentfs.cloud/mcp` also works the same way. That URL is all you need. The first time your client connects, it opens AgentFS in your browser. Sign in if you are not already, pick the team the connection should use, and click **Allow**. Your client gets access and keeps it; there is no API key to copy. ## Claude Code [#claude-code] ```bash claude mcp add --transport http agentfs https://mcp.agentfs.cloud/v1/mcp-oauth ``` Then run `/mcp` in Claude Code and choose **agentfs** to sign in. ## Cursor [#cursor] Add the server to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "agentfs": { "url": "https://mcp.agentfs.cloud/v1/mcp-oauth" } } } ``` Cursor asks you to log in to the server. Click the login button next to it and approve in the browser. ## Other MCP clients [#other-mcp-clients] Choose the client's HTTP or remote-server option and set the URL to `https://mcp.agentfs.cloud/v1/mcp-oauth`. Clients that support MCP sign-in open the browser for you. ## Headless and CI [#headless-and-ci] Where no browser is available, send an API key instead. Create one in the dashboard under **API keys**, or run `agentfs login`, and pass it as a header: ```bash claude mcp add --transport http agentfs https://agentfs.cloud/mcp \ --header "Authorization: Bearer $AGENTFS_KEY" ``` Keep `AGENTFS_KEY` in your client's environment or secret store; do not paste it into a committed configuration file. ## Available tools [#available-tools] | Tool | What it does | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `upload_file` | Write a file from `text`, `content_base64`, or a source `url`, and return its hosted URL. Takes `path` or `project` + `prefix`, `if_exists`, `visibility`, `expires_in`, `label`, and `idempotency_key` (send the same value when retrying a call, and the retry returns the first file instead of creating another). | | `list_files` | List files, newest first. Filters: `project`, `prefix`, `q`, `run_id`, and `status` (`trashed` lists deleted files). Pages with `limit` and `cursor`. | | `read_file` | Read a text file's content by ID or path. | | `get_file` | Get a file object and its hosted URL by ID. | | `rename_file` | Rename a file, keeping its ID and hosted URL. | | `delete_file` | Delete a file by ID. It stays in the trash for 30 days. | | `restore_file` | Bring a deleted file back from the trash, with the same path, ID, and hosted URL. | | `create_access_url` | Create a time-limited URL for a private file. | Without a connection, only `upload_file` is available; see [MCP for agents](./mcp-keyless.mdx). A browser connection can use all files in the team you picked. An API key keeps its own permissions. ### Updating a file [#updating-a-file] To change a file it wrote earlier, an agent sends the same `path` to `upload_file` with `if_exists=replace`. The ID and URL stay the same. Without it, a path already in use is an error. When the agent only knows the path, `read_file` accepts a `path`, and `list_files` with a `path` filter returns the file object with its ID and ETag. ### Reading content [#reading-content] `read_file` returns text inline, up to 64 KiB per call. Pass `offset` to continue from where the last call stopped; the result carries `offset`, `length`, `size_bytes`, and `eof`. A binary file is not returned inline — use `create_access_url` for a link to it instead. See [Uploads](./uploads.mdx), [Files](./files.mdx), and [Sharing](./sharing.mdx) for the same operations over HTTP. # Projects (/projects) A project is the first segment of every file path, such as `research/report.pdf`. Use a bearer API key for these endpoints. ## Create a project [#create-a-project] Send JSON with a project name: ```bash title="create project" curl -sS -X POST https://agentfs.cloud/v1/projects \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "content-type: application/json" \ -d '{ "name": "research", "default_visibility": "private", "default_expires_in": 604800 }' ``` `name` is required, starts with a letter or number, and is at most 100 characters. It may contain letters, numbers, `.`, `_`, and `-`. Project names are case-insensitive. The other accepted fields are: | Field | Value | | -------------------- | -------------------------------------------------------------------------------------------------------- | | `default_visibility` | `public`, `unlisted`, or `private`; defaults to `unlisted`. | | `default_expires_in` | Positive seconds, `null` to disable the default, or omitted. The maximum is 31,536,000 seconds (1 year). | The response has status `201`: ```json { "type": "project", "id": "p_a13b", "name": "research", "site_url": null, "storage_region": "us-east", "default_visibility": "private", "default_expires_in": 604800, "file_count": 0, "size_bytes": 0, "created_at": "2026-04-01T12:00:00.000Z", "updated_at": "2026-04-01T12:00:00.000Z" } ``` ## List projects [#list-projects] Call `GET /v1/projects`: ```bash curl -sS "https://agentfs.cloud/v1/projects?limit=50" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` The response is `{ "items": [project objects], "next_cursor": "..." }`. The default `limit` is 50 and the maximum is 200. Pass `next_cursor` back as `cursor` to get the next page. A key scoped to selected projects sees only those projects. ## Get a project [#get-a-project] Address a project by name: ```bash curl -sS https://agentfs.cloud/v1/projects/research \ -H "Authorization: Bearer $AGENTFS_KEY" ``` `GET /v1/projects/:project` returns one project object. URL-encode the name when needed. ## Update project defaults [#update-project-defaults] Use `PATCH /v1/projects/:project` with one or more settings: ```bash curl -sS -X PATCH https://agentfs.cloud/v1/projects/research \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "content-type: application/json" \ -d '{"default_visibility":"unlisted","default_expires_in":null}' ``` The API update body accepts `default_visibility` and `default_expires_in`. It must contain at least one setting. This API does not rename a project. A file without its own visibility uses the project's current `default_visibility`, so a change applies to existing files at once. `default_expires_in` sets the expiry of files uploaded after the change; files already stored keep the expiry they were given. ## Delete a project [#delete-a-project] Delete an empty project with: ```bash curl -sS -X DELETE https://agentfs.cloud/v1/projects/research \ -H "Authorization: Bearer $AGENTFS_KEY" ``` Only team owners and admins can delete a project; for an API key, that is the role of the key's owner. If the project contains files, the request returns `409 project_not_empty`. Add `recursive=true` to delete the files and the project: ```bash curl -sS -X DELETE "https://agentfs.cloud/v1/projects/research?recursive=true" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` A successful delete returns `204` with no body. The project and its files disappear at once, whatever the project's size; the stored bytes are removed within the hour. # Quickstart (/quickstart) ## Connect an account [#connect-an-account] 1. Open [agentfs.cloud](https://agentfs.cloud), sign in, open a project, and open **API Keys**. 2. Select **Create key**. 3. Store the secret when it appears. AgentFS shows it only once. Set the key in your shell, then upload. The file goes into the key's project: ```bash title="authenticated upload" export AGENTFS_KEY="afs_..." curl -sS -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./report.pdf" \ -F "prefix=research" ``` The authenticated response uses the same file object shape. The file lands in `research/` inside the key's project. Use [Authentication](./authentication.mdx), [Uploads](./uploads.mdx), and [Files](./files.mdx) for the rest of the API. # Retries and recovery (/retries) AgentFS returns errors as `application/problem+json`. Read `status`, `code`, `detail`, and `retryable` before choosing the next action. Do not retry an operation merely because it failed. ## Decision table [#decision-table] | Response | Meaning | Agent action | | ------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `400`, `401`, `403`, `404`, `409`, `412`, `413`, `422`, `451` | The request, credentials, permissions, or current state need attention. | Stop retrying. Read `code` and `detail`, then correct the input or ask for access. | | `408` | The request took too long to arrive. | Retry with backoff. | | `429` | The caller has reached a rate or quota limit. | Wait for `Retry-After` seconds, then retry. Back off further if it happens again. | | `500`–`599` | AgentFS or its storage dependency could not complete the request. | Retry with backoff. A `503` carries `Retry-After`; wait at least that long. | | Network error before a response | The server may have received the request. | Retry only a write that is safe to repeat (see below). | Retry only these: `408`, `429`, `5xx`, and dropped connections. Every other status fails the same way twice. ## Safe writes [#safe-writes] For `POST /v1/files`, set `Idempotency-Key` to a stable value for one logical upload. Reusing the key with identical bytes returns the first file rather than creating another one. Do not reuse it for different content. For a path an agent owns, upload with `if_exists=replace`. Repeating the same byte upload to that path is safe and keeps its file ID and hosted URL. `POST /v1/batch` requires `Idempotency-Key`; preserve it until the batch reaches a terminal state. Reads (`GET`) are always safe to retry. So are `POST /v1/uploads` with the same `path` and `size_bytes` (it returns the live session), part uploads, `POST /v1/uploads/:id/complete`, deletes, and restores. A plain `POST /v1/files` without `Idempotency-Key` or `if_exists=replace` is not: a retry after a lost response can create a second file. ## Backoff [#backoff] Every `429` and `503` carries a `Retry-After` header in seconds. Wait at least that long before the next attempt. Otherwise use exponential backoff: wait about 1 second, then double the wait after each failure, and cap it between 30 and 60 seconds. Add random jitter, for example by waiting a random time between zero and the current delay, so agents that failed together do not retry together. Stop after a bounded number of attempts, such as 8. Never blindly retry a `409`: it may mean a path already exists, an ETag precondition failed, a multipart upload has already completed, or an idempotency key was reused for different bytes. Resolve the state first. ## Request identity [#request-identity] Pass a unique `X-Agent-ID` and `X-Run-ID` on authenticated requests. They make it possible to list the files one run created and to distinguish a retried operation from work started by another agent. See [Errors](./errors.mdx) for every documented problem code and [the generated endpoint reference](/api-reference) for endpoint-specific request details. # Sharing (/sharing) Set `visibility` when you upload a file, or set `default_visibility` on a project. Choose `public`, `unlisted`, or `private`. ## Visibility [#visibility] * **Public** and **unlisted** files work at their hosted URL without a key or signature. * **Private** files return `url: null` from the file API until you create a signed access URL. ## Hosted URLs [#hosted-urls] Files are served from `https://agentfsusercontent.com/f/`. HTML, SVG, and XML files are served from a sandboxed origin, `https://s.agentfsusercontent.com/f/`, so a page someone uploads cannot act as agentfs.cloud. Links on the old hosts, `f.agentfsusercontent.com`, `f.agentfs.cloud` and `s.agentfs.cloud`, keep working. They redirect permanently to the same path and query on the new host. Rendered Markdown pages show Raw, Download, and Report links. Report opens an email to [abuse@agentfs.cloud](mailto:abuse@agentfs.cloud). A file blocked for abuse returns `451` [`file_blocked`](/errors#file-blocked) instead of its bytes. An unlisted file is not an access control boundary; use `private` when a file must not be readable without a signed link. Set visibility on a one-shot upload: ```bash curl -sS -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./report.pdf" \ -F "path=research/report.pdf" \ -F "visibility=private" ``` Uploads without a key cannot be public or private. They are always unlisted. ## Project defaults [#project-defaults] A file with no `visibility` value uses its project's `default_visibility`, which defaults to `unlisted`. Changing the project default applies right away to every file that has no visibility of its own, including files uploaded earlier. A file's own visibility always wins over the project default. Folders only organize paths. They have no visibility or expiry of their own. ## Create a signed link [#create-a-signed-link] Call `POST /v1/files/:id/access` with a key that has `share` permission: ```bash curl -sS -X POST https://agentfs.cloud/v1/files/f_8f2c/access \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "content-type: application/json" \ -d '{"expires_in":"24h"}' ``` For a private file, the response contains the link and its expiry: ```json { "file_id": "f_8f2c", "url": "https://agentfsusercontent.com/f/f_8f2c?expires=1775048400&v=1&signature=example", "markdown": "[report.pdf](https://agentfsusercontent.com/f/f_8f2c?expires=1775048400&v=1&signature=example)", "expires_at": "2026-04-01T13:00:00.000Z" } ``` If the file is public or unlisted, the endpoint returns its existing URL and `expires_at: null`; those URLs do not become temporary. For a private file, omit `expires_in` for a one-hour link. Accepted durations use `s`, `m`, `h`, `d`, or `w`, and a signed link can last at most 7 days. The file's own expiry still applies. A signed link cannot make an expired file available. A private link looks like `?expires=&v=&signature=`. Older links without `v` keep working until they expire, unless the file's links were revoked. ## Revoke private links [#revoke-private-links] If a private link leaks, revoke every private link issued for the file so far. The key needs `write` permission: ```bash curl -sS -X POST https://agentfs.cloud/v1/files/f_8f2c/access/revoke \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ```json { "id": "f_8f2c", "access_version": 2 } ``` Old links then return `404`. Links you create afterwards work as usual. Revoking does not touch public or unlisted URLs: change the file's visibility or delete it for those. # Uploads (/uploads) Files can be up to 5 TB. `POST /v1/files` sends a file in one request, which is the quickest way to upload files up to 100 MiB. For anything larger, use a [resumable upload](#resumable-upload): it handles up to 5 TB and picks up where it stopped. The CLI chooses for you: logged in, it sends files over 16 MB in parts, six at a time, retries each part on its own, and resumes an interrupted upload when you run the same command again. ## One-shot upload [#one-shot-upload] Send a `multipart/form-data` request to `https://agentfs.cloud/v1/files`: ```bash title="one-shot upload" curl -sS -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@./report.pdf" \ -F "path=research/report.pdf" \ -F "visibility=private" \ -F "label=Research report" ``` The `file` part is required. Use these other parts when you need them: | Part | Value | Notes | | --------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- | | `path` | `project/folders/name.ext` | Optional; the first segment is the project. Requires a key. | | `visibility` | `public`, `unlisted`, or `private` | Uploads without a key are always `unlisted`. | | `expires_in` | A duration such as `24h` or `7d` | Requires a key. Uploads without a key expire after 24 hours. | | `label` | Text up to 200 characters | Optional display label. Requires a key. | | `content_disposition` | `inline` or `attachment` | Defaults to `inline`. | | `if_exists` | `error` or `replace` | What to do when `path` already holds a file. Defaults to `error`. Requires `path`, so it requires a key. | AgentFS infers `content_type` from the uploaded file and its filename. HTML, SVG, and XML files are always served from the sandboxed host `s.agentfsusercontent.com`, whatever type you declared. Send an `Idempotency-Key` header with 1–200 characters if you need a retry to return the original file instead of creating another one. Reusing that key with different content returns `409 idempotency_conflict`. Send `if_exists=replace` to overwrite a file you wrote earlier. The file keeps its ID, path, and hosted URL, and the response is `200` instead of `201`. See [Files](./files.mdx#update-a-file-you-wrote-before). A one-shot request carries up to 104,857,600 bytes (100 MiB). Larger files go through a resumable upload. ## Upload without a key [#upload-without-a-key] To try AgentFS, or to share one file quickly, upload without an API key or an account: ```bash title="upload without a key" curl -F file=@report.pdf https://agentfs.cloud/v1/files ``` The response is the file object, with its `url`. These uploads work differently from uploads with a key: * Each file can be up to 100 MB (104,857,600 bytes). * The file is unlisted: anyone with the link can open it, and nobody can browse to it. * The link stops working 24 hours after the upload, and the file is deleted. * AgentFS picks the file name from the one you uploaded, plus a short random suffix. * Each IP address can send 20 uploads per minute and keep up to 500 MB of files that have not expired yet. * You can't list, rename, replace, or delete the file afterwards. Only the one-shot `POST /v1/files` works without a key, with a multipart form or a raw body. Uploading from a JSON `url` needs a key. `path`, `project`, `prefix`, `if_exists=replace`, `expires_in`, `label`, `visibility` other than `unlisted`, and the `Idempotency-Key` header need a key: sending any of them without one returns `400 api_key_required`. Resumable uploads and every other endpoint need a key too. To keep files, choose where they go, or upload anything larger than 100 MB, [create an API key](./quickstart.mdx) or run `agentfs login`. ## Resumable upload [#resumable-upload] Create a session with JSON. A key is required for agent requests: ```bash title="create an upload session" curl -sS -X POST https://agentfs.cloud/v1/uploads \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "content-type: application/json" \ -d '{ "path": "research/video.mp4", "size_bytes": 50000000, "content_type": "video/mp4" }' ``` `path` and `size_bytes` are required. You may also send `content_type`, a 64-character hexadecimal `sha256`, `visibility`, `expires_in`, `label`, `content_disposition`, and `multipart`. The declared size must be positive and no greater than 5,000,000,000,000 bytes (5 TB). `multipart` is a boolean that defaults to `false`. By default, files up to 5,000,000,000 bytes get one `upload_url` for a single `PUT`, and larger files get parts. Send `"multipart": true` to get parts at any size. Parts make an upload resumable: each part is sent and retried on its own, and a new run can skip the parts already stored. The response includes fields such as: ```json { "type": "upload", "id": "up_8f2c", "file_id": "f_1a2b", "url": "https://agentfsusercontent.com/f/f_1a2b", "status": "active", "transport": "s3", "part_size": 50000000, "total_parts": 1, "expected_size_bytes": 50000000, "uploaded_parts": [], "expires_at": "2026-04-02T12:00:00.000Z", "upload_url": "https://", "upload_url_expires_at": "2026-04-01T17:55:00.000Z" } ``` A session lasts 24 hours unless the file expiry is sooner. Follow what the session tells you: * **`upload_url` is present:** `PUT` the whole file there in one request. * **`transport: "s3"` without `upload_url`:** fetch part URLs with `GET /v1/uploads/:id/part-urls?from=1&to=100` and `PUT` each part to its URL. * **`transport: "worker"`:** `PUT` each part to the API, as shown below. If you get `503 upload_unavailable`, wait and create the session again. The `url` in this response is where the file will live. An HTML, SVG, or XML file can move to the sandboxed host when the upload completes, so share the `url` from the completed file object. If the connection drops, send the same `POST /v1/uploads` again with the same `path` and `size_bytes`. While the session is live, you get it back with `200` and its `uploaded_parts`, so you only send the parts that are missing. `GET /v1/uploads/:id` returns the same. ### Send the bytes to `upload_url` [#send-the-bytes-to-upload_url] `PUT` the whole file to `upload_url` in one request. `Content-Length` must equal `size_bytes`, because the signature covers it: ```bash title="upload the file" curl -sS -X PUT "$UPLOAD_URL" \ -H "Content-Length: 50000000" \ --data-binary @video.mp4 ``` Storage answers `200` with an `ETag`, and that is the last step: AgentFS sees the file arrive and marks it ready within a few seconds. The `url` from the first response starts working then. Call complete, shown below, only if you need the file ready before you move on, or if you declared a `sha256`. If the URL expires before you finish, fetch the session again with `GET /v1/uploads/:id` for a fresh one and send the file again. ### Upload parts [#upload-parts] Files above 5 GB, and sessions created with `"multipart": true`, go up in parts. Every part is `part_size` bytes from the session, except the last, which is whatever remains. Fetch up to 100 part URLs at a time and `PUT` each part to its URL with `Content-Length` set to the part's `size_bytes`. Parts can go up in parallel; the CLI sends six at a time. Storage records the parts; completion checks them all. A part URL that has expired answers `403`: fetch fresh URLs and send the part again. ```bash title="part URLs" curl -sS "https://agentfs.cloud/v1/uploads/up_8f2c/part-urls?from=1&to=100" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Upload parts through the API (`transport: "worker"`) [#upload-parts-through-the-api-transport-worker] Number parts from `1` through `total_parts`. Send each part as the request body and include `Content-Length`: ```bash title="upload part 1" curl -sS -X PUT https://agentfs.cloud/v1/uploads/up_8f2c/parts/1 \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Length: $PART_SIZE" \ --data-binary @part-001 ``` Every part except the last must be exactly `part_size` bytes, and the last part exactly the bytes that remain. The response is: ```json { "part_number": 1, "etag": "\"part-etag\"", "size_bytes": 16777216 } ``` The same part number may be sent again; the later upload replaces the earlier part. ### Complete or abort [#complete-or-abort] Uploads sent to `upload_url` complete on their own. Part uploads need this call once every part is in place: ```bash curl -sS -X POST https://agentfs.cloud/v1/uploads/up_8f2c/complete \ -H "Authorization: Bearer $AGENTFS_KEY" ``` A successful completion returns the file object. If storage could not assemble the parts, the response is `502 upload_complete_failed`, which is safe to retry: call complete again. A completion in progress can return `202` with the session object; call `complete` again or inspect the session with: ```bash curl -sS https://agentfs.cloud/v1/uploads/up_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` Delete an unfinished session to abort it: ```bash curl -sS -X DELETE https://agentfs.cloud/v1/uploads/up_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` The abort response is `204`. You cannot abort a completed session. # Account (/api/account) ## Get account [#get-account] `GET /v1/me` Return the account, plan limits, and usage for the API key. Any valid API key can call this endpoint. You may also omit `Authorization` to check whether the request is unauthenticated. ### Authentication [#authentication] No header is required for the unauthenticated response. When you send `Authorization`, use a valid API key. ### Response fields [#response-fields] For an authenticated key: | Field | Type | Description | | ------------------------------- | ----------------- | -------------------------------------------------------------------------------- | | `type` | string | Always `account`. | | `id` | string | Organization ID. | | `authenticated` | boolean | Always `true` in the authenticated response. | | `default_project` | string | Default project name, or `default` when no default project is set. | | `plan` | string | `free`, `starter`, `developer`, `pro`, or `unknown`. | | `limits.storage_bytes` | integer or `null` | Hard storage limit. `null` means no hard cap or an unknown plan. | | `limits.included_storage_bytes` | integer or `null` | Included storage for the plan. | | `limits.max_file_bytes` | `null` | Always `null` in this response. | | `limits.max_batch_files` | integer | Always `25`. | | `limits.max_batch_bytes` | integer | Always `104857600` (100 MiB). | | `usage` | object or `null` | Organization usage when the key has organization-level `read`; otherwise `null`. | | `usage.size_bytes` | integer | Bytes in ready and uploading files. | | `usage.file_count` | integer | Count of ready and uploading files. | | `usage.remaining_bytes` | integer or `null` | Storage left before the plan's cap. `null` when the plan has no cap. | Without `Authorization`, return only `type`, `authenticated`, and `next`. Returns `200 OK`. ### Request [#request] ```bash title="Request" curl https://agentfs.cloud/v1/me \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response] ```json title="200 OK" { "type": "account", "id": "org_1a2b", "authenticated": true, "default_project": "reports", "plan": "developer", "limits": { "storage_bytes": 107374182400, "included_storage_bytes": 107374182400, "max_file_bytes": null, "max_batch_files": 25, "max_batch_bytes": 104857600 }, "usage": { "size_bytes": 482130, "file_count": 12, "remaining_bytes": 214748182518 } } ``` An unauthenticated request returns: ```json title="200 OK — unauthenticated" { "type": "account", "authenticated": false, "next": "Call POST /v1/auth/device to connect an agent." } ``` ### Errors [#errors] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`internal_error`](/errors#internal-error) (500). # Batches (/api/batch) Use batches when you need to send several one-shot files together. A batch accepts 1–25 files and at most 104,857,600 bytes (100 MiB) in total. Each item uses the one-shot file rules, including path validation and the 100 MiB per-file limit. ### Batch object [#batch-object] | Field | Type | Description | | ---------------- | ------- | ------------------------------------------------------------------ | | `type` | string | Always `batch`. | | `id` | string | Batch ID. | | `status` | string | `completed` when every item stored, `failed` when any item failed. | | `total` | integer | Number of manifest entries. | | `completed` | integer | Number of files stored successfully. | | `failed` | integer | Number of files that failed. | | `items` | array | Result for each manifest entry. | | `items[].part` | string | Manifest part name. | | `items[].path` | string | Stored path without a leading slash. | | `items[].file` | object | [File object](./files.mdx#file-object) when the item completed. | | `items[].error` | object | Failure details with `code` and `detail` when the item failed. | | `items[].status` | string | Item status when it is still processing. | | `created_at` | string | ISO 8601 creation time. | The `file`, `error`, and `status` fields are alternatives for an item. A batch can return `200` with `status: "failed"`; inspect each item's `error`. Batch records are retained for 24 hours. ## Upload a batch [#upload-a-batch] `POST /v1/batch` Upload the manifest and all named file parts in one multipart request. Send an API key with `write` permission for every target path. A path in a project the key cannot access fails authorization before the batch is created; a new project requires organization-level `write` permission. ### Request body [#request-body] Use `multipart/form-data`. | Field | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `manifest` | string | Yes | A JSON array with 1–25 entries. Each entry is `{ "part": string, "path": string }`. `part` is 1–64 characters matching `[A-Za-z0-9_-]`; `path` is 1–2,048 characters and must include a project and filename. | | Each named part | file | Yes | Include one file field whose name exactly matches every manifest `part`. Part names must be unique. | The sum of file sizes and the request body are limited to 104,857,600 bytes. File content types are inferred from filenames; the batch endpoint does not accept per-file metadata fields. ### Idempotency header [#idempotency-header] `Idempotency-Key` is required and must contain 1–200 characters. Reusing it with the same manifest and file bytes returns the existing batch. Reusing it with a different manifest or bytes returns [`idempotency_conflict`](/errors#idempotency-conflict). If a batch was cut short before it finished, the same request completes it: files that were already stored are not stored again. Returns `200 OK`. The batch runs to completion inside the request, so the response is the final result: read `status`, `completed`, `failed`, and `items`. ### Response fields [#response-fields] Returns a [batch object](#batch-object). ### Request [#request] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/batch \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Idempotency-Key: import-2026-03" \ -F 'manifest=[{"part":"readme","path":"docs/readme.md"}]' \ -F "readme=@README.md;type=text/markdown" ``` ### Response [#response] ```json title="200 OK" { "type": "batch", "id": "b_5e21", "status": "completed", "total": 1, "completed": 1, "failed": 0, "items": [ { "part": "readme", "path": "docs/readme.md", "file": { "type": "file", "id": "f_8f2c", "name": "readme.md", "path": "docs/readme.md", "label": null, "content_type": "text/markdown", "content_disposition": "inline", "size_bytes": 48213, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-readme", "revision": 1, "visibility": "unlisted", "expires_at": null, "tags": [], "metadata": {}, "agent_id": null, "run_id": null, "url": "https://agentfsusercontent.com/f/f_8f2c", "markdown": "[readme.md](https://agentfsusercontent.com/f/f_8f2c)", "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:01.000Z" } } ], "created_at": "2026-03-02T12:00:00.000Z" } ``` ### Errors [#errors] [`batch_too_large`](/errors#batch-too-large) (413), [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`invalid_manifest`](/errors#invalid-manifest) (400), [`forbidden`](/errors#forbidden) (403), [`idempotency_conflict`](/errors#idempotency-conflict) (409), [`batch_create_failed`](/errors#batch-create-failed) (500), [`batch_item_create_failed`](/errors#batch-item-create-failed) (500), [`batch_update_failed`](/errors#batch-update-failed) (500), [`internal_error`](/errors#internal-error) (500). Individual item failures are returned inside `items[].error`; their codes can include the file upload errors documented for [`POST /v1/files`](./files.mdx#upload-a-file). ## Get a batch [#get-a-batch] `GET /v1/batch/:id` Return a batch and check each item result. The API key must have `read` permission for every path in the manifest. ### Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | Batch ID. | Expired batches are reported as not found. ### Response fields [#response-fields-1] Returns a [batch object](#batch-object). Returns `200 OK`. ### Request [#request-1] ```bash title="Request" curl https://agentfs.cloud/v1/batch/b_5e21 \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-1] ```json title="200 OK" { "type": "batch", "id": "b_5e21", "status": "completed", "total": 1, "completed": 1, "failed": 0, "items": [ { "part": "readme", "path": "docs/readme.md", "file": { "type": "file", "id": "f_8f2c", "name": "readme.md", "path": "docs/readme.md", "label": null, "content_type": "text/markdown", "content_disposition": "inline", "size_bytes": 48213, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-readme", "revision": 1, "visibility": "unlisted", "expires_at": null, "tags": [], "metadata": {}, "agent_id": null, "run_id": null, "url": "https://agentfsusercontent.com/f/f_8f2c", "markdown": "[readme.md](https://agentfsusercontent.com/f/f_8f2c)", "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:01.000Z" } } ], "created_at": "2026-03-02T12:00:00.000Z" } ``` ### Errors [#errors-1] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`batch_not_found`](/errors#batch-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`invalid_manifest`](/errors#invalid-manifest) (400), [`internal_error`](/errors#internal-error) (500). # Device authorization (/api/device-auth) Use the device flow when an agent needs a new API key: 1. The agent requests a device code. 2. The user opens the returned verification URL, signs in, and approves the code. 3. The agent polls the token endpoint until it receives the API key. The code expires after 900 seconds. Poll no more often than the returned `interval` value. ## Start device authorization [#start-device-authorization] `POST /v1/auth/device` Create a pending device authorization. This endpoint needs no authentication and has no request body. ### Response fields [#response-fields] | Field | Type | Description | | --------------------------- | ------- | ------------------------------------------------------------------ | | `device_code` | string | Secret code the agent sends to the token endpoint. | | `user_code` | string | Code the user enters or confirms. | | `verification_uri` | string | User verification URL. | | `verification_uri_complete` | string | Verification URL with the user code in its `code` query parameter. | | `expires_in` | integer | Lifetime in seconds: `900`. | | `interval` | integer | Minimum polling interval in seconds: `5`. | | `next` | string | Instructions to show the user and agent. | Returns `200 OK`. ### Request [#request] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/auth/device ``` ### Response [#response] ```json title="200 OK" { "device_code": "device-code-example", "user_code": "AB12-CD34", "verification_uri": "https://agentfs.cloud/device", "verification_uri_complete": "https://agentfs.cloud/device?code=AB12-CD34", "expires_in": 900, "interval": 5, "next": "Show the verification URL and code to the user, then poll the token endpoint every 5 seconds." } ``` ### Errors [#errors] [`rate_limited`](/errors#rate-limited) (429), [`device_authorization_failed`](/errors#device-authorization-failed) (500), [`internal_error`](/errors#internal-error) (500). ## Approve device authorization [#approve-device-authorization] `POST /v1/auth/device/approve` Approve a pending user code after the user has authenticated. This endpoint uses the browser's authenticated session, not an API key. The web client calls it after the user confirms the code. ### Authentication [#authentication] Send the session cookie from the AgentFS web origin. Mutating cookie requests must include the web origin or an accepted same-site fetch context. ### Request body [#request-body] Send JSON with `Content-Type: application/json`. | Field | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------ | | `user_code` | string | Yes | Code returned by `POST /v1/auth/device`. | | `default_project` | string | No | Existing project name to make the new key's default project. | If `default_project` is present, the project must already exist in the active organization. The endpoint also ensures the account's available regions before approval. ### Response fields [#response-fields-1] | Field | Type | Description | | -------- | ------ | ----------------------------- | | `status` | string | Always `approved` on success. | Returns `200 OK`. ### Request [#request-1] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/auth/device/approve \ -H "Content-Type: application/json" \ -H "Origin: https://agentfs.cloud" \ -H "Cookie: " \ -d '{"user_code":"AB12-CD34","default_project":"reports"}' ``` ### Response [#response-1] ```json title="200 OK" { "status": "approved" } ``` ### Errors [#errors-1] [`csrf_rejected`](/errors#csrf-rejected) (403), [`unauthorized`](/errors#unauthorized) (401), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_device_code`](/errors#invalid-device-code) (400 or 404), [`expired_device_code`](/errors#expired-device-code) (400), [`project_not_found`](/errors#project-not-found) (404), [`region_unavailable`](/errors#region-unavailable) (422), [`internal_error`](/errors#internal-error) (500). ## Exchange a device code for an API key [#exchange-a-device-code-for-an-api-key] `POST /v1/auth/device/token` Poll with the device code until the user approves it. This endpoint needs no authentication. ### Request body [#request-body-1] Send JSON with `Content-Type: application/json`. | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------- | | `device_code` | string | Yes | Secret code returned by the start endpoint. | While the request is pending, return `428` with [`authorization_pending`](/errors#authorization-pending) and `Retry-After: 5`. If you poll too soon, return `429` with [`slow_down`](/errors#slow-down) and a `Retry-After` value containing the remaining wait in seconds. ### Response fields [#response-fields-2] On approval, return: | Field | Type | Description | | ----------------- | ------ | -------------------------------------------------------------- | | `type` | string | Always `account`. | | `id` | string | Organization ID. | | `api_key` | string | New API key. Store it as `AGENTFS_KEY`; it is not shown again. | | `default_project` | string | Selected project name, or `default`. | | `next` | string | Storage instruction. | A selected `default_project` gives the key a full-permission scope for that project. Without one, give the key full organization permissions. Returns `200 OK` after approval. ### Request [#request-2] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/auth/device/token \ -H "Content-Type: application/json" \ -d '{"device_code":"device-code-example"}' ``` ### Pending response [#pending-response] ```json title="428 Authorization Pending" { "type": "https://agentfs.cloud/docs/errors#authorization-pending", "title": "authorization_pending", "status": 428, "code": "authorization_pending", "detail": "The device has not been approved yet.", "retryable": false } ``` ### Approved response [#approved-response] ```json title="200 OK" { "type": "account", "id": "org_1a2b", "api_key": "afs_example_key", "default_project": "reports", "next": "Store this value as AGENTFS_KEY. It will not be shown again." } ``` ### Errors [#errors-2] [`invalid_device_code`](/errors#invalid-device-code) (400), [`expired_device_code`](/errors#expired-device-code) (400), [`access_denied`](/errors#access-denied) (403), [`slow_down`](/errors#slow-down) (429), [`authorization_pending`](/errors#authorization-pending) (428), [`internal_error`](/errors#internal-error) (500). # Events (/api/events) Every mutation an API key makes writes an event. Poll [`GET /v1/events`](#list-events) to read them. ### Event object [#event-object] | Field | Type | Description | | ------------ | ------ | --------------------------------------------------------------------------------------------------- | | `id` | string | Event ID, prefixed `evt_`. | | `type` | string | The action, such as `file.upload`, `file.replace`, `file.move`, `file.delete`, or `project.create`. | | `data` | object | Event payload: `target_type`, `target_id`, `target_name`, and `api_key_id`. | | `created_at` | string | ISO 8601 creation time. | ## List events [#list-events] `GET /v1/events` A forward feed, oldest first. The key needs organization-level `read` permission. ### Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------------------------------------- | | `after` | string | No | Opaque cursor. Returns events written after it. Omit it to start at the beginning. | | `limit` | integer | No | Number of items, from 1 through 200. Defaults to 50. | | `type` | string | No | Return only events with this exact type. | ### Response fields [#response-fields] | Field | Type | Description | | ------------- | ---------------- | --------------------------------------------------------------- | | `items` | array | Event objects, oldest first. | | `next_cursor` | string or `null` | Cursor for the next request, or `null` when the page was empty. | `next_cursor` is returned whenever the page is not empty, including a page that was not full. Keep the last cursor you received and poll with it; each call returns everything written since. A malformed `after` or `limit` returns [`invalid_query`](/errors#invalid-query). Returns `200 OK`. ### Request [#request] ```bash title="Request" curl "https://agentfs.cloud/v1/events?limit=50&after=eyJrIjoiMTc3MjUzOTIwMDAwMDpldnRfYTFiMiJ9" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response] ```json title="200 OK" { "items": [ { "id": "evt_c3d4", "type": "file.upload", "data": { "target_type": "file", "target_id": "f_8f2c", "target_name": "/reports/report.pdf", "api_key_id": "k_5f1a" }, "created_at": "2026-03-02T12:00:01.000Z" } ], "next_cursor": "eyJrIjoiMTc3MjUzOTIwMTAwMDpldnRfYzNkNCJ9" } ``` ### Errors [#errors] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`forbidden`](/errors#forbidden) (403), [`invalid_query`](/errors#invalid-query) (400), [`internal_error`](/errors#internal-error) (500). # Files (/api/files) Use `/v1/files` for file operations and for quick one-request uploads up to 100 MiB. [Resumable uploads](./uploads.mdx) handle files up to 5 TB. ### File object [#file-object] The API returns this object for a file: | Field | Type | Description | | --------------------- | ---------------- | --------------------------------------------------------------------------------- | | `type` | string | Always `file`. | | `id` | string | File identifier. | | `name` | string | The final path segment. | | `path` | string | The path without its leading slash. | | `label` | string or `null` | Optional display label. | | `content_type` | string | Stored media type. | | `content_disposition` | string | `inline` or `attachment`. | | `size_bytes` | integer | Stored size in bytes. | | `sha256` | string or `null` | SHA-256 digest when available, as lowercase hexadecimal. | | `etag` | string or `null` | Storage entity tag. Use it as the `If-Match` precondition on a replace or a move. | | `revision` | integer | Byte generation. A new file starts at `1`; each replace adds one. | | `visibility` | string | Effective visibility: `public`, `unlisted`, or `private`. | | `expires_at` | string or `null` | ISO 8601 expiration time. | | `agent_id` | string or `null` | The `X-Agent-ID` sent by the request that created the file. | | `run_id` | string or `null` | The `X-Run-ID` sent by the request that created the file. | | `url` | string or `null` | Hosted URL for public and unlisted files. `null` for private files. | | `markdown` | string or `null` | Markdown link when `url` is present. | | `created_at` | string | ISO 8601 creation time. | | `updated_at` | string | ISO 8601 update time. | A file with no explicit visibility uses its project's current `default_visibility`. Public and unlisted files return a hosted URL; private files require a temporary access URL. Hosted URLs look like `https://agentfsusercontent.com/f/`. Browser-executed content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`, `text/xml`, and `application/xml`) are served from a separate, sandboxed origin instead: `https://s.agentfsusercontent.com/f/`. This applies whatever type the upload declared. Older `f.agentfs.cloud` and `s.agentfs.cloud` links keep working: they redirect permanently to the same path on the new host. ## Upload a file [#upload-a-file] `POST /v1/files` Store one file in a single request with a key that has `write` permission on the target path. One request carries up to 100 MiB; send larger files through `/v1/uploads`. ### Request body [#request-body] Use `multipart/form-data`, or send the bytes as the raw request body. **Raw body.** Send any `Content-Type` other than `multipart/form-data`, set `Content-Length` to the file size, and pass the fields below as query parameters (`path`, `project`, `prefix`, `name`, `if_exists`, `visibility`, `expires_in`, `content_disposition`, `label`). The `Content-Type` header is the file's type; `application/octet-stream` or a missing header means it is inferred from the name. Raw bodies stream straight through without being buffered, so this is the form to use from SDKs and scripts. With `Idempotency-Key`, a raw request is fingerprinted by its fields and size rather than its bytes. ```bash title="raw body" curl -sS -X POST "https://agentfs.cloud/v1/files?path=research/notes.md" \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: text/markdown" \ --data-binary @notes.md ``` **From a URL.** Send `application/json` with `url` plus any of the fields below. The server fetches the URL itself and streams the bytes into storage, so nothing passes through your process. The URL must be `http` or `https`, must not redirect, must not point at a private or internal host, and may deliver at most 100 MB. `content_type` overrides what the source reports; `name` sets the filename when no `path` is given, defaulting to the last URL segment. ```bash title="from a URL" curl -sS -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/report.pdf","project":"research"}' ``` **Form.** Use `multipart/form-data` with the fields below. | Field | Type | Required | Description | | --------------------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | file | Yes | File bytes. | | `path` | string | No | `project/filename` or a nested path. Required for a chosen location; needs an API key. | | `visibility` | string | No | `public`, `unlisted`, or `private` for authenticated uploads. Uploads without a key may omit it or send `unlisted`. If omitted for an authenticated upload, the file follows the project's current default visibility. | | `expires_in` | string or `null` | No | Duration such as `24h` or `7d` using an `s`, `m`, `h`, `d`, or `w` suffix, or `null`. Authenticated values can be at most 52 weeks. Uploads without a key cannot set it and expire after 24 hours. If omitted, a project default may apply; `null` is accepted but also allows that project default to apply. | | `content_disposition` | string | No | `inline` or `attachment`. Defaults to `inline`. | | `label` | string or `null` | No | Optional label, at most 200 characters. | | `if_exists` | string | No | `error` (the default) or `replace`. See [upsert to a path](#upsert-to-a-path). `replace` requires `path`, so it requires an API key. | Path segments may contain only letters, digits, `.`, `_`, and `-`; paths have at most 16 segments and 2,048 encoded characters. When an authenticated request omits `path`, use the key's default project when it has one, otherwise `default`, and generate a sanitized filename with a suffix. Uploads without a key generate a path under `guest`; see [Upload without a key](/uploads#upload-without-a-key). The content type is inferred from the filename when the uploaded file reports no type or reports `application/octet-stream`. ### Idempotency header [#idempotency-header] `Idempotency-Key` is optional and must contain 1–200 characters. A repeated key with the same request returns the existing file. Reusing it for a different request returns [`idempotency_conflict`](/errors#idempotency-conflict). Returns `201 Created` and a `Location` header containing `/v1/files/{id}`. ### Upsert to a path [#upsert-to-a-path] By default a live file at `path` returns [`path_exists`](/errors#path-exists). Send `if_exists=replace` to write over it instead: ```bash title="Upsert" curl -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -F "file=@report.pdf;type=application/pdf" \ -F "path=reports/report.pdf" \ -F "if_exists=replace" ``` The file keeps its ID, path, and hosted URL; `revision` goes up by one. Only the bytes and the content type change. Visibility, label, and expiry stay as they are, so set them with the upload that created the file. The response is `200 OK` with the file object. A create is still `201 Created`. Uploading the same bytes and content type again is a no-op that returns the current file, so a retry is safe. Send `If-Match: ` to make the replacement conditional on the file you last read. A mismatch returns `412` [`precondition_failed`](/errors#precondition-failed) and nothing is written. `if_exists=replace` needs a `path`. ### Response fields [#response-fields] Returns a [file object](#file-object). ### Request [#request] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/files \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Idempotency-Key: report-2026-03" \ -F "file=@report.pdf;type=application/pdf" \ -F "path=reports/report.pdf" \ -F "visibility=private" \ -F "expires_in=24h" \ -F "label=Quarterly report" ``` ### Response [#response] ```json title="201 Created" { "type": "file", "id": "f_8f2c", "name": "report.pdf", "path": "reports/report.pdf", "label": "Quarterly report", "content_type": "application/pdf", "content_disposition": "inline", "size_bytes": 48213, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-report", "revision": 1, "visibility": "private", "expires_at": "2026-03-03T12:00:00.000Z", "agent_id": null, "run_id": null, "url": null, "markdown": null, "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:01.000Z" } ``` ### Errors [#errors] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`invalid_metadata`](/errors#invalid-metadata) (400), [`invalid_path`](/errors#invalid-path) (400), [`api_key_required`](/errors#api-key-required) (401), [`precondition_failed`](/errors#precondition-failed) (412), [`file_pinned`](/errors#file-pinned) (409), [`forbidden`](/errors#forbidden) (403), [`file_too_large`](/errors#file-too-large) (413), [`storage_limit_exceeded`](/errors#storage-limit-exceeded) (413), [`billing_unavailable`](/errors#billing-unavailable) (503), [`quota_exceeded`](/errors#quota-exceeded) (429), [`idempotency_conflict`](/errors#idempotency-conflict) (409), [`path_exists`](/errors#path-exists) (409), [`region_unavailable`](/errors#region-unavailable) (422), [`project_create_failed`](/errors#project-create-failed) (500), [`folder_create_failed`](/errors#folder-create-failed) (500), [`upload_unavailable`](/errors#upload-unavailable) (503), [`upload_failed`](/errors#upload-failed) (502), [`anonymous_quota_unavailable`](/errors#anonymous-quota-unavailable) (503), [`internal_error`](/errors#internal-error) (500). ## List files [#list-files] `GET /v1/files` List files visible to the API key, ready files by default. Results are ordered newest first, with the file ID breaking ties. The key must have `read` permission for the requested project or path. ### Query parameters [#query-parameters] Filters combine. They narrow the list; the key's read scopes are applied separately and always win. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project` | string | No | Project name or project ID. Values beginning with `p_` are treated as IDs; other values are treated as names. | | `path` | string | No | One exact path such as `reports/2026/final.pdf`. | | `prefix` | string | No | Path prefix such as `reports/2026`. Matches that path and everything below it. | | `q` | string | No | Case-insensitive text matched against the file name or stored path. `search` is the same filter under its older name. | | `search` | string | No | Alias of `q`. | | `run_id` | string | No | Files created by requests that sent this `X-Run-ID`. | | `agent_id` | string | No | Files created by requests that sent this `X-Agent-ID`. | | `status` | string | No | Comma list of `ready`, `uploading`, `failed`, and `trashed`. Defaults to `ready`. Use `trashed` to find deleted files you can [restore](#restore-a-file). | | `limit` | integer | No | Number of items, from 1 through 200. Defaults to 50. | | `cursor` | string | No | Opaque cursor from the previous response. | A filter value must contain 1–200 characters; anything else returns [`invalid_query`](/errors#invalid-query). ```bash title="Combined filters" curl -G "https://agentfs.cloud/v1/files" \ -H "Authorization: Bearer $AGENTFS_KEY" \ --data-urlencode "prefix=reports/2026" \ --data-urlencode "run_id=run-2026-04-01" ``` ### Response fields [#response-fields-1] | Field | Type | Description | | -------------------- | ---------------- | --------------------------------------------------------------- | | `items` | array | File objects with `project` and `project_id` fields added. | | `items[].project` | string | Project name. | | `items[].project_id` | string | Project ID. | | `items[].status` | string | `ready`, `uploading`, `failed`, or `trashed`. | | `next_cursor` | string or `null` | Cursor for the next page, or `null` when there is no next page. | Returns `200 OK`. ### Request [#request-1] ```bash title="Request" curl "https://agentfs.cloud/v1/files?project=p_1a2b&limit=50&q=report" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-1] ```json title="200 OK" { "items": [ { "type": "file", "id": "f_8f2c", "name": "report.pdf", "path": "reports/report.pdf", "label": "Quarterly report", "content_type": "application/pdf", "content_disposition": "inline", "size_bytes": 48213, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-report", "revision": 1, "visibility": "public", "expires_at": null, "agent_id": "research-agent", "run_id": "run_2026_03_02", "url": "https://agentfsusercontent.com/f/f_8f2c", "markdown": "[Quarterly report](https://agentfsusercontent.com/f/f_8f2c)", "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:01.000Z", "project": "reports", "project_id": "p_1a2b", "status": "ready" } ], "next_cursor": null } ``` ### Errors [#errors-1] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`project_not_found`](/errors#project-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`invalid_query`](/errors#invalid-query) (400), [`internal_error`](/errors#internal-error) (500). ## Get a file [#get-a-file] `GET /v1/files/:id` Return one ready file with an API key that has `read` permission on its path. ### Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Response fields [#response-fields-2] Returns a [file object](#file-object). Returns `200 OK`. ### Request [#request-2] ```bash title="Request" curl https://agentfs.cloud/v1/files/f_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-2] ```json title="200 OK" { "type": "file", "id": "f_8f2c", "name": "report.pdf", "path": "reports/report.pdf", "label": "Quarterly report", "content_type": "application/pdf", "content_disposition": "inline", "size_bytes": 48213, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-report", "revision": 1, "visibility": "private", "expires_at": "2026-03-03T12:00:00.000Z", "agent_id": null, "run_id": null, "url": null, "markdown": null, "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:01.000Z" } ``` ### Errors [#errors-2] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). ## Read a file's bytes [#read-a-files-bytes] `GET /v1/files/:id/content` Redirect to the file's bytes. The key needs `read` permission on the file path. A public or unlisted file redirects to its hosted URL; a private file redirects to a signed URL that is valid for 5 minutes. Use this to read a file you stored without minting a share link first. ### Path parameters [#path-parameters-1] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Response [#response-3] Returns `302 Found` with the URL in `Location` and an empty body. Follow the redirect with `curl -L`. If the file was blocked for abuse, the hosted URL answers `451` [`file_blocked`](/errors#file-blocked). ```http title="302 Found" HTTP/1.1 302 Found location: https://agentfsusercontent.com/f/f_8f2c?expires=1772539200&v=1&signature=example-signature cache-control: no-store ``` ### Request [#request-3] ```bash title="Request" curl -L https://agentfs.cloud/v1/files/f_8f2c/content \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Errors [#errors-3] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). ## Delete a file [#delete-a-file] `DELETE /v1/files/:id` Delete a file with an API key that has `delete` permission on its path, including an unfinished file row. A delete moves the file to the trash, where it is purged after 30 days; **its bytes still count toward your storage** until then. Until it is purged, you can [restore it](#restore-a-file). To free the space now, add `?permanent=true`: the file is removed for good and cannot be restored. Both forms are safe to repeat. Repeating a permanent delete returns [`file_not_found`](/errors#file-not-found) (404), because the file is gone. ### Path parameters [#path-parameters-2] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Query parameters [#query-parameters-1] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `permanent` | `true` | No | Delete for good and free the storage now. Works on a file that is already in the trash. | ### Response [#response-4] Returns `204 No Content` with an empty body. ```http title="204 No Content" HTTP/1.1 204 No Content ``` ### Request [#request-4] ```bash title="Request" curl -X DELETE https://agentfs.cloud/v1/files/f_8f2c \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Errors [#errors-4] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`storage_unavailable`](/errors#storage-unavailable) (503), [`storage_delete_failed`](/errors#storage-delete-failed) (502), [`internal_error`](/errors#internal-error) (500). ## Restore a file [#restore-a-file] `POST /v1/files/:id/restore` Bring a file back from the trash. It returns to the same path with the same ID and hosted URL. The key needs `write` permission on the file path. Restoring a file that is already live returns it again with `200`, so a retry is safe. If another live file now holds the path, the response is `409` [`path_exists`](/errors#path-exists): move or delete that file first. A file that was purged, deleted with `?permanent=true`, or never existed returns `404` [`file_not_found`](/errors#file-not-found). ### Path parameters [#path-parameters-3] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Response [#response-5] Returns `200 OK` with the [file object](#file-object). ### Request [#request-5] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/files/f_8f2c/restore \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Errors [#errors-5] [`unauthorized`](/errors#unauthorized) (401), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`path_exists`](/errors#path-exists) (409), [`internal_error`](/errors#internal-error) (500). ## Create a file access URL [#create-a-file-access-url] `POST /v1/files/:id/access` Return a hosted URL for a file the API key can share. The key must have `share` permission on the file path. Public and unlisted files return their existing URL and `expires_at: null`. Private files get a signed URL that defaults to one hour and may last up to seven days. ### Path parameters [#path-parameters-4] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Request body [#request-body-1] Send a JSON object. The body may be empty. | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `expires_in` | string | No | Duration such as `1h` or `7d` using an `s`, `m`, `h`, `d`, or `w` suffix. Used for private files only; the maximum is seven days. | ### Response fields [#response-fields-3] | Field | Type | Description | | ------------ | ---------------- | ------------------------------------------------------------------------------- | | `file_id` | string | File ID. | | `url` | string | Hosted file URL. | | `markdown` | string | Markdown link using the file label or name. | | `expires_at` | string or `null` | ISO 8601 expiration for a private URL, or `null` for public and unlisted files. | Returns `200 OK`. ### Request [#request-6] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/files/f_8f2c/access \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: application/json" \ -d '{"expires_in":"24h"}' ``` ### Response [#response-6] ```json title="200 OK" { "file_id": "f_8f2c", "url": "https://agentfsusercontent.com/f/f_8f2c?expires=1772539200&v=1&signature=example-signature", "markdown": "[Quarterly report](https://agentfsusercontent.com/f/f_8f2c?expires=1772539200&v=1&signature=example-signature)", "expires_at": "2026-03-03T12:00:00.000Z" } ``` ### Errors [#errors-6] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`invalid_expiration`](/errors#invalid-expiration) (400), [`internal_error`](/errors#internal-error) (500). ## Revoke private links [#revoke-private-links] `POST /v1/files/:id/access/revoke` Invalidate every private link issued for this file so far. Links created before the revoke return `404` from then on; links you create afterwards work as usual. Use it when a private link leaked. The key needs `write` permission on the file path. Public and unlisted URLs are not affected. To hide one of those, change the file's visibility or delete it. ### Path parameters [#path-parameters-5] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------- | | `id` | string | Yes | File ID. | ### Response fields [#response-fields-4] | Field | Type | Description | | ---------------- | ------- | ------------------------------------------------------------- | | `id` | string | File ID. | | `access_version` | integer | The file's new access version. Private links carry it as `v`. | Returns `200 OK`. ### Request [#request-7] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/files/f_8f2c/access/revoke \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-7] ```json title="200 OK" { "id": "f_8f2c", "access_version": 2 } ``` ### Errors [#errors-7] [`unauthorized`](/errors#unauthorized) (401), [`file_not_found`](/errors#file-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). # API reference (/api) Call the API at `https://agentfs.cloud`. This guide explains the shared behavior of the API. For generated, endpoint-by-endpoint request and response details, use the **Endpoint reference** section in the sidebar. ## Authentication [#authentication] Send an API key in the `Authorization` header: ```bash title="Authorization" curl https://agentfs.cloud/v1/me \ -H "Authorization: Bearer $AGENTFS_KEY" ``` Use the [device authorization flow](/api/device-auth) to connect an agent and receive a key. The server accepts the key only in the `Bearer ` form. A missing or invalid key returns [`unauthorized`](/errors#unauthorized). Most `/v1` endpoints require an API key. The exceptions are: * `GET /v1/me` returns an unauthenticated response when you omit `Authorization`. * The device authorization endpoints have their own authentication rules. * The multipart upload endpoints also accept an authenticated browser session when no `Authorization` header is present. ### API key permissions [#api-key-permissions] A key carries one or more scopes. A scope can cover the whole organization, a project, or a path prefix within a project. | Field | Type | Description | | ------------- | ---------------- | --------------------------------------------------------------------------------------------------- | | `project_id` | string or `null` | `null` targets the organization. Otherwise, use a project ID. | | `prefix` | string | An empty string targets the whole project. A non-empty value targets that path and its descendants. | | `permissions` | array | One or more of `read`, `write`, `delete`, or `share`. | The required permission depends on the endpoint. File reads and listings use `read`; uploads and project updates use `write`; file and upload deletion use `delete`; temporary file access URLs use `share`. Creating a project requires organization-level `write`. Batch reads and writes check every path in the batch. ## Requests and responses [#requests-and-responses] Send JSON with `Content-Type: application/json` where an endpoint documents a JSON body. Use `multipart/form-data` for one-shot files and batches. Multipart upload parts use a binary request body. Successful JSON responses use `application/json`. A successful `204` response has no body. Errors use [`application/problem+json`](/errors) and have this shape: ```json title="Error response" { "type": "https://agentfs.cloud/docs/errors#invalid-path", "title": "invalid_path", "status": 400, "code": "invalid_path", "detail": "Path segments are malformed.", "retryable": false } ``` `detail` is omitted when the handler does not provide one. `retryable` is `true` for status codes from `500` through `599`, and for `429`. It is `false` otherwise. Every `429` and `503` carries a `Retry-After` header in seconds. Retry `408`, `429`, and `5xx` with exponential backoff; see [Retries](/retries). See the [errors guide](/errors) for the error catalog. ## Rate limits [#rate-limits] `/v1` requests carrying an `Authorization` header are counted per key: 600 requests per 60 seconds. Uploads without a key to `POST /v1/files` are counted per IP address: 20 per 60 seconds. `POST /v1/auth/device` is counted per IP address: 10 per 60 seconds. Over a limit, the response is `429` [`rate_limited`](/errors#rate-limited) with `retryable: true` and a `Retry-After` header in seconds. Wait for it and retry the same request. ## Pagination [#pagination] `GET /v1/files` and `GET /v1/projects` accept `limit` and `cursor`. `limit` defaults to `50` and must be an integer from `1` through `200`. Pass the returned `next_cursor` as the next request's `cursor`. Cursors are opaque. A malformed cursor returns [`invalid_query`](/errors#invalid-query); project listing also rejects a syntactically valid cursor that is not in the visible collection. ```json title="Paginated response" { "items": [], "next_cursor": null } ``` [`GET /v1/events`](./events.mdx#list-events) pages the other way. It is a forward feed: it reads oldest first, takes its cursor as `after`, and returns `next_cursor` on every non-empty page so an agent can keep polling from the last one it saw. ## Idempotency [#idempotency] `POST /v1/files` accepts an optional `Idempotency-Key` header with 1–200 characters. Repeating the key with the same upload returns the existing file with `201`; changing the request for the same key returns [`idempotency_conflict`](/errors#idempotency-conflict). Uploads without an API key cannot send it. `POST /v1/batch` requires `Idempotency-Key` with at most 200 characters. Repeating it with a different manifest returns [`idempotency_conflict`](/errors#idempotency-conflict). Batch responses use `200`, including when the existing batch is returned. `POST /v1/files` with [`if_exists=replace`](./files.mdx#upsert-to-a-path) is safe to retry without a key: writing the same bytes to the same path again returns the current file unchanged. ## Agent and run headers [#agent-and-run-headers] When you authenticate with an API key, you may send these printable labels: | Header | Maximum length | Recorded as | | -------------- | -------------: | ------------------ | | `X-Agent-ID` | 128 characters | Agent identifier | | `X-Run-ID` | 128 characters | Run identifier | | `X-Session-ID` | 128 characters | Session identifier | The values are trimmed and must contain at least one printable character. An invalid value returns [`invalid_agent_label`](/errors#invalid-agent-label). These headers identify a request; they do not authenticate it. The API does not return a separate request ID header. A file records the `X-Agent-ID` and `X-Run-ID` of the request that created it, as `agent_id` and `run_id` on the [file object](./files.mdx#file-object). Both are list filters: `GET /v1/files?run_id=...` returns everything one run wrote. ## Paths [#paths] File paths use the form `project/filename` or `project/folder/filename`; responses omit the leading slash. Leading and repeated slashes are normalized. Each segment may contain only letters, digits, `.`, `_`, and `-`. The parser allows at most 16 segments and 2,048 encoded characters. The first segment is the project name. A path that names a project not yet present can create that project when the key has organization-level `write` permission. # Projects (/api/projects) Projects provide the first path segment for files. Use a project name in a file path, or use its ID in collection filters. ### Project object [#project-object] | Field | Type | Description | | -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | `type` | string | Always `project`. | | `id` | string | Project identifier. | | `name` | string | Project name. | | `site_url` | string or `null` | Project site URL, when one is set. | | `storage_region` | string | Where the project's files are stored: `us-east` or `eu-west`. Set when the project is created and cannot change. | | `default_visibility` | string | `public`, `unlisted`, or `private`. | | `default_expires_in` | integer or `null` | Default file expiration in seconds. | | `file_count` | integer | Number of ready files in the project. | | `size_bytes` | integer | Total bytes in ready files. | | `created_at` | string | ISO 8601 creation time. | | `updated_at` | string | ISO 8601 update time. | ## List projects [#list-projects] `GET /v1/projects` List projects when the API key has `read` permission on each project (an organization scope or a project scope with an empty prefix). The API filters inaccessible projects before it applies the cursor. ### Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------- | | `limit` | integer | No | Number of items, from 1 through 200. Defaults to 50. | | `cursor` | string | No | Opaque cursor from the previous response. | ### Response fields [#response-fields] | Field | Type | Description | | ------------- | ---------------- | --------------------------------------------------------------- | | `items` | array | Project objects. | | `next_cursor` | string or `null` | Cursor for the next page, or `null` when there is no next page. | Returns `200 OK`. ### Request [#request] ```bash title="Request" curl "https://agentfs.cloud/v1/projects?limit=50" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response] ```json title="200 OK" { "items": [ { "type": "project", "id": "p_1a2b", "name": "reports", "site_url": null, "storage_region": "us-east", "default_visibility": "unlisted", "default_expires_in": null, "file_count": 12, "size_bytes": 482130, "created_at": "2026-03-01T12:00:00.000Z", "updated_at": "2026-03-02T12:00:00.000Z" } ], "next_cursor": null } ``` ### Errors [#errors] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`invalid_query`](/errors#invalid-query) (400), [`internal_error`](/errors#internal-error) (500). ## Get a project [#get-a-project] `GET /v1/projects/:project` Return one project by name. Project names are matched case-insensitively, and the API key must have `read` permission on the whole project (an empty path prefix). ### Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------- | | `project` | string | Yes | URL-encoded project name. | ### Response fields [#response-fields-1] Returns a [project object](#project-object). Returns `200 OK`. ### Request [#request-1] ```bash title="Request" curl https://agentfs.cloud/v1/projects/reports \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-1] ```json title="200 OK" { "type": "project", "id": "p_1a2b", "name": "reports", "site_url": null, "storage_region": "us-east", "default_visibility": "unlisted", "default_expires_in": null, "file_count": 12, "size_bytes": 482130, "created_at": "2026-03-01T12:00:00.000Z", "updated_at": "2026-03-02T12:00:00.000Z" } ``` ### Errors [#errors-1] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`project_not_found`](/errors#project-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). ## Create a project [#create-a-project] `POST /v1/projects` Create a project. The API key must have organization-level `write` permission; a project-scoped key cannot create a project. ### Request body [#request-body] Send JSON with `Content-Type: application/json`. | Field | Type | Required | Description | | -------------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | string | Yes | 1–100 characters. The final name must start with a letter or digit and contain only letters, digits, `.`, `_`, and `-`. | | `default_visibility` | string | No | `public`, `unlisted`, or `private`. Defaults to `unlisted`. | | `default_expires_in` | integer or `null` | No | Positive seconds up to 31,536,000 (one year), or `null`. | | `storage_region` | string | No | `us-east` (default) or `eu-west`. `eu` is accepted as a shorthand for `eu-west`. The files are stored in that region, and it cannot change later. | Returns `201 Created` with a [project object](#project-object). The API route does not accept `site_url` on create. ### Response fields [#response-fields-2] Returns a [project object](#project-object). ### Request [#request-2] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/projects \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "reports", "default_visibility": "unlisted", "default_expires_in": 86400 }' ``` ### Response [#response-2] ```json title="201 Created" { "type": "project", "id": "p_1a2b", "name": "reports", "site_url": null, "storage_region": "us-east", "default_visibility": "unlisted", "default_expires_in": 86400, "file_count": 0, "size_bytes": 0, "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:00:00.000Z" } ``` ### Errors [#errors-2] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`forbidden`](/errors#forbidden) (403), [`invalid_project`](/errors#invalid-project) (400), [`project_exists`](/errors#project-exists) (409), [`region_unavailable`](/errors#region-unavailable) (422), [`project_create_failed`](/errors#project-create-failed) (500), [`internal_error`](/errors#internal-error) (500). ## Update project settings [#update-project-settings] `PATCH /v1/projects/:project` Update a project's default visibility or expiration. The API key must have `write` permission for the project. ### Path parameters [#path-parameters-1] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------- | | `project` | string | Yes | URL-encoded project name. | ### Request body [#request-body-1] Send JSON with `Content-Type: application/json`. Include at least one field. | Field | Type | Required | Description | | -------------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------- | | `default_visibility` | string | No | `public`, `unlisted`, or `private`. | | `default_expires_in` | integer or `null` | No | Positive seconds up to 31,536,000, or `null`. Omit it to keep the current value. | | `storage_region` | string | No | Do not send this field. Any supplied value is rejected because a project's storage region cannot change. | Returns `200 OK` with the updated [project object](#project-object). ### Response fields [#response-fields-3] Returns a [project object](#project-object). ### Request [#request-3] ```bash title="Request" curl -X PATCH https://agentfs.cloud/v1/projects/reports \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: application/json" \ -d '{"default_visibility":"public"}' ``` ### Response [#response-3] ```json title="200 OK" { "type": "project", "id": "p_1a2b", "name": "reports", "site_url": null, "storage_region": "us-east", "default_visibility": "public", "default_expires_in": null, "file_count": 12, "size_bytes": 482130, "created_at": "2026-03-01T12:00:00.000Z", "updated_at": "2026-03-02T12:01:00.000Z" } ``` ### Errors [#errors-3] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`invalid_project_settings`](/errors#invalid-project-settings) (400), [`jurisdiction_immutable`](/errors#jurisdiction-immutable) (409), [`project_not_found`](/errors#project-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). ## Delete a project [#delete-a-project] `DELETE /v1/projects/:project` Delete a project the API key can delete. The key must have `delete` permission for the project. Only team owners and admins can delete a project; for an API key, that is the role of the key's owner. By default, the project must contain no files. Pass `recursive=true` to delete its files and folders with the project. The rows are deleted at once, whatever the project's size, and the stored bytes within the hour. ### Path parameters [#path-parameters-2] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------- | | `project` | string | Yes | URL-encoded project name. | ### Query parameters [#query-parameters-1] | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------------ | | `recursive` | string | No | Delete contents only when the value is exactly `true`. Any other value behaves as `false`. | ### Response [#response-4] Returns `204 No Content` with an empty body. ```http title="204 No Content" HTTP/1.1 204 No Content ``` ### Request [#request-4] ```bash title="Request" curl -X DELETE "https://agentfs.cloud/v1/projects/reports?recursive=true" \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Errors [#errors-4] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`project_not_found`](/errors#project-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`project_not_empty`](/errors#project-not-empty) (409), [`internal_error`](/errors#internal-error) (500). # Uploads (/api/uploads) Resumable uploads handle files up to 5,000,000,000,000 bytes (5 TB), and are required above 100 MiB. Files up to 5,000,000,000 bytes go up in one `PUT` unless you ask for parts. Larger files go up in at most 10,000 parts, each `part_size` bytes except the last. The API keeps at most 64 active uploads for an organization. A session expires after 24 hours, or sooner when the file's expiration is sooner. An authenticated browser session can call these endpoints without an API key; an agent should send an API key. ### Upload session shape [#upload-session-shape] | Field | Type | Description | | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Always `upload`. | | `id` | string | Upload session ID. | | `file_id` | string | Reserved file ID. | | `status` | string | `creating`, `active`, `completing`, `completed`, `aborted`, or `failed`. | | `transport` | string | `s3`: send the bytes to `upload_url`, or to the URLs from [part URLs](#get-part-urls). `worker`: send parts to the API with [upload a streamed part](#upload-a-streamed-part). | | `part_size` | integer | The file size for a single-`PUT` session. Otherwise the required size of every non-final part; the final part can be smaller. | | `total_parts` | integer | Number of parts calculated from `expected_size_bytes` and `part_size`. | | `expected_size_bytes` | integer | Declared complete-file size. | | `uploaded_parts` | array | Recorded parts, ordered by `part_number`. | | `uploaded_parts[].part_number` | integer | Part number starting at 1. | | `uploaded_parts[].etag` | string | Recorded part entity tag. | | `uploaded_parts[].checksum_sha256` | string or `null` | Recorded direct-part SHA-256 checksum, when supplied. | | `uploaded_parts[].size_bytes` | integer | Recorded part size. | | `expires_at` | string | ISO 8601 session expiration time. | | `upload_url` | string | Present on an active `s3` session of at most 5,000,000,000 bytes: a signed URL that accepts one `PUT` of exactly `expected_size_bytes` bytes. Larger `s3` sessions have no `upload_url`; fetch part URLs instead. | | `upload_url_expires_at` | string | ISO 8601 time after which `upload_url` stops working. Fetch the session again for a fresh one. | | `file` | object | Included on a completed session; this is the [file object](./files.mdx#file-object). | `upload_url` is a capability: anyone holding it can write that one object until it expires. Do not log it. ## Create an upload [#create-an-upload] `POST /v1/uploads` Create a file record and a resumable upload session. Send an API key with `write` permission on the target path, or use an authenticated browser session. Creating is safe to retry. If the response is lost, send the same request again: while the first session is still live, the same `path`, `size_bytes` and `sha256` return that session with `200` instead of [`path_exists`](/errors#path-exists). A different size or hash for the same path is still a conflict. Files up to 5,000,000,000 bytes get a session with `upload_url`: `PUT` the whole file there, then call complete. Larger files, and any file sent with `multipart: true`, go up in parts: follow the session's `transport`, then call complete. Every part is resumable on its own. If you get `503` [`upload_unavailable`](/errors#upload-unavailable), retry shortly. The `url` in the create response is where the file will live. An HTML, SVG, or XML file can move to the sandboxed host `s.agentfsusercontent.com` at completion, so share the `url` from the completed file object. ### Request body [#request-body] Send JSON. | Field | Type | Required | Description | | --------------------- | ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | string | Yes | `project/filename` or a nested path. The path must include a project and filename. | | `size_bytes` | integer | Yes | Positive declared size, at most 5,000,000,000,000 bytes. | | `content_type` | string | No | Trimmed media type, 1–255 characters. If omitted, infer it from the filename. `application/octet-stream` is also replaced by filename inference. | | `sha256` | string | No | Expected SHA-256 digest of the whole file as 64 hexadecimal characters. Completion reads the stored bytes back to check it, so it is accepted only for uploads up to 1 GiB (1,073,741,824 bytes); a mismatch returns `422 checksum_mismatch`. | | `visibility` | string | No | `public`, `unlisted`, or `private`. | | `expires_in` | string or `null` | No | Duration such as `24h` or `7d` using an `s`, `m`, `h`, `d`, or `w` suffix, or `null`. A string can be at most 52 weeks. When omitted, use the project's default expiration if one exists. | | `label` | string or `null` | No | Optional label, at most 200 characters. | | `content_disposition` | string | No | `inline` or `attachment`. Defaults to `inline`. | | `multipart` | boolean | No | Defaults to `false`. `true` gives a multipart session at any size, with parts fetched through [part URLs](#get-part-urls), so the upload can resume part by part. With `false`, files up to 5,000,000,000 bytes get one `upload_url`. | Paths use at most 16 segments and 2,048 encoded characters. Every segment may contain only letters, digits, `.`, `_`, and `-`. Returns `201 Created`. ### Response fields [#response-fields] Returns an [upload session](#upload-session-shape). A new session has no `uploaded_parts`. A direct session may include `direct`; a streamed session does not. ### Request [#request] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/uploads \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Type: application/json" \ -d '{ "path": "media/video.mp4", "size_bytes": 150000000, "content_type": "video/mp4", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "visibility": "private", "expires_in": "7d", "label": "Demo video", "content_disposition": "attachment", "multipart": true }' ``` ### Response [#response] ```json title="201 Created" { "type": "upload", "id": "up_7c31", "file_id": "f_8f2c", "status": "active", "transport": "s3", "part_size": 16777216, "total_parts": 9, "expected_size_bytes": 150000000, "uploaded_parts": [], "expires_at": "2026-03-03T12:00:00.000Z" } ``` ### Errors [#errors] [`unauthorized`](/errors#unauthorized) (401), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`csrf_rejected`](/errors#csrf-rejected) (403), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_upload`](/errors#invalid-upload) (400), [`forbidden`](/errors#forbidden) (403), [`path_exists`](/errors#path-exists) (409), [`upload_rate_limited`](/errors#upload-rate-limited) (429), [`storage_limit_exceeded`](/errors#storage-limit-exceeded) (413), [`billing_unavailable`](/errors#billing-unavailable) (503), [`invalid_metadata`](/errors#invalid-metadata) (400), [`region_unavailable`](/errors#region-unavailable) (422), [`project_create_failed`](/errors#project-create-failed) (500), [`folder_create_failed`](/errors#folder-create-failed) (500), [`upload_unavailable`](/errors#upload-unavailable) (503), [`upload_create_failed`](/errors#upload-create-failed) (500 or 502), [`upload_not_found`](/errors#upload-not-found) (404), [`upload_not_active`](/errors#upload-not-active) (409), [`invalid_token`](/errors#invalid-token) (400), [`unknown_bucket`](/errors#unknown-bucket) (400), [`upload_failed`](/errors#upload-failed) (502), [`internal_error`](/errors#internal-error) (500). ## Get an upload [#get-an-upload] `GET /v1/uploads/:id` Return the current session and its recorded parts. The API key must have `write` permission on the upload's file path. ### Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------ | | `id` | string | Yes | Upload session ID. | An unfinished session that has expired is reported as not found. A completed session remains readable. ### Response fields [#response-fields-1] Returns an [upload session](#upload-session-shape). Completed sessions also include `file`. Returns `200 OK`. ### Request [#request-1] ```bash title="Request" curl https://agentfs.cloud/v1/uploads/up_7c31 \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-1] ```json title="200 OK" { "type": "upload", "id": "up_7c31", "file_id": "f_8f2c", "status": "active", "transport": "s3", "part_size": 16777216, "total_parts": 9, "expected_size_bytes": 150000000, "uploaded_parts": [ { "part_number": 1, "etag": "etag-part-1", "checksum_sha256": null, "size_bytes": 16777216 } ], "expires_at": "2026-03-03T12:00:00.000Z" } ``` ### Errors [#errors-1] [`unauthorized`](/errors#unauthorized) (401), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`upload_not_found`](/errors#upload-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`internal_error`](/errors#internal-error) (500). ## Get part URLs [#get-part-urls] `GET /v1/uploads/:id/part-urls?from=1&to=100` Signed `PUT` URLs for the parts of a multipart `s3` session, at most 100 per call. Each URL accepts exactly `size_bytes` bytes for its part and expires at `expires_at`; an expired URL answers `403`, so fetch again for fresh ones. Parts can be sent in parallel. Storage answers each part with an `ETag`; you do not need to send it back, completion reads the parts from storage. ```json title="200 OK" { "upload_id": "up_7c31", "part_size": 16777216, "total_parts": 9, "parts": [ { "part_number": 1, "size_bytes": 16777216, "url": "https://", "expires_at": "2026-03-02T17:55:00.000Z" } ] } ``` Errors: [`upload_not_found`](/errors#upload-not-found) (404), [`upload_not_active`](/errors#upload-not-active) (409) for a session without part URLs, [`invalid_part`](/errors#invalid-part) (400), [`upload_expired`](/errors#upload-expired) (410), [`upload_unavailable`](/errors#upload-unavailable) (503). ## Upload a streamed part [#upload-a-streamed-part] `PUT /v1/uploads/:id/parts/:part` Send one binary part to a `worker` session. The API key must have `write` permission on the file path. An `s3` session refuses parts with `409 upload_not_active`; send its bytes to `upload_url` instead. Non-final parts must be exactly `part_size` bytes. The final part must be exactly the remaining bytes of `expected_size_bytes`. Send the exact byte count in `Content-Length`; any other size is a `400 invalid_part`. Sending the same part number again replaces its recorded part. ### Path parameters [#path-parameters-1] | Parameter | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------------------------- | | `id` | string | Yes | Upload session ID. | | `part` | integer | Yes | Positive part number, starting at 1 and no greater than `total_parts`. | ### Request body [#request-body-1] Send the raw binary bytes. `Content-Length` is required. ### Response fields [#response-fields-2] | Field | Type | Description | | ------------- | ------- | --------------------------------- | | `part_number` | integer | Stored part number. | | `etag` | string | Entity tag returned for the part. | | `size_bytes` | integer | Stored part size. | Returns `200 OK`. ### Request [#request-2] ```bash title="Request" curl -X PUT https://agentfs.cloud/v1/uploads/up_7c31/parts/1 \ -H "Authorization: Bearer $AGENTFS_KEY" \ -H "Content-Length: $PART_SIZE" \ --data-binary @part-001.bin ``` ### Response [#response-2] ```json title="200 OK" { "part_number": 1, "etag": "etag-part-1", "size_bytes": 16777216 } ``` ### Errors [#errors-2] [`unauthorized`](/errors#unauthorized) (401), [`csrf_rejected`](/errors#csrf-rejected) (403), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`invalid_part`](/errors#invalid-part) (400), [`upload_not_found`](/errors#upload-not-found) (404), [`upload_not_active`](/errors#upload-not-active) (409), [`upload_expired`](/errors#upload-expired) (410), [`forbidden`](/errors#forbidden) (403), [`length_required`](/errors#length-required) (411), [`upload_unavailable`](/errors#upload-unavailable) (503), [`invalid_token`](/errors#invalid-token) (400), [`unknown_bucket`](/errors#unknown-bucket) (400), [`part_upload_failed`](/errors#part-upload-failed) (502), [`upload_failed`](/errors#upload-failed) (502), [`internal_error`](/errors#internal-error) (500). ## Complete an upload [#complete-an-upload] `POST /v1/uploads/:id/complete` Verify every part and mark the file ready. The API key must have `write` permission on the file path. A single-`PUT` `s3` session without `sha256` completes on its own a few seconds after its bytes arrive; calling this endpoint just finishes it sooner. For a `worker` or multipart `s3` session the server requires contiguous part numbers from 1 through `total_parts` and an exact sum of part sizes. For a single-`PUT` `s3` session it checks that the object at `upload_url` has arrived with exactly `expected_size_bytes` bytes, and verifies `sha256` when one was declared; `409 object_not_ready` means the bytes are not there yet. A retry after completion returns the file object again. `502` [`upload_complete_failed`](/errors#upload-complete-failed) means storage could not assemble the parts; it is safe to retry, and if storage already finished, the retry succeeds. ### Path parameters [#path-parameters-2] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------ | | `id` | string | Yes | Upload session ID. | ### Response fields [#response-fields-3] On success, return a [file object](./files.mdx#file-object) with `200 OK`. During a concurrent completion, return the [upload session](#upload-session-shape) with `202 Accepted` while the session remains `completing`. ### Request [#request-3] ```bash title="Request" curl -X POST https://agentfs.cloud/v1/uploads/up_7c31/complete \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Response [#response-3] ```json title="200 OK" { "type": "file", "id": "f_8f2c", "name": "video.mp4", "path": "media/video.mp4", "label": "Demo video", "content_type": "video/mp4", "content_disposition": "attachment", "size_bytes": 150000000, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "etag": "etag-video", "revision": 1, "visibility": "private", "expires_at": "2026-03-09T12:00:00.000Z", "tags": [], "metadata": {}, "agent_id": null, "run_id": null, "url": null, "markdown": null, "created_at": "2026-03-02T12:00:00.000Z", "updated_at": "2026-03-02T12:03:00.000Z" } ``` ### Errors [#errors-3] [`unauthorized`](/errors#unauthorized) (401), [`csrf_rejected`](/errors#csrf-rejected) (403), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`upload_not_found`](/errors#upload-not-found) (404), [`forbidden`](/errors#forbidden) (403), [`upload_not_active`](/errors#upload-not-active) (409), [`upload_expired`](/errors#upload-expired) (410), [`missing_parts`](/errors#missing-parts) (409), [`size_mismatch`](/errors#size-mismatch) (409 or 422), [`checksum_missing`](/errors#checksum-missing) (409), [`checksum_mismatch`](/errors#checksum-mismatch) (422), [`upload_not_pending`](/errors#upload-not-pending) (409), [`object_not_ready`](/errors#object-not-ready) (409), [`upload_unavailable`](/errors#upload-unavailable) (503), [`invalid_token`](/errors#invalid-token) (400), [`unknown_bucket`](/errors#unknown-bucket) (400), [`upload_complete_failed`](/errors#upload-complete-failed) (502), [`upload_finalize_failed`](/errors#upload-finalize-failed) (502), [`upload_failed`](/errors#upload-failed) (502), [`internal_error`](/errors#internal-error) (500). ## Abort an upload [#abort-an-upload] `DELETE /v1/uploads/:id` Abort an unfinished session and remove its reserved file. The API key must have `delete` permission on the file path. An unknown session is treated as already aborted and returns `204`. A completed session cannot be aborted. ### Path parameters [#path-parameters-3] | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------ | | `id` | string | Yes | Upload session ID. | ### Response [#response-4] Returns `204 No Content` with an empty body. ```http title="204 No Content" HTTP/1.1 204 No Content ``` ### Request [#request-4] ```bash title="Request" curl -X DELETE https://agentfs.cloud/v1/uploads/up_7c31 \ -H "Authorization: Bearer $AGENTFS_KEY" ``` ### Errors [#errors-4] [`unauthorized`](/errors#unauthorized) (401), [`csrf_rejected`](/errors#csrf-rejected) (403), [`no_active_organization`](/errors#no-active-organization) (403), [`invalid_agent_label`](/errors#invalid-agent-label) (400), [`forbidden`](/errors#forbidden) (403), [`upload_completed`](/errors#upload-completed) (409), [`upload_abort_unavailable`](/errors#upload-abort-unavailable) (503), [`upload_abort_failed`](/errors#upload-abort-failed) (502), [`upload_unavailable`](/errors#upload-unavailable) (503), [`invalid_token`](/errors#invalid-token) (400), [`upload_failed`](/errors#upload-failed) (502), [`internal_error`](/errors#internal-error) (500). # Get the current account (/api-reference/getAccount) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/me": { "get": { "operationId": "getAccount", "summary": "Get the current account", "description": "Returns the authenticated account. Without a bearer key it reports the guest caller.", "responses": { "200": { "$ref": "#/components/responses/Json" }, "401": { "$ref": "#/components/responses/Problem" } } } } } } # List files (/api-reference/listFiles) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files": { "get": { "operationId": "listFiles", "summary": "List files", "parameters": [ { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/cursor" }, { "in": "query", "name": "project", "schema": { "type": "string" } }, { "in": "query", "name": "prefix", "schema": { "type": "string" } }, { "in": "query", "name": "status", "description": "Comma list of ready, uploading, failed, and trashed. Defaults to ready.", "schema": { "type": "string" } } ], "responses": { "200": { "$ref": "#/components/responses/Json" }, "401": { "$ref": "#/components/responses/Problem" } } } } } } # Upload a file (/api-reference/uploadFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files": { "post": { "operationId": "uploadFile", "summary": "Upload a file", "description": "Accepts an upload with an API key, or without one: a keyless upload is limited to 100 MB, is always unlisted, expires after 24 hours, and cannot set path, project, prefix, if_exists=replace, expires_in, label, or Idempotency-Key. Send Idempotency-Key with a key to make a lost response safe to retry.", "security": [ { "bearerAuth": [] }, {} ], "parameters": [ { "$ref": "#/components/parameters/idempotencyKey" }, { "$ref": "#/components/parameters/ifMatch" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "file" ], "properties": { "file": { "type": "string", "format": "binary" }, "path": { "type": "string", "example": "research/report.pdf" }, "if_exists": { "type": "string", "enum": [ "error", "replace" ], "default": "error" }, "visibility": { "type": "string", "enum": [ "public", "unlisted", "private" ] }, "expires_in": { "type": "string", "example": "7d" }, "label": { "type": "string" }, "content_disposition": { "type": "string", "enum": [ "inline", "attachment" ] } } } } } }, "responses": { "200": { "$ref": "#/components/responses/File" }, "201": { "$ref": "#/components/responses/File" }, "409": { "$ref": "#/components/responses/Problem" }, "413": { "$ref": "#/components/responses/Problem" } } } } } } # Get file metadata (/api-reference/getFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}": { "get": { "operationId": "getFile", "summary": "Get file metadata", "responses": { "200": { "$ref": "#/components/responses/File" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # Move or rename a file (/api-reference/moveFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}": { "patch": { "operationId": "moveFile", "summary": "Move or rename a file", "parameters": [ { "$ref": "#/components/parameters/ifMatch" } ], "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/File" }, "409": { "$ref": "#/components/responses/Problem" } } } } } } # Delete a file (/api-reference/deleteFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}": { "delete": { "operationId": "deleteFile", "summary": "Delete a file", "parameters": [ { "in": "query", "name": "permanent", "schema": { "type": "boolean", "default": false } } ], "responses": { "204": { "description": "File deleted" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # Get a file download URL (/api-reference/downloadFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}/content": { "get": { "operationId": "downloadFile", "summary": "Get a file download URL", "parameters": [ { "$ref": "#/components/parameters/fileId" } ], "responses": { "302": { "description": "Redirect to the hosted file" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # Create a temporary private-file URL (/api-reference/createAccessUrl) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}/access": { "post": { "operationId": "createAccessUrl", "summary": "Create a temporary private-file URL", "parameters": [ { "$ref": "#/components/parameters/fileId" } ], "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/Json" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # Revoke every private link issued for a file (/api-reference/revokeAccessUrls) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}/access/revoke": { "post": { "operationId": "revokeAccessUrls", "summary": "Revoke every private link issued for a file", "parameters": [ { "$ref": "#/components/parameters/fileId" } ], "responses": { "200": { "$ref": "#/components/responses/Json" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # Restore a file from the trash (/api-reference/restoreFile) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/files/{id}/restore": { "post": { "operationId": "restoreFile", "summary": "Restore a file from the trash", "parameters": [ { "$ref": "#/components/parameters/fileId" } ], "responses": { "200": { "$ref": "#/components/responses/File" }, "404": { "$ref": "#/components/responses/Problem" }, "409": { "$ref": "#/components/responses/Problem" } } } } } } # Create a resumable upload (/api-reference/createUpload) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/uploads": { "post": { "operationId": "createUpload", "summary": "Create a resumable upload", "description": "Handles files up to 5 TB, and is required above 100 MiB. Files up to 5 GB get one upload_url unless the body sets multipart: true. Repeating an identical live request returns the existing session.", "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/Json" }, "201": { "$ref": "#/components/responses/Json" }, "429": { "$ref": "#/components/responses/Problem" } } } } } } # Get an upload session (/api-reference/getUpload) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/uploads/{id}": { "get": { "operationId": "getUpload", "summary": "Get an upload session", "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Abort an upload (/api-reference/abortUpload) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/uploads/{id}": { "delete": { "operationId": "abortUpload", "summary": "Abort an upload", "responses": { "204": { "description": "Upload aborted" } } } } } } # Upload one streamed part (/api-reference/uploadPart) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/uploads/{id}/parts/{part}": { "put": { "operationId": "uploadPart", "summary": "Upload one streamed part", "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Complete a resumable upload (/api-reference/completeUpload) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/uploads/{id}/complete": { "post": { "operationId": "completeUpload", "summary": "Complete a resumable upload", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } } ], "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/File" }, "409": { "$ref": "#/components/responses/Problem" } } } } } } # Upload a batch of files (/api-reference/createBatch) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/batch": { "post": { "operationId": "createBatch", "summary": "Upload a batch of files", "parameters": [ { "$ref": "#/components/parameters/idempotencyKey" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "manifest" ], "properties": { "manifest": { "type": "string", "description": "JSON batch manifest" } } } } } }, "responses": { "200": { "$ref": "#/components/responses/Json" }, "201": { "$ref": "#/components/responses/Json" }, "413": { "$ref": "#/components/responses/Problem" } } } } } } # Get a batch's progress and results (/api-reference/getBatch) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/batch/{id}": { "get": { "operationId": "getBatch", "summary": "Get a batch's progress and results", "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "$ref": "#/components/responses/Json" }, "404": { "$ref": "#/components/responses/Problem" } } } } } } # List projects (/api-reference/listProjects) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/projects": { "get": { "operationId": "listProjects", "summary": "List projects", "parameters": [ { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/cursor" } ], "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Create a project (/api-reference/createProject) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/projects": { "post": { "operationId": "createProject", "summary": "Create a project", "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "201": { "$ref": "#/components/responses/Json" }, "409": { "$ref": "#/components/responses/Problem" } } } } } } # Get a project (/api-reference/getProject) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/projects/{project}": { "get": { "operationId": "getProject", "summary": "Get a project", "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Update project settings (/api-reference/updateProject) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/projects/{project}": { "patch": { "operationId": "updateProject", "summary": "Update project settings", "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Delete an empty project (/api-reference/deleteProject) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/projects/{project}": { "delete": { "operationId": "deleteProject", "summary": "Delete an empty project", "responses": { "204": { "description": "Project deleted" }, "409": { "$ref": "#/components/responses/Problem" } } } } } } # List organization events (/api-reference/listEvents) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/events": { "get": { "operationId": "listEvents", "summary": "List organization events", "parameters": [ { "in": "query", "name": "after", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/limit" } ], "responses": { "200": { "$ref": "#/components/responses/Json" } } } } } } # Start device authorization (/api-reference/startDeviceAuthorization) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/auth/device": { "post": { "operationId": "startDeviceAuthorization", "summary": "Start device authorization", "security": [], "responses": { "200": { "$ref": "#/components/responses/Json" }, "429": { "$ref": "#/components/responses/Problem" } } } } } } # Approve a device authorization request (/api-reference/approveDeviceAuthorization) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/auth/device/approve": { "post": { "operationId": "approveDeviceAuthorization", "summary": "Approve a device authorization request", "security": [], "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/Json" }, "400": { "$ref": "#/components/responses/Problem" } } } } } } # Claim a device API key (/api-reference/claimDeviceToken) { "openapi": "3.2.0", "info": { "title": "AgentFS API", "version": "v1", "description": "The HTTP API for files, projects, resumable uploads, batches, and agent authentication. Every error uses application/problem+json." }, "paths": { "/v1/auth/device/token": { "post": { "operationId": "claimDeviceToken", "summary": "Claim a device API key", "security": [], "requestBody": { "$ref": "#/components/requestBodies/Json" }, "responses": { "200": { "$ref": "#/components/responses/Json" }, "428": { "$ref": "#/components/responses/Problem" } } } } } }