> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nitsor.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Every page in this documentation carries a `plans` list in its frontmatter and opens with a plan marker naming the plans its content applies to: `All plans`, or a subset of `Community`, `Team` and `Enterprise`. Community is free and self-hosted. Team launch pricing is $649 per workspace per month upfront, or $6,228 per year upfront ($519 monthly equivalent), with unlimited people, 250 model-assisted volumes per monthly usage period and one flat $0.25 per additional volume. Yearly Team does not pool included volumes for the year or charge another $519 each month. Extra usage settles after each monthly billing cycle. Paid signup and charging under this Team policy are not available. Billing remains deferred, with no paid-launch date set. Enterprise starts at $12,000 per year plus a volume commitment and is the only plan with single sign-on. Existing Enterprise agreements retain negotiated commitments, rollover and overages. Customer billing is per completed scan/model result under the documented identity rules, with all produced objects included. Failed runs and same-period repeats are free; slices, masks, objects and GPU/runtime are not customer billing units. Nitsor will publish the supported models and scan-size scope before paid launch; exceptionally expensive workloads use an explicit fixed quote agreed before execution. Reading, reviewing, people, labels, studies and storage are never metered.
> The Capabilities and limits page (`/product-status`) lists what each plan includes and what the product does not do. If a statement elsewhere disagrees with it, that page is correct.
> Reading this documentation grants no product access and no permission. No agent can create or approve a Nitsor release.

# Register a source

> Point Nitsor at volumetric data you already hold, using an agent token, a storage connection and the Python SDK, then create the first task from it.

