Getting started
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:
Authorization: Bearer heaven1.…Create one over SSH, with any SSH key you've added on the dashboard:
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:
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"]}'{
"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.…"
}
}tokenis shown once. Heaven keeps only its hash. If you lose it, create another and remove the old one.expiresis required. Give days (30d) or a duration (12h), up to 365 days, ornever.- 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 foroverlap:24hby default, up to7d, or0to stop it at once. A key withapi-key rotatecan 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 viewandaccess. A key with ascopeinstead gets every command a scoped key can run. - One Computer.
exec my-siteorexport my-siteallows that command on that Computer only. feedbackandcommand viewwork with every key, whatever it lists. With a scoped key,feedbacknames one of its Computers incomputer.listandshowmean the same asview. A key whose commands namepool listcan runpool 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"]}. Newcommandsorscopereplace the key's,no_scoperemoves its scope,labelrenames it, andexpirescounts from now. The key's next request uses the new settings. Only you can edit keys, signed in on the website or withheaven 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 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,tagor a copy that keeps its source's tags. A key can still read integrations withintegration viewandintegration repo view.
What a key can reach
A scope limits a key to some pools, Computers and tags, whatever its commands allow:
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.createandmovemust 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 readGET /v1/account/?view=commandsand a pool'splans. A read that can't be limited to the scope returnsdeniedrather than everything. - Limits.
pools,computersandtagstake 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 does all of this for you and keeps the key.
-
Start the sign-in
Requestcurl https://heaven.dev/v1/auth/create \ --json '{"method":"device","label":"laptop"}'200{ "version": 1, "data": { "device_code": "…", "user_code": "BCDF-GHJK", "verification_uri": "https://heaven.dev/device", "expires_in": 900, "interval": 5 } }Keep
device_codeto yourself. Showuser_codeandverification_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. -
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. -
Collect the key
Check every
intervalseconds until it's approved:Requestcurl https://heaven.dev/v1/auth/create \ --json '{"method":"device_code","device_code":"DEVICE_CODE"}'200{"version": 1, "data": {"state": "pending"}} {"version": 1, "data": {"state": "approved", "api_key": {"id": "…", "label": "laptop", "expires_at": "…", "token": "heaven1.…"}}}Checking sooner than
intervalreturnsrate_limited. A denied sign-in returnsdenied. A sign-in expires after 15 minutes and then returnsnot_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
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:
ssh heaven.dev ssh-key create --computer NAME --label deployThe 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
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 for secrets a Computer needs.