permahost.io permanent file hosting for ai agents

curl and the HTTP API

The CLI and the MCP tool both call the HTTP API at https://app.permahost.io/api/v1. An agent with only curl can call it directly with a Bearer token.

Prefer the CLI when possible

If the agent can install the CLI, use it instead of curl. permahost file upload dist/ uploads a whole directory in one command, keeps its token fresh, checks paths and credits before sending anything, and prints the URLs. With curl the agent does each of those by hand, one request per file. Install it with one line (install), then run permahost login.

Like the CLI, curl sends files from disk, so their bytes never pass through the model's context.

Get a token

Tokens come from the OAuth sign-in at app.permahost.io, which needs a browser once. The simplest way to get one is the CLI: permahost login saves an access token to its config file, and permahost whoami refreshes it when it is about to expire.

Shell
$ permahost login
$ permahost whoami > /dev/null
$ TOKEN=$(jq -r .accessToken ~/.config/permahost/config.json)
$ API=https://app.permahost.io/api/v1

The config file is $PERMAHOSTIO_CONFIG, or permahost/config.json in the user config directory: ~/.config on Linux, ~/Library/Application Support on macOS, %AppData% on Windows. An access token lasts 1 hour. When a call answers 401, run permahost whoami again and re-read the token; don't trade the refresh token yourself, or the CLI's saved one may stop working.

Without the CLI, any OAuth 2.1 client can get a token: the server's metadata is at https://app.permahost.io/.well-known/oauth-authorization-server (dynamic client registration, PKCE S256, refresh tokens).

Calls

Every call below sends -H "Authorization: Bearer $TOKEN". Responses are JSON; ids, times and sizes here are examples.

Check the limits and prices

Shell
$ curl -s -H "Authorization: Bearer $TOKEN" $API/limits
{"maxUploadBytes":16777216,"pricing":{"storageBlockBytes":26214400,"storageCredits":500,"folderCredits":500,"domainCredits":500,"domainSetupCredits":500}}

Create a folder

Shell
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" $API/folders
{"folder":{"id":"k7q2m9xa3bd","createdAt":"2026-11-02T08:14:00Z","sizeBytes":0,"url":"https://k7q2m9xa3bd.file.permahost.io/","title":"","defaultTitle":"k7q2m9xa3bd"},"creditsSpent":500}

A new folder costs 500 credits a month. Reuse it: every later file goes into this folder with its id.

Upload a file

The request body is the file; the URL path after /files/ is its path in the folder. Uploading to a path that already holds a file replaces it in place.

Shell
$ curl -s -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @dist/index.html $API/folders/k7q2m9xa3bd/files/index.html
{"file":{"path":"index.html","size":4210,"updatedAt":"2026-11-02T08:14:05Z","url":"https://k7q2m9xa3bd.file.permahost.io/index.html"},"creditsSpent":500}
$ curl -s -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @dist/css/app.css $API/folders/k7q2m9xa3bd/files/css/app.css
{"file":{"path":"css/app.css","size":3072,"updatedAt":"2026-11-02T08:14:06Z","url":"https://k7q2m9xa3bd.file.permahost.io/css/app.css"},"creditsSpent":0}

The first file started the account's first 25 MB storage block this month (500 credits); the second stayed inside it. Use --data-binary, not -d, so curl sends the bytes unchanged.

List a folder, and all folders

Shell
$ curl -s -H "Authorization: Bearer $TOKEN" $API/folders/k7q2m9xa3bd
{"folder":{"id":"k7q2m9xa3bd",...},"files":[{"path":"css/app.css","size":3072,...},{"path":"index.html","size":4210,...}]}
$ curl -s -H "Authorization: Bearer $TOKEN" $API/folders
{"folders":[{"id":"k7q2m9xa3bd","createdAt":"2026-11-02T08:14:00Z","sizeBytes":7282,"url":"https://k7q2m9xa3bd.file.permahost.io/","title":"","defaultTitle":"index.html"}]}

Download, rename, delete

Shell
$ curl -s -H "Authorization: Bearer $TOKEN" -o index.html $API/folders/k7q2m9xa3bd/files/index.html
$ curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"title":"Q3 report"}' $API/folders/k7q2m9xa3bd
$ curl -s -X DELETE -H "Authorization: Bearer $TOKEN" $API/folders/k7q2m9xa3bd/files/css/app.css
$ curl -s -X DELETE -H "Authorization: Bearer $TOKEN" $API/folders/k7q2m9xa3bd

Downloads through the API always come as attachments; the public URL is the one to share.

Credits and usage

Shell
$ curl -s -H "Authorization: Bearer $TOKEN" $API/users/me/usage
{"usage":{"folders":1,"storageBytes":7282,"storageBlocks":1,"domains":0,"monthlyCredits":1000,"pricing":{...}}}

GET /users/me returns the account with credits, creditsLimit and creditsRenewAt.

Custom domains

Shell
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"hostname":"www.example.com"}' $API/folders/k7q2m9xa3bd/domains
{"domain":{"id":"3hVt9qXc2Lm","folderId":"k7q2m9xa3bd","hostname":"www.example.com","target":"k7q2m9xa3bd.file.permahost.io","status":"pending","createdAt":"2026-11-02T08:20:00Z"}}
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" $API/folders/k7q2m9xa3bd/domains/3hVt9qXc2Lm/verify
{"domain":{"id":"3hVt9qXc2Lm",...,"status":"active","verifiedAt":"2026-11-02T08:31:00Z"}}

Create the CNAME record from hostname to target before verifying. See custom domains.

Errors

Errors are JSON, such as {"error":"insufficient credits"}.

StatusMeaning
400Bad request: invalid file path, invalid folder title, invalid hostname.
401No token, or it expired. Refresh it (permahost whoami) and retry.
402insufficient credits: this month's credits can't pay for a new folder, storage block or domain.
404not found: no folder, file or domain of yours with that id or path.
409domain already in use: another folder has verified that hostname.
413upload too large: the file is over maxUploadBytes.
422domain not verified: the CNAME record isn't there yet, or points elsewhere.

Endpoints

CallWhat it does
GET /limitsUpload limit and prices
GET /users/meThe account and its credits
GET /users/me/usageWhat the account hosts and its monthly cost
POST /foldersCreate a folder
GET /foldersList your folders
GET /folders/{id}A folder and its files
PATCH /folders/{id}Rename a folder: {"title": "..."}
DELETE /folders/{id}Delete a folder and its files
PUT /folders/{id}/files/{path}Write a file (the request body)
GET /folders/{id}/files/{path}Download a file
DELETE /folders/{id}/files/{path}Delete a file
GET /folders/{id}/domainsList a folder's domains
POST /folders/{id}/domainsAdd a domain: {"hostname": "..."}
POST /folders/{id}/domains/{domainId}/verifyCheck the CNAME record now and activate the domain
DELETE /folders/{id}/domains/{domainId}Remove a domain