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_content" \
-H "Content-Type: application/json" -d '{"query":"a sunset"}'
client = niravi.connect("nv_...", workspace="demo_content")
const niravi = connect("nv_...", { workspace: "demo_content" });
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).
hits = client.search("a sunset", workspace_ids=["demo_content", "demo_media"])
for h in hits:
print(h.workspace_id, h.score, h.video_id) # each hit tagged with its workspace
const hits = await niravi.search("a sunset", { workspaceIds: ["demo_content", "demo_media"] });
Discover what a key can reach with GET /api/v1/workspaces (SDK: client.workspaces()):
returns the key’s access list, each with an access field (home / member / demo).
Demos (
demo_*) are read-only — search/recall/chat work, but upload/delete return403. Targeting a workspace not on the key’s list returns403.
Each key also carries scopes:
| Scope | Grants |
|---|---|
read | Search, recall, chat, and all read endpoints |
write | Upload and delete |
Calling a write endpoint with a read-only key returns 403.
The response envelope
Every response is a consistent envelope.
// success
{ "success": true, "data": { /* ... */ }, "meta": { /* request id, timing */ } }
// list (paginated)
{ "success": true, "data": [ /* ... */ ], "pagination": { "limit": 50, "offset": 0, "total": 1280, "has_more": true } }
// error
{ "success": false, "error": { "code": "forbidden", "message": "…", "status": 403 } }
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 = client.videos(all_pages=True) # follows the cursor for you
const videos = await niravi.videos({ allPages: true });
Errors
Failures use standard HTTP status codes; the SDKs raise typed errors:
| Status | Meaning | SDK error |
|---|---|---|
| 401 / 403 | Missing/invalid key, or scope/workspace denied | AuthError |
| 404 | Resource not found | NotFoundError |
| 422 | Invalid request body | ValidationError |
| 429 | Rate limited (see Retry-After) | RateLimitError |
| 5xx | Server error | ServerError |
Rate limits
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers; the SDKs parse them into client.rate_limit / niravi.rateLimit after each call. On 429 the SDKs retry automatically with backoff that honours Retry-After.