Authentication
Every endpoint on https://api.nirvanalabs.io requires authentication. There is
no anonymous or public read path — an unauthenticated request returns 401
before any handler runs.
The public API accepts exactly one kind of credential you can create and use yourself: an organization API key, sent as a bearer token.
| Credential | Organization API key |
| Header | Authorization: Bearer <your-api-key> |
| Format | Opaque string — 32 random bytes, base64url-encoded, about 43 characters. No prefix, no structure, nothing to parse. |
| Issued by | The dashboard, or POST /v1/api_keys with an existing credential |
| Lifetime | Fixed at creation. Required, and at most one year. |
| Scoping | Per resource type (read or edit) and per project |
Obtain an API Key
Section titled “Obtain an API Key”From the Dashboard
Section titled “From the Dashboard”This is the only way to get your first credential, since every other route needs a credential you already hold.
- Sign in to dashboard.nirvanalabs.io as an
organization owner. Members cannot see or create API keys — the role is
denied both
readandeditonapi_key, so the section will not be available to them. - Open your organization and go to API Keys, then Create.
- Fill in the form:
- Name — required, free text.
- Projects — at least one. The key can only touch resources in the projects you select here.
- Permissions — at least one. A matrix of resource type against
readoredit. - Validity — an expiry date is required; a start date is optional. The picker offers 7, 30, 90 and 365-day presets.
- IP restrictions — optional allow and block lists.
- Copy the key from the dialog that appears after you submit.
From the API
Section titled “From the API”Once you hold a key with api_key:edit, you can mint further keys without the
dashboard:
curl -sS -X POST https://api.nirvanalabs.io/v1/api_keys \ -H "Authorization: Bearer $NIRVANA_LABS_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "ci-deploy", "expires_at": "2027-01-01T00:00:00Z", "project_ids": ["123e4567-e89b-12d3-a456-426614174000"], "permissions": [ { "resource_type": "vm", "permission": "edit" }, { "resource_type": "vpc", "permission": "read" } ] }'The 201 response is the only place the new key field ever appears.
A key can never mint a more powerful key than itself. Every requested
permission must be within the calling key’s own permissions, and every project
must be within its project scope; otherwise the call fails with 403 and a
PERMISSION_ESCALATION or PROJECT_SCOPE_ESCALATION type. Keys created from a
dashboard session are not subject to that ceiling, because only owners can
manage keys and owners are unrestricted.
Make an Authenticated Call
Section titled “Make an Authenticated Call”Export the key and send it as a bearer token. GET /v1/regions takes no
required parameters and — unlike almost every other endpoint — needs no resource
permission, so it answers “did this key authenticate?” without also testing what
the key is allowed to do:
export NIRVANA_LABS_API_KEY="REPLACE_WITH_YOUR_API_KEY"
curl -sS https://api.nirvanalabs.io/v1/regions \ -H "Authorization: Bearer $NIRVANA_LABS_API_KEY"Any valid key returns 200 and a JSON list of regions, whatever its scope. Any
of the 401 bodies below means the credential did not reach the API intact —
see When a Call Returns 401.
Pick this endpoint rather than a resource one for a first test. Most endpoints
check permissions before doing anything, so they conflate two different
failures: GET /v1/projects requires project:read, and a perfectly good key
scoped only to vm:edit returns 403 AUTHORIZATION_ERROR there. If you do get
a 403, read it as confirmation that authentication succeeded — the key was
recognised, and only its scope is in question.
The header is parsed as exactly two whitespace-separated fields. The scheme is
matched case-insensitively, so bearer works as well as Bearer; anything else
— a missing scheme, an extra field, Token instead of Bearer — is rejected
before the key is even looked up.
The SDKs, the CLI and the Terraform provider all read the same key from
NIRVANA_LABS_API_KEY and build the same header for you:
import NirvanaLabs from '@nirvana-labs/nirvana';
// apiKey defaults to process.env['NIRVANA_LABS_API_KEY'].const client = new NirvanaLabs();import ( "github.com/nirvana-labs/nirvana-go" "github.com/nirvana-labs/nirvana-go/option")
// WithAPIKey defaults to os.LookupEnv("NIRVANA_LABS_API_KEY").client := nirvana.NewClient(option.WithAPIKey("My API Key"))See Deployment Environment Variables for the full set of variables each tool honours, and SDKs & Tools for complete request examples.
When a Call Returns 401
Section titled “When a Call Returns 401”Every 401 body has the same shape. This is a real response from
https://api.nirvanalabs.io, with no Authorization header:
{ "type": "AUTHORIZATION_HEADER_REQUIRED_ERROR", "message": "authorization header required", "request_id": "9b1f0457-95f7-496c-a9af-a2f29eab2373", "timestamp": "2026-08-26T15:20:14.357541062Z", "context": { "parameters": {} }}Match on type, not on message. There are three:
type |
What it means | What to do |
|---|---|---|
AUTHORIZATION_HEADER_REQUIRED_ERROR |
No Authorization header at all. |
Send the header. If you believe you did, check that your client is not dropping it across a redirect. |
AUTHORIZATION_FORMAT_INVALID_ERROR |
The header is present but is not two fields with a Bearer scheme. Message is invalid authorization format. Use 'Bearer {token}'. |
Send Authorization: Bearer <key>. An unexpanded shell variable produces a bare Bearer with nothing after it, which lands here. |
INVALID_TOKEN_ERROR |
A bearer token was supplied and rejected. Message is invalid token. |
See below — this one is ambiguous by design. |
INVALID_TOKEN_ERROR covers every way a token can be wrong: a mistyped key, a
key from another environment, a deleted key, an expired key, a key whose start
date has not arrived, and a truncated key that is no longer a plausible length.
The API tries the credential as an API key first and then as a dashboard
session token, and it is the second failure that reaches you — so the response
deliberately does not distinguish “expired” from “never existed”.
When you get it, check the key in the dashboard rather than guessing. Its status is the fastest answer:
active— valid now. Look for a copy/paste error, a trailing newline, or the wrong environment.inactive— the start date is in the future.expired— past the expiry date. Create a new key.
If the key is missing from the list entirely, it was deleted.
A 403 Is Not a 401
Section titled “A 403 Is Not a 401”If the credential is recognised but not permitted, the API returns 403, not
401. Retrying with a different header will not help — the key itself needs
changing.
type |
Cause |
|---|---|
AUTHORIZATION_ERROR |
The key lacks the permission for that resource type, or the resource is in a project outside the key’s scope. |
IP_RESTRICTED_ERROR |
The request’s source IP is blocked by, or absent from, the key’s IP rules. |
PERMISSION_ESCALATION |
A key tried to create or update another key with permissions beyond its own. |
PROJECT_SCOPE_ESCALATION |
A key tried to grant access to a project outside its own scope. |
A key scoped to the wrong project is the most common cause. Because the project
list is chosen at creation time, a key that works against one project returns
403 against another with no hint that scoping is the reason.
Scopes
Section titled “Scopes”Access is the intersection of two independent dimensions. A call succeeds only if the key satisfies both.
Resource permissions. One level per resource type, either read or edit.
edit includes read. At least one is required, and the available resource
types are:
vm, vpc, volume, connect_connection, rpc_node_dedicated,
rpc_node_flex, nks_cluster, nks_node_pool, project, api_key,
organization, audit_log, usage.
Project scope. A list of project IDs, at least one. The key cannot see or
touch resources in any other project. Most list endpoints take project_id as
a required query parameter, so a key scoped to a single project still has to
name it on each call.
Both dimensions can be changed after creation with
PATCH /v1/api_keys/{api_key_id}. Sending permissions or project_ids
replaces the whole set rather than adding to it.
Expiry and Rotation
Section titled “Expiry and Rotation”Expiry is not optional. expires_at is required at creation, must be in the
future, and must be within one year of the start date — or of the creation date
when no start date is given. starts_at is optional; when set it must be today
or later, and before expires_at. A key sits at status inactive until its
start date, active between the two dates, and expired afterwards.
There is no grace period and no renewal. At expires_at, calls begin failing
with INVALID_TOKEN_ERROR, and nothing about a key’s validity window can be
edited afterwards.
There is also no rotation endpoint, and no way to change the secret behind an existing key. Rotating means replacing:
- Create a new key with the same permissions and project scope.
- Roll it out everywhere the old one is used.
- Confirm the new key is serving traffic.
- Delete the old key with
DELETE /v1/api_keys/{api_key_id}.
Because expiry is capped at a year, every key in use will need this at least
annually. Set a reminder ahead of expires_at — the first signal otherwise is
production traffic returning 401.