---
name: use-nnue-lab-api
description: Use the nnue-lab HTTP API from CLI tools, CI jobs, or AI agents with a personal access token to inspect, compare, upload, update, or delete experiments and download their artifacts.
---

# Use the nnue-lab API

This file is the step-by-step guide for CLI tools and AI agents. The field-level reference for every request and response is the OpenAPI 3.1 document at `/developers/openapi.json`, and the input format of `experiment.json` is published as JSON Schema at `/developers/experiment.schema.json`. Both are served without authentication and generated from the server's validation schemas, so prefer them over this file when a field name or type matters.

```bash
curl --fail-with-body https://nnue-lab.sh11235.com/developers/openapi.json
curl --fail-with-body https://nnue-lab.sh11235.com/developers/experiment.schema.json
```

Use a personal access token (PAT) for non-browser access to nnue-lab. Keep the token secret: it authenticates as the user who created it, and the API applies that user's current tenant membership, role, and tenant-tier capabilities.

## Create a token

1. Sign in to nnue-lab with Google OAuth.
2. Open the account page and find the **API tokens** section.
3. Choose a descriptive name, a `read` or `write` scope, an optional tenant restriction, and an optional expiration.
4. Create the token and copy the `nlab_...` value immediately. The plaintext value is shown only when the token is created.

Choose the narrowest credential that works:

- `read` allows `GET` and `HEAD` requests permitted by the PAT path policy.
- `write` also allows mutations under `/api/tenants/:slug/experiments`. Normal role and tenant-tier checks still apply.
- A tenant-restricted token can access only `/api/tenants/:slug/...` for its bound tenant. It cannot access another tenant or user-wide endpoints.
- An expiration is optional. When supplied, it must be from 1 to 365 days. Expired and revoked tokens are rejected.

## Configure the CLI environment

Set the site origin, tenant slug, and token without committing the token to source control:

```bash
export NNUE_LAB_URL="https://nnue-lab.sh11235.com"
export NNUE_LAB_TENANT="your-tenant-slug"
export NNUE_LAB_TOKEN="nlab_replace_with_your_token"
```

Send the token in every authenticated request:

```bash
curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments"
```

An `Authorization` header with an unsupported scheme, an unknown token, an expired token, a revoked token, or a token owned by a disabled user returns `401`. A valid token that lacks scope or is excluded by the PAT path policy returns `403`.

## Work with experiments

### List experiments

Use `limit` from 1 to 200 and a zero-based `offset`. Supported sort values are `best_loss`, `experiment_date`, `final_test_accuracy`, `name`, and `training_time_sec`; `order` is `asc` or `desc`. Optional filters include `arch`, `bucket`, `q`, `uploaded_by`, `uploaded_by_unset=true`, `visibility=public|private`, and `tag`. The `uploaded_by`, `uploaded_by_unset`, and `visibility` filters apply only when the caller is a member of the tenant; for other callers they are silently ignored. `uploaded_by` matches the uploader's current display name, not their email address — an email value returns `200` with an empty result.

```bash
curl --fail-with-body --get \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  --data-urlencode "sort=experiment_date" \
  --data-urlencode "order=desc" \
  --data-urlencode "limit=50" \
  --data-urlencode "offset=0" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments"
```

### Get an experiment and its history

```bash
EXPERIMENT_ID="replace-with-experiment-id"

curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}"

curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}/history"
```

### Upload experiment.json

Use a `write` token. Send the document as the multipart field named `file`. Validate the file locally against `/developers/experiment.schema.json` before uploading if your trainer is not tatara:

```bash
curl --fail-with-body \
  -X POST \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  -F "file=@experiment.json;type=application/json" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments"
```

A new experiment returns `201`. An upload that matches an existing experiment returns `200`; check `is_duplicate` rather than the status code. `is_duplicate: true` means your upload was not more complete than the stored snapshot and nothing changed. `is_duplicate: false` with `200` means the existing experiment was updated with your more complete snapshot.

