The OpenAPI document
The OpenAPI document defines request and response schemas. It is generated from the same contract the server and the Python SDK are built from, and the production copy is served anonymously athttps://api.nitsor.com/api/v1/openapi.json. Every other operation needs an agent credential from a workspace. The document describes the contract, not your access.
Community deployments serve the same contract at their own site URL,
NITSOR_CONVEX_SITE_URL in .env, under /api/v1.
The document’s info.version is v1; the served product version is in x-nitsor-product-version and in the capabilities answer. Each operation carries x-nitsor-scope (the permission or rung it needs), x-nitsor-minimum-version and, for lists, x-nitsor-cursor-kind.
Authentication
Every non-anonymous request carriesAuthorization: Bearer <token>, a token of a service account, which the guides call an agent token and the API calls an agent credential (Service accounts and tokens covers creating, replacing and revoking one). A malformed token is rejected before lookup, with the same 401 an unknown token gets. The server stores only the SHA-256 of the token and derives the account and the acting principal from the matching credential record. No request carries an account identifier or caller-declared principal, so a caller cannot claim an identity it cannot prove.
A person signs into the dashboard. Nitsor does not issue personal tokens today, so a person has no credential for the API.
Refused tokens
A refused token always gets401 with the code AUTH_INVALID_TOKEN and retryable: false. When the token matches one Nitsor stored, the answer also carries a reason, a fixed message and remediation for that reason and, beside token_expired, an expiredAt. The words name nothing the caller did not send.
When more than one holds, the first row wins. So
service_account_disabled, the one reason that says wait, is given only when an enable would bring the token back.
Only the holder of the whole token can learn a reason. A token Nitsor never stored, a malformed one and one that is not a service account’s get the same 401 with neither field, and so does a server that predates reasons. Treat reason as an open string: a client that meets one it does not know still has a refusal and the server’s message and remediation. When a token is refused says what an agent does with each. The Python SDK raises AuthRejectedError with reason, expired_at and the server’s remediation, and never retries it.
Discovery and versioning
GET /api/v1/capabilities is anonymous and answers:
A client checks each method it is about to call against these minimums and refuses by name, as
SERVER_VERSION_INCOMPATIBLE, before sending anything. The skew policy is that a client may use a method against any server whose apiVersion equals the method’s and whose productVersion is at or above that method’s floor; a server below one floor still serves every other method; there is no upper bound. Deprecations are announced through the language’s mechanism and kept for at least two stable minors and six months.
Request conventions
Async Requests
Some writes are accepted rather than completed.POST /api/v1/datasets/{id}/versions answers 202 with an operationId. The Request is the durable handle.
A poll honors
Retry-After (5 seconds by default) and sends If-None-Match. The Python SDK’s requests.wait polls with a ten-minute default budget; a wait never cancels on your behalf.
Request path
What happens to one request
Every refusal is one JSON envelope; the Python SDK maps each code to an error class. A decisionId is present only when the denial reached guarded write authorization.
Route inventory
Every operation answers the same error envelope on400, 401, 403, 404, 405, 409, 413, 422 and 429. The scope column is the x-nitsor-scope of each operation: an account permission for account-level routes, a project rung for project-level ones.
Paths above omit the
/api/v1 prefix. The Data model and contracts page describes the records these routes read and write; the OpenAPI document carries every field.
The source representation routes report status (unavailable, pending, failed or ready) and presign a read of a ready viewer representation. See Model label outcomes for outcome categories and finality.
Session and expiry
GET /api/v1/session answers who the credential acts as, which token it is, what it can do and where. It takes limit and cursor to page the reach, like any list. It describes only the token that made the request, never another token of the same service account, and it never returns a token’s text or hash.
accountId, accountName and principal are always present. Every other field is optional on the wire, so a client reads a server that predates it.
In the last 14 days of a token’s life, every authenticated response carries a Nitsor-Token-Expires header. Its value is the instant the token stops working, RFC 3339 in UTC with millisecond precision. Earlier responses carry no header, and a token that has already ended never authenticates. The rule is the same for a service account’s token and a personal token, and the OpenAPI document declares the header. Expiry warnings says what the Python SDK does with it.
Service accounts
A service account is the lasting identity a job runs as, and its tokens are the secrets that prove it. These thirteen routes manage both under the same rules as the dashboard: Service accounts and tokens explains each rule. Every route needs theservice-accounts:manage permission, reads included, on a personal token. Nitsor does not issue personal tokens today, so no call to these routes succeeds yet. A service account’s own token is refused on every one of them with AUTHORIZATION_DENIED (403) naming service-accounts:manage, so a service account cannot manage a service account. Bodies are strict: a body that names an id beside the path’s {id} is refused, and so is an idempotencyKey on any write except the three that mint a secret.
Each write that changes nothing answers
changed: false, such as the same rename again, disabling a disabled service account, revoking a revoked token or shortening to the cutoff the token already has.
Field rules
A service account holds at most 10 live tokens. A replace is allowed at 10, because it ends the replaced token. A token that already has a successor cannot be replaced again, and a successor cannot be replaced while the token it replaced is still live.
The resources
A service account carriesid, subject, name, description, status, accountPermissions, createdAt and createdBy, and once it was disabled or retired disabledAt, disabledBy, disabledReason, retiredAt, retiredBy and retiredReason. owner carries membershipId, active and, once the membership ended, leftAt; name and email appear only for a caller holding members:read. liveTokens carries count, lastUsedAt and soonestExpiresAt, and projectGrants carries count; each says truncated when its read stopped at a ceiling, and then count is a floor.
A token carries id, serviceAccountId, name, status (active, disabled while its service account is disabled, expired or revoked), hint, createdAt, createdBy, expiresAt, lastUsedAt, storageNote, restriction (accountPermissions, absent when the token inherits, and projects) and effectivePermissions, what the service account and the token allow together now. A rolled token adds replacedAt, replacedByTokenId or replacesTokenId, and a revoked one revokedAt, revokedBy and revokedReason. A token never carries its text or its hash.
Secrets are shown once
Three routes mint a secret: create with a first token, add a token and replace a token. Thesecret is on the answer that created the token and on no other. Nitsor keeps only a hash of it and the hint. A retry under the same idempotency key with the same request answers replayed and carries no secret, because the secret was shown on the first answer. The same key with a different request is refused as IDEMPOTENCY_CONFLICT. If you lost the first answer, replace the token.
Refusals
For a caller that holdsservice-accounts:manage, a request that fails the contract’s schema is refused before any rule runs, as INVALID_ARGUMENT (400) with no reason and, when one field is at fault, its fieldPath: a name that is empty or over 100 characters, a description or reason over 500, a storage note over 200, more than 32 projects, an overlap outside 0 to 24 hours or not a whole number, an unknown field, or an expiry in the wrong form. A rule the request passed the schema but broke is the usual error envelope with the code API_ERROR, the registry’s HTTP status and a reason that names the rule. A reused idempotency key keeps its own code: IDEMPOTENCY_CONFLICT (409) with the reason idempotency_conflict.
A service account’s access to a project
Three routes under/api/v1/projects/{id}/service-accounts list, grant, change and remove a service account’s access to one project, the same act as that project’s Access page. Each needs a personal token that holds admin on the project through its own project list and rung cap; no account permission is required, and a service account’s own token never holds admin, so it is refused on all three. A personal token capped at admin lasts 7 days at most. Nitsor does not issue personal tokens today, so no call to these routes succeeds yet.
A drop to
read, and a remove, release the Tasks the service account held in the project, one page per call: while releasedAllHeldTasks is false, send the same request again under a new key. A changed rung also ends any delegated action the service account held there, named in endedDelegatedActions (today only dataset-version:delete); a project admin gives it again if it is still needed. Both writes take the request body’s idempotencyKey: a retry under it answers the first call’s outcome and changes nothing, even after someone else changed the grant since; the same key with a different request is IDEMPOTENCY_CONFLICT (409).
On the two
POST routes, a caller without admin on the project gets AUTHORIZATION_DENIED (403) with missingScope: "admin" and no reason. A decisionId comes with it only when the request reached guarded write authorization, so a refused request with a malformed body carries none. A script that checks for principal_permission_denied must also check for this code.
Decisions, Ledger, and denials
Authorization of a write records both allowed and denied Decisions once the request reaches guarded write authorization; a read check refuses without recording one. An unscoped write whose body is invalid is refused as403 before any Decision is written and carries no decisionId. A denied write that recorded a Decision returns a decisionId, which connects the response to the Decision (verdict, policy, reason code, resource, scope, principal) through GET /api/v1/decisions. A Ledger entry ties each accepted event to its Decision and is listed by GET /api/v1/ledger. Both are attributed to the credential subject that made the request. Neither has a dashboard screen.
Issue-code vocabulary
Every refusal is one JSON envelope:code, message, requestId, retryable, and where they apply fieldPath, remediation, missingScope, decisionId, reason, the rule a domain refusal names beside its code, and expiredAt, the instant a refused token stopped working. Clients can treat the served codes below, plus the client-only DEADLINE_EXCEEDED, as the bounded vocabulary; a newer server may add one, and a client that meets an unknown code treats it as a refusal on the merits.
The Python SDK maps each code to one error class; see Python SDK.
Python SDK
The Python SDK is generated from the same contract. Its authenticated calls work on Team and Enterprise. A Community deployment has no supported way to issue the agent credential it needs; credentials are issued in the hosted app on Team and Enterprise. Its service-account methods need a personal token, so they cannot succeed; see Service accounts. Anonymous discovery needs no credential. The SDK is namednitsor and is not on PyPI. Call the HTTP API directly.
The table describes the SDK as built.
The Python reader streams a pinned version in whole shards, verifies every chunk before it reaches a cache or a tensor, refuses an uneven assignment locally with the numbers and the fix, and checks the per-epoch coverage invariant
yielded + skipped_tombstoned + skipped_holdout == assigned.
Credential profiles
SetNITSOR_TOKEN for a single account; the Quickstart and Register a source journeys both use it. A profile file lets you switch between accounts, or store a token with its base URL, without setting NITSOR_TOKEN in every shell. No command writes it. Create credentials.json by hand in ~/.config/nitsor/, or in the directory that NITSOR_CONFIG_DIR names:
accountId, baseUrl and token are required strings on every profile. defaultProfile names the profile Nitsor() reads when NITSOR_PROFILE is unset; with exactly one profile, defaultProfile can be left out. NITSOR_PROFILE=<name> or profile="<name>" picks a different one. On macOS and Linux, restrict the file to its owner (chmod 600 ~/.config/nitsor/credentials.json). There the SDK prints a warning on stderr, and still reads the file, when other users can read it. On Windows the SDK does not check the file’s permissions, so restrict access to the file yourself.
Next step
Review the Python SDK for the tested client surface or Data model and contracts for the records these routes expose.| Plans | All plans |
|---|---|
| Last verified | 2026-10-21 |