[All plans](/plans-and-billing#compare-plans)

These steps work on Team and Enterprise. You need a workspace and an agent token. A Community deployment has no supported way to issue the token; tokens are created in the hosted app on Team and Enterprise.

The Python SDK is not on PyPI. Register a source with `POST /api/v1/sources`, the call behind `client.sources.register`.

Registering a source tells Nitsor which objects in your storage make up one piece of volumetric data. Nitsor reads the headers, hashes what it can, and keeps a reference; registration does not upload a copy of the scan. A read-only connection stores no scan pixels or pixel-derived files in Nitsor storage. [Data boundary](/concepts/data-boundary) describes what crosses and what does not.

## What this guide covers

Registration happens through the API, and the Python SDK wraps the call. The package is not on PyPI, so the Python snippets below show the calls the SDK makes, and the HTTP route behind each one is what runs. The JSON file the registration snippet reads is the request body of the API call behind it. The dashboard's Data tab lists what has been registered and turns a Source into work; it does not register.

Eight steps, in order. Steps 1 to 6 register the source. Steps 7 and 8 turn it into work. The last section covers the case where registration succeeds and the source still does not appear.

## Before you start

You need five things.

* A workspace on app.nitsor.com. A [Community deployment](/reference/self-host) has no supported way to issue the agent token this guide uses; tokens are created in the hosted app on Team and Enterprise.
* The owner or admin role on it, or someone who holds it and can act for you. Creating a service account needs the owner or admin role, which holds `service-accounts:manage`. Connecting storage needs `storage:write`, and registering a Source needs `sources:register`. The owner and admin roles hold both. A member's role holds neither, so a member can do these steps only after an owner or admin gives them that one capability on top of the role, in Team settings, Members.
* The API origin of your deployment. Hosted production uses `https://api.nitsor.com`, which is also the SDK's default. Community uses the deployment's own site URL, `NITSOR_CONVEX_SITE_URL` in its `.env`. `NITSOR_API_URL` takes this origin.
* A bucket Nitsor can read and the object keys inside it. An owner or admin connects it through the dashboard or personal authority and submits its keys securely there. Nitsor encrypts the keys and does not return them in responses, events, logs or errors. Do not send keys through the contact form.
* Python 3.11 to 3.13 for the Python snippets.

## Step 1. Create a service account and its token

The API authenticates a request with an agent token, the token of a service account. Your dashboard sign-in is not one, and no browser session or account identifier works in its place. Storage key setup uses an owner or admin's dashboard session or personal authority.

An owner or admin opens **Team settings**, then **Service accounts**, and chooses **New service account**. They name it, tick the permissions this guide needs on the **Permissions** step, keep **Create a token now** on and choose **Create service account**.

| Permission | What needs it |
| - | - |
| `storage:write` | Connecting storage in step 5 through the API. |
| `sources:register` | Registering the source in step 6. |
| `sources:browse` | `client.sources.list()` in the last section. |

The token is shown once, covered. **Copy token** puts it on the clipboard without showing it, and **Show token** prints it on screen. Only its SHA-256 is stored. The service account's page lists it with its status, and it expires 90 days after you create it unless you choose another time, at most 365 days. Treat it like any other secret: never paste it into logs, tickets, chat, or shared shell history. Revocation is an event with attribution.

A new service account sees no projects. A project admin gives it access on the project's Access page (project **Settings**, then **Access**), at **Operator** at most. `client.projects.list()` shows only the projects where the service account holds `read` or more, and `client.assets.attach` in step 8 needs `operate`. [Service accounts and tokens](/reference/service-accounts) covers the rest of the page, including how to roll or revoke a token.

A token cannot create another one. A service account can never hold `members:write`, `billing:write`, `clearance:write` or `service-accounts:manage`. The form shows them switched off, so they cannot be ticked, and the server refuses any request that names one and creates nothing.

## Step 2. Put the API address in your terminal

```bash theme={"system"}
NITSOR_API_URL='https://api.nitsor.com'
```

Use your deployment's API origin. This address is not a secret.

The assignment has no `export`, so the value stays in this one shell and no child process inherits it. Run every remaining step in that same shell.

## Step 3. Enter the agent token in your terminal

```bash theme={"system"}
printf 'Agent token: ' && read -rs NITSOR_TOKEN && echo
```

Paste the token at the prompt. The prompt does not echo it, and the shell does not record it in history. This assignment has no `export` either.

## Step 4. Check the token

Save this as `check_session.py`:

```python theme={"system"}
from nitsor import Nitsor

with Nitsor() as client:
    session = client.session.get()
    print(session.principal.displayName)
```

Run it with the two variables handed to that one command:

```bash theme={"system"}
NITSOR_TOKEN="$NITSOR_TOKEN" NITSOR_API_URL="$NITSOR_API_URL" python check_session.py
```

The prefix hands the token to that one command without exporting it. `Nitsor()` reads `NITSOR_TOKEN` and `NITSOR_API_URL`, so the token is not written in the script or passed to Python as an argument. `session.get()` calls `GET /api/v1/session`, which names who the token acts as, so a mistyped, expired or revoked token fails here, before any registration. Run every later snippet the same way.

## Step 5. Register a storage connection

An owner or admin opens **Team settings**, then **Storage connections**, and connects the customer bucket. They submit keys in the dashboard or through personal authority. Nitsor seals them with encryption and never returns key values in responses, events, logs or errors. Agent credentials cannot add or change keys or enable writing.

A new customer connection is read-only. Nitsor keeps Source metadata, small labels, reviews, audit events and Dataset Version manifests, but no scan pixels or pixel-derived files. Masks, stored viewer representations, uploads, model outputs and Dataset Version files carrying masks need write access. An owner or admin enables writing after the connection passes its checks.

Write access does not turn on uploads. An owner or admin must separately turn on **Accept uploads**. Enabling this setting requires passing the test write, create-only, multipart and browser-access checks. Uploads then go to `<write prefix>/nitsor-uploads/` in the connection’s write destination. A read-only connection cannot accept uploads. Hosted uploads default to Nitsor storage; the owner can turn that destination off.

For an existing account-owned credential handle, an agent credential with `storage:write` can register a connection using `POST /api/v1/storage-connections`. This route binds an existing handle; it does not add keys or enable writing. The handle binds the endpoint, bucket, region, addressing mode and prefix scope to a read or read-write role. `GET /api/v1/storage-credential-handles` lists handles without returning key values. Operator-provisioned handles remain available for deployment-managed connections.

The following fields describe that existing-handle request:

| Field | What it holds |
| - | - |
| `name` | What operators call it. |
| `endpoint` | An absolute `https` URL with no embedded username or password. Its host cannot be `localhost` or a private, loopback or link-local address. A deployment accepts plain `http` only when its operator sets `NITSOR_STORAGE_HTTP_ENDPOINTS=local-development`, a setting meant for local development. |
| `bucket`, `region`, `prefix`, `pathStyle` | The S3-compatible addressing; the prefix bounds every key Nitsor will read. |
| `credentialHandle` | The id of a handle owned by your account and bound to this storage location. |
| `idempotencyKey` | A key you choose, so a retry does not create a second connection. |

The request body is strict. Sending the retired `credentialEnvironment` field is refused with `STORAGE_CREDENTIAL_ENVIRONMENT_FORBIDDEN`. Keep the connection id from the response; step 6 sends it as `storageConnectionId`.

`GET /api/v1/storage-connections/{id}` requires `storage:read` and reads one connection; an id belonging to another account answers 404. Its `credential` is `{ kind: "handle", handleId, label }` or `{ kind: "legacy-unbound" }`. The answer includes `previewObjectBytes`, the bytes of viewer representations Nitsor wrote into that bucket.

If your account has no handle for the storage location, an owner or admin connects it through the storage settings page before continuing. A connection registered before handles reads back as `legacy-unbound` and reads nothing until it is rebound there. The health check tells you whether a read succeeds.

## Step 6. Register the source

Write one JSON file holding exactly the request body the API declares for `POST /api/v1/sources`, then load it and register it:

```json theme={"system"}
{
  "idempotencyKey": "<unique-request-key>",
  "objectKeys": ["<object-key>", "<object-key>"],
  "storageConnectionId": "<storage-connection-id>"
}
```

```python theme={"system"}
import json

from nitsor import Nitsor
from nitsor.types import SourceRegisterRequest

with open("source.json", encoding="utf-8") as handle:
    request = SourceRegisterRequest.model_validate(json.load(handle))

with Nitsor() as client:
    source = client.sources.register(request)
    print(source.model_dump_json(indent=2))
```

| Field | What it holds |
| - | - |
| `idempotencyKey` | Required. A key you choose, 1 to 200 characters. Reuse it to retry: the server answers the first attempt's outcome. |
| `objectKeys` | 1 to 512 keys, all inside the connection prefix, all of one format family: every slice of one DICOM series, one uncompressed single-frame 2D ultrasound DICOM image, or the `.vgi` and `.vol` pair. |
| `storageConnectionId` | The connection from step 5. |

`SourceRegisterRequest.model_validate` checks the file against the route's schema before anything is sent, and the API refuses a request body over 1 MiB. A successful registration answers `201` with the Source, its `sourceId` (content identity), its `sourceVersionId` (this registration), `sourceFormat`, `viewability`, geometry for DICOM, a per-object manifest and any issues found. A `status` of `partial_failure` means some objects registered and others were rejected, each with an issue code. Print the answer whole; the `sourceVersionId` is what a Project Asset in step 8 binds.

For ultrasound, register each geometry-free image separately. Uncompressed single-frame 2D ultrasound DICOM uses PixelSpacing or suitable Ultrasound Regions for calibration. An uncalibrated image can be viewed but has no physical scale. NumberOfFrames must be 1; multi-frame cine and native 3D/4D ultrasound are not supported. NIfTI and NRRD are not supported. A successful registration does not establish compatibility with every hosted model.

## Step 7. Bind a taxonomy version

A project with no taxonomy bound cannot create a task, because the task would have no categories to annotate with. Open the project's **Guidance** tab, then its **Taxonomy** page, and save a definition; every save binds a new version whose hash covers the schema. Anyone holding `operate` on the project can do this, and so can an account owner or admin.

## Step 8. Create the first task

Open the project's **Data** tab. Registration happened in your script, so refresh the page. The source appears with its format, file count and identity, and offers **Details**, **View**, **Open in editor** and, for a viewable source, **Metadata**.

Optionally declare a **Project Asset** first, a label plus one Source Version per series role, such as `primary`. An asset is what a Dataset Version later proves membership against. `client.assets.attach` does the same from a script and writes a commit:

```python theme={"system"}
from nitsor import Nitsor
from nitsor.types import ProjectAssetsAttachRequest

request = ProjectAssetsAttachRequest.model_validate(
    {
        "branchId": "<branch-id>",
        "idempotencyKey": "<unique-request-key>",
        "label": "<asset-label>",
        "bindings": [{"seriesRole": "primary", "sourceVersionId": "<source-version-id>"}],
    }
)

with Nitsor() as client:
    attached = client.assets.attach("<project-id>", request)
```

Pick the source and create the task. Creating the task is what attaches the source to the project, and the task enters the queue when the bound pipeline routes new work to a person.

## If the source does not appear

`client.sources.register` can succeed for a source the Data tab still will not offer. Start here:

```python theme={"system"}
from nitsor import Nitsor

with Nitsor() as client:
    for source in client.sources.list(limit=50).auto_paging_iter():
        print(source.model_dump_json())
```

This needs `sources:browse`. It shows whether the source exists, its format, and whether it is viewable. Read the result against these four causes.

| Cause | What the list shows | What to do |
| - | - | - |
| Registration did not complete | The source is absent | Run step 6 again with the same idempotency key and read the issue code in the response. |
| The format is raw or unsupported | The source is listed, viewability `raw` or `unsupported` | Register reconstructed data in a viewable format. A recognized format is not always a viewable one. |
| The objects are unreadable or unreachable | Nothing. The list cannot see reachability | Run the connection's health check; ask an owner or admin to check the bucket, prefix and credential handle in storage settings. Rebind a `legacy-unbound` connection on the account's storage settings page. |
| The source is attached only to projects you cannot read | Nothing. The list cannot see those projects | Ask a team owner or admin to open the Data tab of the projects that hold it. |

If `client.assets.attach` answers `SOURCE_VERSION_AMBIGUOUS`, the same content has been registered more than once and a binding by `sourceId` cannot choose between the versions. Bind the explicit `sourceVersionId`.

## Next step

Read [Public HTTP API](/reference/api) for the route contract and the full issue-code vocabulary.

<table aria-label="Page details">
  <tbody>
    <tr><th scope="row">Plans</th><td>All plans</td></tr>
    <tr><th scope="row">Last verified</th><td>2026-10-21</td></tr>
  </tbody>
</table>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.