Niravi Docs
npm ↗ Get a key ↗

Concepts

The building blocks shared by every endpoint and SDK.

Authentication

Send your key on every request as the X-API-Key header (or Authorization: Bearer nv_… — both work):

curl https://api.niravi.io/api/v1/videos -H "X-API-Key: nv_..."

Keys are created and revoked in the dashboard → Settings → Developer. They are stored hashed; the plaintext is shown only once at creation.

Workspaces & scopes

Each key has an explicit workspace access list — its home workspace (the default) plus any workspaces you grant it (your other workspaces and/or the read-only demos). You set this when you create the key and edit it anytime under Settings → API access. A key can only ever reach workspaces on its list, and only ones you yourself can access.

Target one workspace — send the X-Workspace-Id header (the SDKs take a workspace option). Omit it to use the home workspace.

curl https://api.niravi.io/api/v1/search \
  -H "X-API-Key: nv_..." -H "X-Workspace-Id: demo_newsroom" \
  -H "Content-Type: application/json" -d '{"query":"a reporter speaking to the camera"}'
import requests  # the Python SDK is not on PyPI yet: plain HTTP works

BASE = "https://api.niravi.io/api/v1"
H = {"X-API-Key": "nv_...", "X-Workspace-Id": "demo_newsroom"}
r = requests.post(f"{BASE}/search", headers=H, json={"query": "a reporter speaking to the camera"})
hits = r.json()["data"]["results"]
const niravi = connect("nv_...", { workspace: "demo_newsroom" });

Search across several at once (federated) — pass workspace_ids to /search. Results from every listed workspace merge into one ranked list, and each hit carries its own workspace_id. Only workspaces on the key’s list are queried (others are silently skipped).

r = requests.post(f"{BASE}/search", headers={"X-API-Key": "nv_..."},
                  json={"query": "a handshake", "workspace_ids": ["demo_newsroom", "demo_enterprise"]})
for h in r.json()["data"]["results"]:
    print(h["workspace_id"], h["score"], h["video_id"])   # each hit tagged with its workspace
const hits = await niravi.search("a handshake", { workspaceIds: ["demo_newsroom", "demo_enterprise"] });

Discover what a key can reach with GET /api/v1/workspaces (SDK: niravi.workspaces()): returns the key’s access list, each with an access field (home / member / demo).

Demos (demo_newsroom, demo_enterprise, demo_creator) are read-only and on every key by default — search/recall/chat work, but upload/delete return 403. Targeting a workspace not on the key’s list returns 403.

Each key also carries scopes:

ScopeGrants
readSearch, recall, chat, and all read endpoints
writeUpload, delete, index options, People edits

Calling a write endpoint with a read-only key returns 403. Deleting needs the workspace that owns the asset: an asset shared into your workspace answers 403.

Assets

The API calls everything a “video”: photos and documents share its IDs and endpoints. kind says which it is (video, image, document); a document has one frame per page (page k at timestamp = k − 1) and a page_count. Every box (objects, faces, text, passages) is x, y, w, h normalized to 0..1 with the origin at the top-left. → Images & documents

Engines

Arrow indexes and searches everything new, and it is the default search engine of every new workspace (and of every workspace that is empty or holds only Arrow assets). The older NiNo pipelines and NiQu (engine: "classic") search are deprecated: they keep working for libraries they indexed. → Indexing & engines, Deprecations

The response envelope

A successful response is an envelope:

// success
{ "success": true, "data": { /* ... */ }, "meta": { "request_id": "…", "rate_limit": { /* the X-RateLimit-* headers */ } } }

// list (paginated)
{ "success": true, "data": [ /* ... */ ], "pagination": { "limit": 50, "offset": 0, "total": 1280, "has_more": true } }

A failure has the same outer shape with "success": false: see Errors. The SDKs unwrap data for you and surface pagination/errors as language-native types.

Pagination

List endpoints (e.g. GET /videos) take limit (max 200) and offset, and return a pagination object with has_more. To page through everything, advance offset by the number of items until has_more is false — or let an SDK do it:

videos, offset = [], 0
while True:
    page = requests.get(f"{BASE}/videos", headers=H, params={"limit": 200, "offset": offset}).json()
    videos += page["data"]
    if not page["pagination"]["has_more"]:
        break
    offset += len(page["data"])
const videos = await niravi.videos({ allPages: true });

Errors

Every /api/v1 failure, whatever the endpoint and whatever went wrong, has one body:

{
  "success": false,
  "error": { "code": "not_found", "message": "Video not found", "status": 404 },
  "meta": { "request_id": "c796118b-81b2-4741-a9ad-12ecc3be646e" },
  "detail": "Video not found"
}

Branch on error.code; show error.message; quote meta.request_id when you write to us. Some codes add a field to error (workspace_id on workspace_forbidden, retry_after on rate_limit_exceeded). detail is a legacy mirror kept for older clients (a string, an object or a list of field errors): read error instead.

Statuserror.code (examples)WhenSDK error
400bad_request, search_errorInvalid request, e.g. "query is required"NiraviError
401unauthorizedMissing or invalid keyAuthError
403forbidden, workspace_forbiddenScope denied ("Missing required scope: write"), or a workspace not on the key’s list (error.workspace_id names it)AuthError
404not_foundUnknown asset, or an unknown path ("No such endpoint: GET /api/v1/nope")NotFoundError
409not_readyThe asset is still processing (e.g. a .nmap export)NiraviError
422invalid_options, unprocessableInvalid request body or optionsValidationError
429rate_limit_exceededRate limited: wait error.retry_after seconds (also the Retry-After header)RateLimitError
502chat_unavailableChat or recall could not compose an answer: retryServerError
5xxServer errorServerError
def niravi_error(r):
    e = r.json()["error"]
    return e["code"], e["message"]

The SDKs raise these as typed errors that carry code, status, requestId / request_id and the original detail.

Rate limits

Limits apply per key and workspace, by the workspace’s plan. A key follows its workspace’s current plan: an upgrade applies to existing keys at once.

PlanRequests / minuteRequests / day
Free10100
Starter, Studio605,000
Team, Team Premium12050,000
Enterprise600500,000

Free also caps processing at 60 minutes of media a month and one upload processing at a time; Enterprise sets both by agreement. GET /api/v1/me shows your key’s plan_tier.

Every v1 response carries the key’s current limits, errors included (once the key has been recognised; a 401 has none). Limits are counted per API server instance, so they are enforced approximately:

HeaderMeaning
X-RateLimit-Limit-MinuteRequests allowed per minute
X-RateLimit-Limit-DayRequests allowed per day
X-RateLimit-Remaining-MinuteRequests left in the current minute
X-RateLimit-ResetUnix time the current minute window resets
X-RateLimit-Limit / X-RateLimit-RemainingAliases of the minute pair, for clients that read the common names

Successful responses repeat them under meta.rate_limit. A 429 adds Retry-After (seconds) and error.retry_after. Both SDKs retry a 429 with backoff that honours Retry-After, and expose the last response’s limits as rateLimit / rate_limit. For higher limits, write to hello@niravi.io.