# curl and the HTTP API

> The [CLI](https://permahost.io/docs/cli/) and the [MCP tool](https://permahost.io/docs/mcp/) 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.

HTML version: https://permahost.io/docs/api/ · All docs: https://permahost.io/docs.md

Prefer the CLI when possible

**If the agent can install the [CLI](https://permahost.io/docs/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](https://permahost.io/docs/cli/#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](https://permahost.io/docs/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 |