### Update experiment metadata

Use a `write` token. Supported fields are `name`, `tags`, `parent_experiment_id`, `memo`, `uploaded_by`, `visibility`, and `allow_public_download`. `uploaded_by` is not a normal updatable field: only a tenant owner can set it, only to an email of a current tenant member, and only while the experiment's existing value is `NULL`. Once set it cannot be changed, and another attempt returns `409`.

```bash
curl --fail-with-body \
  -X PATCH \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"tags":["baseline","halfkp"],"memo":"Uploaded from a training host."}' \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}"
```

### Delete an experiment

Use a `write` token. Deletion is limited to the uploader, a tenant owner, or a system administrator, and tenant capability rules still apply.

```bash
curl --fail-with-body \
  -X DELETE \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}"
```

## Compare experiments and inspect lineage

Compare from 1 to 10 experiment IDs:

```bash
curl --fail-with-body --get \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  --data-urlencode "ids=experiment-id-1,experiment-id-2" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/compare"
```

Fetch a lineage chain by its root experiment ID:

```bash
ROOT_ID="replace-with-root-experiment-id"

curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/lineages/${ROOT_ID}"
```

## List and download artifacts

List NNUE files or engine artifacts attached to an experiment:

```bash
curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}/nnue"

curl --fail-with-body \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}/engines"
```

Download an item returned by those list endpoints. Percent-encode path parameters when constructing URLs programmatically.

```bash
NNUE_FILENAME="network.bin"
ARTIFACT_ID="replace-with-engine-artifact-id"

curl --fail-with-body --output "${NNUE_FILENAME}" \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}/nnue/${NNUE_FILENAME}/download"

curl --fail-with-body --output engine-artifact \
  -H "Authorization: Bearer ${NNUE_LAB_TOKEN}" \
  "${NNUE_LAB_URL}/api/tenants/${NNUE_LAB_TENANT}/experiments/${EXPERIMENT_ID}/engines/${ARTIFACT_ID}/download"
```

NNUE and engine uploads use multipart init, part, complete, and abort endpoints under the same experiment path. They require a `write` token and the corresponding tenant capability; personal tenants cannot upload binary artifacts. Prefer an nnue-lab client that implements this multipart protocol instead of constructing it ad hoc.

## Session-only operations

Do not use a PAT for these operations. They require a browser session cookie and return `403 token_not_allowed` when a PAT is supplied:

- OAuth, login, logout, and other `/api/auth/...` flows
- System administration under `/api/admin/...`
- Invitation acceptance under `/api/invitations/...`
- Token listing, creation, and revocation under `/api/me/tokens/...`
- Tenant member and invitation management
- CSA player link management under `/api/tenants/:slug/experiments/:id/csa-players/...`
- User/account mutations and tenant settings mutations

PAT mutations are allowlisted only below `/api/tenants/:slug/experiments`. The deny rules are evaluated first, so a path under that prefix can still be session-only.

## Operational limits and safety

- Keep at most 20 active tokens per user. Revoke an unused token before creating another when the limit is reached. Revoked and expired tokens do not count as active.
- API traffic is limited to 120 requests per minute per IP, 200 per minute per authenticated user, and 600 per minute per tenant. Experiment and binary upload initialization is additionally limited to 50 requests per tenant per hour. A limited request returns `429` with `Retry-After`.
- Treat the token like a password. Store it in a secret manager, restrict file permissions, avoid command-line arguments that may enter shell history or process listings, and never commit it.
- Use separate tokens for separate machines or jobs, choose `read` unless mutation is necessary, add a tenant restriction whenever possible, and set an expiration for temporary automation.
- Rotate a token immediately if it may have leaked. Sign in, revoke it in the account page, and issue a replacement; token management cannot be performed with the leaked PAT itself.
- Retry transient failures and `429` responses with bounded exponential backoff. Do not retry validation, authorization, or capability errors without changing the request or credential.
