# Authentication

Source: https://heaven.dev/docs/authentication

Every request carries an API key, a signed token or a sign-in in its Authorization header.

## API keys

Send a key as a bearer token on every request:

Header:

```
Authorization: Bearer heaven1.…
```

Create one over SSH, with any SSH key you've added on the dashboard:

```sh
ssh heaven.dev api-key create --label deploy --expires 30d \
  --command view --command restart --command "'exec my-site'"
```

Or create one with the API, using a credential that can create keys:

Request:

```sh
curl https://heaven.dev/v1/account/modify \
  -H "Authorization: Bearer $HEAVEN_API_KEY" \
  --json '{"action":"api-key create","label":"deploy","expires":"30d","commands":["view","restart","exec my-site"]}'
```

200:

```json
{
  "version": 1,
  "data": {
    "id": "51d8138f-4472-4ec4-b4f8-1ec1d2ba65a2",
    "label": "deploy",
    "cmds": ["view", "restart", "exec my-site"],
    "created_at": "2026-11-01T23:18:35Z",
    "expires_at": "2026-12-01T23:18:35Z",
    "last_used_at": null,
    "revoked_at": null,
    "token": "heaven1.…"
  }
}
```

- **`token` is shown once.** Heaven keeps only its hash. If you lose it, create another and remove the old one.
- **`expires` is required.** Give days (`30d`) or a duration (`12h`), up to 365 days, or `never`.
- **Rotating a key** gives it a new secret, shown once, and keeps its ID, commands, scope and expiry: `{"action":"api-key rotate","key":"KEY_ID","overlap":"1h"}`. The old secret keeps working for `overlap`: `24h` by default, up to `7d`, or `0` to stop it at once. A key with `api-key rotate` can rotate itself.
- **Removing a key** ends it at once: `{"action":"api-key remove","key":"KEY_ID"}`.

## What a key can run

A key can run only the commands its `commands` list names. Each endpoint's reference gives the command for every read and action, such as `create`, `restart`, `pool view` or `ssh-key add`.

