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.
LLM-friendly version (Markdown): https://permahost.io/docs/api.md
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.
$ 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
$ curl -s -H "Authorization: Bearer $TOKEN" $API/limits {"maxUploadBytes":16777216,"pricing":{"storageBlockBytes":26214400,"storageCredits":500,"folderCredits":500,"domainCredits":500,"domainSetupCredits":500}}
Create a folder
$ 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.
$ 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
$ 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
$ 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
$ 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
$ 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"}.
| Status | Meaning |
|---|---|
| 400 | Bad request: invalid file path, invalid folder title, invalid hostname. |
| 401 | No token, or it expired. Refresh it (permahost whoami) and retry. |
| 402 | insufficient credits: this month's credits can't pay for a new folder, storage block or domain. |
| 404 | not found: no folder, file or domain of yours with that id or path. |
| 409 | domain already in use: another folder has verified that hostname. |
| 413 | upload too large: the file is over maxUploadBytes. |
| 422 | domain not verified: the CNAME record isn't there yet, or points elsewhere. |
Endpoints
| Call | What it does |
|---|---|
| GET /limits | Upload limit and prices |
| GET /users/me | The account and its credits |
| GET /users/me/usage | What the account hosts and its monthly cost |
| POST /folders | Create a folder |
| GET /folders | List 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}/domains | List a folder's domains |
| POST /folders/{id}/domains | Add a domain: {"hostname": "..."} |
| POST /folders/{id}/domains/{domainId}/verify | Check the CNAME record now and activate the domain |
| DELETE /folders/{id}/domains/{domainId} | Remove a domain |