Getting Started
Authenticate, understand the scope ladder, and script against the API with a personal access token.
Belune has an API behind every screen in the dashboard. A personal access token authenticates as you, scoped to whatever you chose when you created it, for CI, monitoring, and anything else that isn't a person clicking around.
Create one from Account → Personal Access Tokens. The value is shown exactly once, at creation — copy it before closing the dialog. There's no "regenerate"; create a new token and delete the old one once the new one is confirmed working.
Authenticating
Send the token as a bearer credential, in the header only:
curl -H "Authorization: Bearer belune_pat_..." \
https://belune.example.com/api/projectsA personal access token is never accepted via the session cookie the dashboard itself uses. The two are separate authentication paths, not alternatives; a browser session doesn't use a token, and a token can't use the cookie.
A Bearer header also exempts the request from CSRF protection, so a script never fetches or sends an X-CSRF-Token — that dance exists for the cookie-based session only.
Scopes
Every token gets one scope, chosen at creation. The four form a ladder, and each one includes everything narrower than it, so a token only ever needs the rung that matches what it's for, not a combination:
| Scope | Also grants | What it's for |
|---|---|---|
metrics | — | Metrics only. The narrowest option, for a monitoring scraper. |
read | metrics | Viewing projects, applications, and their data. |
deploy | read, metrics | Triggering deploys, restarts, and other runtime actions — without general write access. |
write | deploy, read, metrics | Full access: create, update, and configure, plus everything above. |
A token's reach also follows its owner: it can see and do whatever the owner's role and project access already allow, narrowed further by scope. A Member's read token only reaches the projects that Member can reach — shared or owned.
Project-Pinning
By default a token reaches every project its owner can. Pinning it to one or more projects at creation narrows that further: every project outside the pin returns 403, including ones the owner could otherwise reach.
Create a pinned token from Account → Personal Access Tokens → Create token, choosing one or more projects from the picker instead of leaving it on "All projects." The picker only ever offers projects you can already reach — a Member sees their own and any shared with them — because pinning can only narrow a token below what its owner already has. It can never grant reach the owner doesn't independently have; that boundary is enforced at every request, not just at creation.
The pin set is fixed at creation and cannot be edited afterward. To re-scope a token, create a new one pinned the way you want and delete the old one — the same rotate-don't-edit pattern as changing a token's scope.
Deleting a Pinned Project Narrows the Token, Never Deletes It
Deleting a project the token is pinned to removes that project from the token's reach — it does not delete the token itself. If every project a token was pinned to is deleted, the token is left reaching nothing, not "every project its owner can reach." Pinning only ever narrows; losing every pin narrows a token all the way down to empty, it never widens back out.
Pinning has no representation in the OpenAPI spec — see The Endpoint Reference below — so this section is the only place it's documented; every affected operation's own description just notes that it rejects a project outside a pinned token's pin set.
What a Token Can Never Do
Six things stay behind a live dashboard session no matter what scope a token carries. This is deliberate, as a leaked token should be a bad day, not a catastrophe.
- Delete or restore resources. Every project, application, database, domain, volume, and backup delete or restore, plus orphaned-backup cleanup — rejects a token outright.
- Read a stored secret. Database connection credentials, deploy-hook tokens, webhook secrets, environment variable values, and file mount contents are all decrypted on demand and returned as plaintext — never to a token, even at
writescope. - Manage tokens or users. Minting or revoking a personal access token, and creating, inviting, deleting, changing the role of, or resetting the password or two-factor authentication of any user, all require a session.
- Manage your own account, or enumerate your tokens. Changing your password or profile, logging out, and enrolling, disabling, or checking the status of two-factor authentication and regenerating recovery codes all require a session — a token manages infrastructure, not the account it belongs to. Listing your own tokens joins them: read-only, but the list includes every token's scopes and last-used time, which is a credential inventory a leaked token could use to find the one that holds
write. - Open a terminal. Both starting a container
execsession and the WebSocket tunnel it uses reject a token. - Read or change platform configuration. Listing or updating instance settings, the SMTP configuration, restarting a service, and opening a host shell all require a session. A token should never be able to reach that switch, even though actually opening a shell from there still needs the password, and a second factor if one is enrolled.
About the Exemption
Deleting an unused TLS certificate is an exemption to Delete or restore resources: it takes an Admin's write-scoped token, and a certificate any domain still serves can't be deleted at all.
Notification preferences are an exemption to Manage your own account — they're infrastructure config, the same as everything else a token already manages, not account security. GET /auth/me, which reports the calling token's own identity, is likewise unaffected: it is how a script confirms what it authenticated as.
Reading the server's public IP address is an exemption to Read or change platform configuration, since that's a fact about the box, not configuration of it.
Each of these returns 403 with {"error": "this action requires a session, not a personal access token"} — a different message from a plain scope mismatch, so a script can tell "wrong scope, mint a wider token" apart from "no token reaches this, ever."
Changed Since v0.1.6
Four of the six shipped with v0.1.6, personal access tokens' first release. Two are new, and a script written against v0.1.6 will start getting 403 where it got 200:
- Manage your own account, or enumerate your tokens. v0.1.6 let a token change its own password, manage two-factor authentication, and list its own tokens. Notification preferences are the exception — never gated then, still not now.
- Read or change platform configuration. v0.1.6 let a token read and write instance settings and the SMTP config, restart a service, and open a host shell. Reading the server's public IP is the exception, and still works.
Point either at a session credential instead, or drop the call if it isn't essential.
One boundary moved the other way: deleting an unused TLS certificate no longer needs a session, so a v0.1.6 script that got 403 there now gets 200. A certificate any domain still serves stays undeletable regardless — enforced by a foreign-key constraint rather than the session check.
Limits and Errors
- 100 requests a minute per token, not per user. A token minted for one integration has its own budget, separate from your login session and from any other token on the account. Going over returns
429as plain text, withX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders so a client can back off before hitting it rather than after. - Every other error is
{"error": "<message>"}, with a status code that means what it says:401for a missing, invalid, or expired token,403for a scope or session boundary (see above),404for something that doesn't exist or isn't yours to see. Refer more on HTTP Response Codes.
The Endpoint Reference
Every token-callable endpoint is in the sidebar, grouped by resource. Routes that always require a live dashboard session aren't listed there, see What a Token Can Never Do for that boundary.
The underlying OpenAPI 3.1 spec is served at a stable public URL, outside auth. Point a codegen tool, client, or linter at it directly rather than scraping these pages — and note it is more complete than they are: it carries every registered route, session-only ones included, each with an accurate security block.
Two things are deliberately absent rather than guessed at.
requiredis omitted from every request schema: field optionality here is enforced imperatively, not encoded in the types, and inferring it from validation code would be far fuzzier than the static resolution the rest of the spec rests on.- Project-pinning — a project outside a pinned token's pin set being rejected — has no OpenAPI representation at all, so it is called out in the description of each affected operation instead.
Common Tasks
Every example below assumes BELUNE_TOKEN is a token with at least read scope (deploy for the one that triggers a deploy), and real ids in place of $PROJECT_ID/$APP_ID.
List Projects and Applications
curl -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects | jq '.[] | {id, name, slug}'
curl -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/applications \
| jq '.[] | {id, name, status}'Check Application and Deployment Status
curl -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/applications/$APP_ID \
| jq '{status}'
curl -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/applications/$APP_ID/deployments \
| jq '.[0] | {id, status, started_at, finished_at}'A deployment moves through pending → building → deploying → success (or failed at any step) — see Deployments for what each stage means.
Trigger a Deploy
curl -X POST -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/applications/$APP_ID/deployNeeds deploy scope or wider. Returns 202 with the new deployment record immediately — the build and rollout happen after the response, so poll the deployments endpoint above, or tail logs below, to see it through.
If the only caller is a CI pipeline firing after it publishes an image, a deploy hook is the better fit — one tokenized URL, no personal access token or scope to manage. Reach for a PAT when the script needs to do more than trigger a single deploy.
Tail Logs
curl -N -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/applications/$APP_ID/logsStreams as Server-Sent Events; -N turns off curl's output buffering so lines print as they arrive. For a bounded page of history instead of a live tail, use .../logs/history.
List Backups
curl -H "Authorization: Bearer $BELUNE_TOKEN" \
https://belune.example.com/api/projects/$PROJECT_ID/backups \
| jq '.[] | {id, status, started_at, size_bytes}'Covers every database in the project. read scope reaches this; starting a new backup needs write. Restoring one needs a live session — no scope lets a token do it, per above.
Regenerating API Docs
After add or modify API entrypoints, run this command to generate the docs:
task generate:api-docsIt runs against a disposable database, and nothing it does touches a real install.