- **Without `commands`,** a key gets a read-only set: `account view`, `view`, `pool view`, `pool usage`, `operation view`, `usage`, `snapshot view`, `ssh-key view` and `access`. A key with a [`scope`](https://heaven.dev/docs/authentication#scope) instead gets every command a scoped key can run.
- **One Computer.** `exec my-site` or `export my-site` allows that command on that Computer only.
- **`feedback` and `command view`** work with every key, whatever it lists. With a scoped key, `feedback` names one of its Computers in `computer`.
- `list` and `show` mean the same as `view`. A key whose commands name `pool list` can run `pool view`.
- You can hold up to 1,000 keys, counting sign-in keys and MCP approvals.
- `GET /v1/account/?view=commands` (`command view`) lists every command a key can name, with its endpoint, fields, flags and usage. A program can build any command's request from it.
- **Editing a key** changes it in place and keeps its ID and secret: `{"action":"api-key edit","key":"KEY_ID","commands":["view","restart"]}`. New `commands` or `scope` replace the key's, `no_scope` removes its scope, `label` renames it, and `expires` counts from now. The key's next request uses the new settings. Only you can edit keys, signed in on the website or with `heaven login`. API keys you create can't.

> **Some commands can't be given to API keys you create.** These commands give out access or spend money, so they could get around a key's limits. A key is refused them even if its commands name them. Use a [sign-in key](https://heaven.dev/docs/authentication#device-sign-in) or SSH instead.

- **Access:** adding SSH keys, sharing a Computer or making its website public, share links, public ports, transfers and support access.
- **Money:** billing, buying or releasing capacity, and buying, attaching or releasing static addresses.
- **Your account:** deleting it, setting or clearing defaults, and accepting or declining invitations.
- **Pools:** creating or changing pools (pool snapshots excepted), and sharing pools or pool snapshots.
- **Organizations and integrations:** every organization command, changing integrations, and creating a Computer with integrations or GitHub. So is adding a tag that an integration is attached to, whether by `create`, `tag` or a copy that keeps its source's tags. A key can still read integrations with `integration view` and `integration repo view`.

## What a key can reach

A `scope` limits a key to some pools, Computers and tags, whatever its commands allow:

Request:

```sh
curl https://heaven.dev/v1/account/modify \
  -H "Authorization: Bearer $HEAVEN_API_KEY" \
  --json '{"action":"api-key create","label":"web-deploy","expires":"30d","scope":{"pools":["web"]}}'
```

- **In scope** is a Computer the scope names, or one that has a tag or is in a pool the scope names. Heaven checks this on every request, so a Computer created in the pool is in scope, and one moved out of it isn't. The key can read every pool snapshot of a pool in its scope.
- **Out of scope,** a Computer or pool returns `not_found`, and lists show only what's in scope. `create` and `move` must name one of the scope's pools.
- **No account commands.** A scoped key can't run account, organization or integration commands (`denied`), and can't create keys. It can still read `GET /v1/account/?view=commands` and a pool's `plans`. A read that can't be limited to the scope returns `denied` rather than everything.
- **Limits.** `pools`, `computers` and `tags` take up to 64 each. Pools must be in your personal account. The scope stores IDs, so renaming something doesn't change it.

## Sign in from a program

A program, a script or an agent can sign in with a short code that you approve in your browser. Nothing needs copying back, so it works anywhere, even inside a Computer. The key it gets acts as you for 30 days. [`heaven login`](https://heaven.dev/docs/cli#login) does all of this for you and keeps the key.

1. ### Start the sign-in

   Request:

   ```sh
   curl https://heaven.dev/v1/auth/create \
     --json '{"method":"device","label":"laptop"}'
   ```

   200:

   ```json
   {
     "version": 1,
     "data": {
       "device_code": "…",
       "user_code": "BCDF-GHJK",
       "verification_uri": "https://heaven.dev/device",
       "expires_in": 900,
       "interval": 5
     }
   }
   ```

   Keep `device_code` to yourself. Show `user_code` and `verification_uri`. To offer the machine's SSH key as well, add `"ssh_public_key"`. The approval page adds it to your account if you leave its box ticked.

2. ### Approve it in the browser

   Open `https://heaven.dev/device`, sign in if needed, type the code and approve. Deny a code you didn't start.

3. ### Collect the key

   Check every `interval` seconds until it's approved:

   Request:

   ```sh
   curl https://heaven.dev/v1/auth/create \
     --json '{"method":"device_code","device_code":"DEVICE_CODE"}'
   ```

   200:

   ```json
   {"version": 1, "data": {"state": "pending"}}

   {"version": 1, "data": {"state": "approved", "api_key": {"id": "…", "label": "laptop", "expires_at": "…", "token": "heaven1.…"}}}
   ```

   Checking sooner than `interval` returns `rate_limited`. A denied sign-in returns `denied`. A sign-in expires after 15 minutes and then returns `not_found`, so start again.

A sign-in key can run every command you can, including those that API keys you create can't. It's listed with your API keys under its label, and `api-key remove` ends it.

## Signed SSH tokens

If your account has an SSH key, you don't need a stored API key at all. Sign a short-lived `heaven0` token on your own machine instead. Removing the SSH key ends every token it signed.

**Sign a token with OpenSSH**

```sh
b64url() { tr -d '\n=' | tr '+/' '-_'; }
PAYLOAD=$(python3 -c 'import json,time; print(json.dumps({"exp":int(time.time())+3600},separators=(",",":")),end="")')
PAYLOAD64=$(printf '%s' "$PAYLOAD" | base64 | b64url)
SIGNATURE=$(printf '%s' "$PAYLOAD" | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n v0@heaven.dev)
SIG64=$(printf '%s\n' "$SIGNATURE" | sed '1d;$d' | b64url)
TOKEN="heaven0.$PAYLOAD64.$SIG64"
curl https://heaven.dev/v1/account/ -H "Authorization: Bearer $TOKEN"
```

The payload needs `exp`, in Unix seconds. To limit the token as an API key's `commands` do, add `"cmds":[…]`. Without it, the token gets the read-only set. An SSH key limited to a tag or to one Computer can't sign these tokens.

## Tokens for a private site

A program such as Git, curl or a deploy job opens a private site, `https://NAME.heaven.dev`, with a token for that Computer. This command makes one that lasts until you remove it:

```sh
ssh heaven.dev ssh-key create --computer NAME --label deploy
```

The token is shown once. Heaven signs it with a new SSH key limited to that Computer, lists the key in `ssh-key view`, and keeps no copy of the token. `ssh-key remove` revokes it.

You can also sign a token yourself, with the same recipe as above and the namespace `v0@NAME.heaven.dev`. A token opens what your account can open there: the main address with web access, and every port with ownership or root access. Send it as `X-Heaven-Authorization: Bearer TOKEN`, or as the password for HTTPS Basic authentication, which Git uses. The username is ignored.

**Sign a site token with your own key**

```sh
b64url() { tr -d '\n=' | tr '+/' '-_'; }
PAYLOAD=$(python3 -c 'import json,time; print(json.dumps({"exp":int(time.time())+3600},separators=(",",":")),end="")')
PAYLOAD64=$(printf '%s' "$PAYLOAD" | base64 | b64url)
SIGNATURE=$(printf '%s' "$PAYLOAD" | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n v0@NAME.heaven.dev)
SIG64=$(printf '%s\n' "$SIGNATURE" | sed '1d;$d' | b64url)
TOKEN="heaven0.$PAYLOAD64.$SIG64"
curl https://NAME.heaven.dev/ -H "X-Heaven-Authorization: Bearer $TOKEN"
```

An SSH key limited to one Computer (`ssh-key add --computer`) signs tokens for that Computer only. A key limited to a tag can't sign them. Renaming the Computer ends its tokens.

## Signing out

`POST /v1/auth/delete` ends a website sign-in and returns `{"logged_out": true}`. To end an API key, including a sign-in key, use `api-key remove`. `heaven logout` removes the program's own key.

## Keeping keys safe

- Keys are passwords. Keep them out of URLs, logs, shell history and code.
- Give each program its own key with only the commands it needs, and the shortest expiry that works.
- Never put a key inside a Computer you share. Use an [integration](https://heaven.dev/docs/integrations) for secrets a Computer needs.
