Skip to content

Authentication

Every /v1 endpoint requires an API key that belongs to a business (workspace). There are no unauthenticated endpoints under /v1, and there are no OAuth flows or session cookies on this surface.

Base URL: https://api.nuansis.co

Send the key as a bearer token:

Authorization: Bearer nvs_live_sk_1a2b3c4d5e6f_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0

X-API-Key is also accepted as an alternative header. If both are present and Authorization carries a Bearer token, Authorization wins; otherwise X-API-Key is used.

X-API-Key: nvs_live_sk_1a2b3c4d5e6f_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0

Keys are matched by their non-secret prefix and sha256(secret). The plaintext key is never stored, which is why it is shown only once when it is created.

nvs_<environment>_sk_<prefix>_<secret>
Part Value
environment live or test
prefix 12 lowercase hex characters, non-secret, used for identification
secret 64 lowercase hex characters, stored only as a SHA-256 hash

A string that does not match this shape is rejected before any database lookup.

A key is minted with a subset of four scopes. A route declares the scopes it needs; the key must hold all of them.

Scope Grants
verification:write Start a verification and submit a code for checking
collections:write Create one-off collections, reserved accounts, and deposits
payouts:write Create transfers, save beneficiaries, and withdraw to a beneficiary
read Read any resource

Two rules make scopes forgiving in one direction only:

  • Write implies read. A key holding any *:write scope satisfies a read requirement.
  • Read does not imply write. A key minted with only read is rejected with 403 on every mutating endpoint.

If a key is minted without an explicit scope list, it is granted all four scopes. If a supplied list contains only unknown values, it also falls back to all four; unknown values mixed with valid ones are dropped.

Endpoint Required scope
GET /v1/capabilities none beyond a valid key
POST /v1/cards none beyond a valid key (returns 501)
POST /v1/verifications verification:write
POST /v1/verifications/:reference/check verification:write
GET /v1/verifications/:reference read
POST /v1/collections collections:write
GET /v1/collections/:id read
POST /v1/reserved-accounts collections:write
GET /v1/reserved-accounts read
GET /v1/reserved-accounts/:id read
POST /v1/deposits collections:write
POST /v1/transfers payouts:write
POST /v1/withdrawals payouts:write
POST /v1/beneficiaries payouts:write
GET /v1/beneficiaries read
GET /v1/balances read
GET /v1/channels read
GET /v1/recipients read
GET /v1/transactions read
GET /v1/banks read
POST /v1/banks/resolve read

A missing key and a key that does not validate both return 401. The distinction is carried in the message:

{
"error": { "code": "unauthorized", "message": "missing_api_key" },
"request_id": "8b1f5a2c-6a4d-4f0e-9f7a-2c3d4e5f6a7b"
}
{
"error": { "code": "unauthorized", "message": "invalid_api_key" },
"request_id": "8b1f5a2c-6a4d-4f0e-9f7a-2c3d4e5f6a7b"
}

invalid_api_key covers a malformed string, an unknown prefix/secret pair, and a revoked key. The API deliberately does not say which.

{
"error": {
"code": "insufficient_scope",
"message": "This API key is missing the required scope(s)",
"details": {
"required": ["payouts:write"],
"granted": ["read"],
"missing": ["payouts:write"]
}
},
"request_id": "8b1f5a2c-6a4d-4f0e-9f7a-2c3d4e5f6a7b"
}

Use details.missing to decide whether to retry with a different key or to fix the key’s permissions.

Keys are independent credentials, so rotation is a two-step deploy rather than an in-place edit:

  1. Create a new key with the same scopes under Settings → API keys.
  2. Deploy it to your environment and confirm traffic is succeeding.
  3. Revoke the old key with DELETE /api-keys/:id (dashboard session auth) or the dashboard’s revoke action.

Revocation sets revoked_at. The authentication lookup filters on revokedAt IS NULL, so a revoked key fails on the very next request — there is no grace period and no cached validation.

last_used_at is updated at most once per minute per key, so it is a useful signal that a key is still in use, not a precise audit trail.

Every response includes X-Request-Id. If you send an X-Request-Id header, the API uses your value verbatim; otherwise it generates a UUID. Successful responses echo the same value as meta.request_id; errors echo it as the top-level request_id.