Skip to content

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

This is the only way to get your first credential, since every other route needs a credential you already hold.

  1. Sign in to dashboard.nirvanalabs.io as an organization owner. Members cannot see or create API keys — the role is denied both read and edit on api_key, so the section will not be available to them.
  2. Open your organization and go to API Keys, then Create.
  3. 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 read or edit.
    • 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.
  4. Copy the key from the dialog that appears after you submit.

Once you hold a key with api_key:edit, you can mint further keys without the dashboard:

Terminal window
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.

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:

Terminal window
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.

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.

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.

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 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:

  1. Create a new key with the same permissions and project scope.
  2. Roll it out everywhere the old one is used.
  3. Confirm the new key is serving traffic.
  4. 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.