# HTTP API

JSON over HTTPS under `/api/v1`. Artifacts and versions are query parameters, so an artifact's slashes need no escaping. Paths are fixed words. The types are the Effect schemas in `packages/artifacts-api`, shared by the app, the command line and the [o41-checkpoint](https://artifacts.041.io/docs/o41-checkpoint.md) crate.

## Authentication

Send `Authorization: Bearer <credential>`. Three kinds work:

- An **organization API key**, for boxes and scripts. It acts as its organization.
- An app **session token**. It acts in the session's active organization.
- A **CLI OAuth token** from `o41-artifacts login`, which names its organization in the `x-o41-organization` header; membership is checked.

## Errors

Every error is JSON with a stable `error` code and a `message` for people:

```json
{
  "error": "not_ready",
  "message": "The organization has no usable location for this write; add a fallback location."
}
```

| Code | Status | When |
|---|---|---|
| `bad_request` | 400 | A body or parameter that does not decode, or a location that fails its probe |
| `unauthorized` | 401 | No credential, or one that does not verify |
| `forbidden` | 403 | An API key where a person is needed, or an organization you are not in |
| `not_found` | 404 | No such version, note, connection, location or key |
| `not_empty` | 409 | `new` or `fork` on an artifact with versions |
| `superseded` | 409 | Another writer opened the artifact; the body carries the current `epoch` |
| `not_after_latest` | 409 | A version named at or below the latest |
| `not_ready` | 409 | No location can take the write: add a fallback |
| `conflict` | 409 | Retiring the fallback, and similar state clashes |
| `upstream` | 502 | A cloud or bucket call failed |
| `internal` | 500 | A bug on our side |

## Writers and versions

What a training run calls. `open` once before the first step, then `begin`, the PUTs and `commit` for every version.

### `POST /artifacts/open`

```text
POST /api/v1/artifacts/open?artifact=experiments/hoonshot/1234
```

Open the writer: from the latest (`latest`), from nothing (`new`), or as a fork of `source`, a full path. Takes the artifact over with a new epoch and returns the version continued from, signed.

Request:

```json
{
  "mode": "latest",
  "place": {
    "cloud": "gcp",
    "region": "europe-west4",
    "source": "skypilot"
  }
}
```

Response, 200:

```json
{
  "epoch": 2,
  "version": {
    "artifact": "experiments/hoonshot/1234",
    "version": "step-00000500",
    "committed": "2026-10-06T11:42:10.000Z",
    "location": {
      "id": "l7f3k2q9d0a",
      "cloud": "aws",
      "region": "us-east-1"
    },
    "transfer": "cross-cloud",
    "bytes": 8380239515,
    "meta": {
      "step": 500
    },
    "source": null,
    "expires": "2026-10-06T13:00:00.000Z",
    "files": [
      {
        "name": "params.safetensors",
        "size": 2793406464,
        "url": "https://o41art-7f3k2q9d0a1b.s3.us-east-1.amazonaws.com/experiments/hoonshot/1234/step-00000500/params.safetensors?X-Amz-…"
      },
      {
        "name": "state.json",
        "size": 912,
        "url": "https://o41art-7f3k2q9d0a1b.s3.us-east-1.amazonaws.com/…/state.json?X-Amz-…"
      }
    ]
  },
  "source": null
}
```

`409 not_empty` for `new` or `fork` on an artifact with versions.

### `POST /versions/begin`

```text
POST /api/v1/versions/begin?artifact=experiments/hoonshot/1234&version=step-00000510
```

Begin writing a version above the latest: places it, starts the uploads, signs one URL per part. `exclude` lists locations to skip after uploads to them failed.

Request:

```json
{
  "epoch": 2,
  "place": {
    "cloud": "gcp",
    "region": "europe-west4",
    "source": "skypilot"
  },
  "files": [
    {
      "name": "params.safetensors",
      "size": 2793406464
    },
    {
      "name": "state.json",
      "size": 912
    }
  ],
  "meta": {
    "step": 510
  }
}
```

Response, 201:

```json
{
  "upload": "u9Kq2mX…",
  "placement": {
    "location": {
      "id": "l2b8x0c1e5f",
      "cloud": "gcp",
      "region": "europe-west4"
    },
    "match": "region"
  },
  "warning": null,
  "expires": "2026-10-06T13:02:00.000Z",
  "files": [
    {
      "name": "params.safetensors",
      "size": 2793406464,
      "partSize": 33554432,
      "parts": [
        {
          "url": "https://storage.googleapis.com/o41art-2b8x0c1/…/params.safetensors?partNumber=1&uploadId=…&X-Amz-…",
          "headers": {}
        },
        {
          "url": "…",
          "headers": {}
        }
      ]
    },
    {
      "name": "state.json",
      "size": 912,
      "partSize": 912,
      "parts": [
        {
          "url": "https://storage.googleapis.com/o41art-2b8x0c1/…/state.json?X-Amz-…",
          "headers": {}
        }
      ]
    }
  ]
}
```

PUT each part's bytes to its `url` with its `headers` and keep the `ETag` each PUT returns. Every part but the last is `partSize` bytes. `warning` is set when the write fell back.

### `POST /versions/resign`

```text
POST /api/v1/versions/resign?artifact=experiments/hoonshot/1234&upload=u9Kq2mX…
```

Fresh URLs for a pending upload: same upload id, same parts. URLs live 60 minutes; ask again on a 403 and continue with the parts already sent.

Request:

```json
{
  "epoch": 2
}
```

Response, 200:

```json
{
  "upload": "u9Kq2mX…",
  "placement": {
    "location": {
      "id": "l2b8x0c1e5f",
      "cloud": "gcp",
      "region": "europe-west4"
    },
    "match": "region"
  },
  "warning": null,
  "expires": "2026-10-06T14:02:00.000Z",
  "files": [
    {
      "name": "params.safetensors",
      "size": 2793406464,
      "partSize": 33554432,
      "parts": [
        {
          "url": "https://storage.googleapis.com/o41art-2b8x0c1/…/params.safetensors?partNumber=1&uploadId=…&X-Amz-…",
          "headers": {}
        },
        {
          "url": "…",
          "headers": {}
        }
      ]
    },
    {
      "name": "state.json",
      "size": 912,
      "partSize": 912,
      "parts": [
        {
          "url": "https://storage.googleapis.com/o41art-2b8x0c1/…/state.json?X-Amz-…",
          "headers": {}
        }
      ]
    }
  ]
}
```

### `POST /versions/commit`

```text
POST /api/v1/versions/commit?artifact=experiments/hoonshot/1234&upload=u9Kq2mX…
```

Complete the uploads, check sizes, write `_manifest.json`, commit. Only now can the version be the latest.

Request:

```json
{
  "epoch": 2,
  "files": [
    {
      "name": "params.safetensors",
      "etags": [
        "\"a1…\"",
        "\"b2…\"",
        "…"
      ]
    },
    {
      "name": "state.json",
      "etags": [
        "\"c3a1…\""
      ]
    }
  ]
}
```

Response, 200:

```json
{
  "version": "step-00000510",
  "committed": "2026-10-06T12:03:41.000Z"
}
```

`409 superseded` when another writer opened the artifact:

```json
{
  "error": "superseded",
  "message": "Another writer opened this artifact; this one must stop.",
  "epoch": 3
}
```

`409 not_after_latest` when a version above this one was committed first.

### `GET /versions/read`

```text
GET /api/v1/versions/read?artifact=experiments/hoonshot/1234&version=latest&urls=1&cloud=gcp&region=europe-west4&source=env
```

One version; `version` may be `latest`. `urls=1` signs its files. The reader's `cloud`, `region` and `source` are optional and set `transfer` and the usage counts.

Response, 200:

```json
{
  "artifact": "experiments/hoonshot/1234",
  "version": "step-00000500",
  "committed": "2026-10-06T11:42:10.000Z",
  "location": {
    "id": "l7f3k2q9d0a",
    "cloud": "aws",
    "region": "us-east-1"
  },
  "transfer": "cross-cloud",
  "bytes": 8380239515,
  "meta": {
    "step": 500
  },
  "source": null,
  "expires": "2026-10-06T13:00:00.000Z",
  "files": [
    {
      "name": "params.safetensors",
      "size": 2793406464,
      "url": "https://o41art-7f3k2q9d0a1b.s3.us-east-1.amazonaws.com/experiments/hoonshot/1234/step-00000500/params.safetensors?X-Amz-…"
    },
    {
      "name": "state.json",
      "size": 912,
      "url": "https://o41art-7f3k2q9d0a1b.s3.us-east-1.amazonaws.com/…/state.json?X-Amz-…"
    }
  ]
}
```

`404 not_found` when there is no such version.

### `GET /versions/list`

```text
GET /api/v1/versions/list?artifact=experiments/hoonshot/1234
```

Committed versions in name order, a page of up to 200 versions and 5,000 files; pass `next` as `after` for the next page. `urls=1` signs the page's newest versions' files, up to 100 URLs; the rest come with `url: null`, and `versions/read` signs any one version.

Response, 200:

```json
{
  "versions": [
    {
      "artifact": "experiments/hoonshot/1234",
      "version": "step-00000500",
      "committed": "2026-10-06T11:42:10.000Z",
      "location": {
        "id": "l7f3k2q9d0a",
        "cloud": "aws",
        "region": "us-east-1"
      },
      "transfer": "unknown",
      "bytes": 8380239515,
      "meta": {
        "step": 500
      },
      "source": null,
      "expires": null,
      "files": [
        {
          "name": "params.safetensors",
          "size": 2793406464,
          "url": null
        },
        {
          "name": "state.json",
          "size": 912,
          "url": null
        }
      ]
    }
  ],
  "next": null
}
```

### `GET /artifacts/list`

```text
GET /api/v1/artifacts/list?under=experiments/hoonshot/
```

Artifacts whose path starts with `under`, from the organization's index, a page of 1,000 in name order; pass `next` as `after` for the next page. May lag a commit by a moment; the latest itself never does.

Response, 200:

```json
{
  "artifacts": [
    {
      "artifact": "experiments/hoonshot/1234",
      "versions": 52,
      "bytes": 435772454780,
      "latest": "step-00000510",
      "lastCommit": "2026-10-06T12:03:41.000Z",
      "source": null
    }
  ],
  "next": null
}
```

### `GET /artifacts/note`

```text
GET /api/v1/artifacts/note?artifact=experiments/hoonshot/1234&note=evaluated.json
```

A note's bytes, `application/octet-stream`. `PUT` the same URL with the bytes as the body to replace it (204).

A note name is 1 to 128 letters, digits, `.`, `_` or `-`; a note is at most 64 KiB.

## Organization setup

What the app's pages call. Creating, completing and deleting need a signed-in person, not an API key.

### `GET /org`

```text
GET /api/v1/org
```

The organization: its connections, locations, issuer, and whether it is `ready` to take writes, which needs an active fallback.

Response, 200:

```json
{
  "orgId": "org_2x…",
  "name": "Hoonshot Labs",
  "issuer": "https://artifacts.041.io",
  "connections": [
    {
      "id": "c4d1e9a0b2c",
      "cloud": "aws",
      "name": "research account",
      "state": "active",
      "subject": "org_2x…:c4d1e9a0b2c",
      "issuer": "https://artifacts.041.io",
      "audience": "o41-artifacts",
      "identifiers": {
        "cloud": "aws",
        "aws": {
          "roleArn": "arn:aws:iam::123456789012:role/o41-artifacts-4d1e9a0b2c"
        }
      },
      "error": null,
      "setup": "# 041 Artifacts: connect an AWS account. Run with the AWS CLI as an IAM administrator.\n…\necho \"roleArn=arn:aws:iam::$ACCOUNT_ID:role/o41-artifacts-4d1e9a0b2c\"",
      "created": "2026-10-06T09:00:00.000Z"
    }
  ],
  "locations": [
    {
      "id": "l7f3k2q9d0a",
      "name": "aws us-east-1",
      "kind": "created",
      "cloud": "aws",
      "region": "us-east-1",
      "protocol": "s3",
      "endpoint": "https://s3.us-east-1.amazonaws.com",
      "bucket": "o41art-7f3k2q9d0a1b",
      "path": "",
      "connection": "c4d1e9a0b2c",
      "fallback": false,
      "default": true,
      "state": "active",
      "created": "2026-10-06T09:05:00.000Z"
    }
  ],
  "ready": true
}
```

### `POST /connections`

```text
POST /api/v1/connections
```

Start connecting a cloud. The response's `setup` is the script to run in your account, with the subject filled in.

Request:

```json
{
  "cloud": "aws",
  "name": "research account"
}
```

Response, 201:

```json
{
  "id": "c4d1e9a0b2c",
  "cloud": "aws",
  "name": "research account",
  "state": "pending",
  "subject": "org_2x…:c4d1e9a0b2c",
  "issuer": "https://artifacts.041.io",
  "audience": "o41-artifacts",
  "identifiers": null,
  "error": null,
  "setup": "# 041 Artifacts: connect an AWS account. Run with the AWS CLI as an IAM administrator.\n…\necho \"roleArn=arn:aws:iam::$ACCOUNT_ID:role/o41-artifacts-4d1e9a0b2c\"",
  "created": "2026-10-06T09:00:00.000Z"
}
```

### `PUT /connections`

```text
PUT /api/v1/connections?id=c4d1e9a0b2c
```

Complete a connection with the identifiers the script printed. The app exchanges a token to check it before marking it active.

Request:

```json
{
  "cloud": "aws",
  "aws": {
    "roleArn": "arn:aws:iam::123456789012:role/o41-artifacts-4d1e9a0b2c"
  }
}
```

Response, 200:

```json
{
  "id": "c4d1e9a0b2c",
  "cloud": "aws",
  "name": "research account",
  "state": "active",
  "subject": "org_2x…:c4d1e9a0b2c",
  "issuer": "https://artifacts.041.io",
  "audience": "o41-artifacts",
  "identifiers": {
    "cloud": "aws",
    "aws": {
      "roleArn": "arn:aws:iam::123456789012:role/o41-artifacts-4d1e9a0b2c"
    }
  },
  "error": null,
  "setup": "# 041 Artifacts: connect an AWS account. Run with the AWS CLI as an IAM administrator.\n…\necho \"roleArn=arn:aws:iam::$ACCOUNT_ID:role/o41-artifacts-4d1e9a0b2c\"",
  "created": "2026-10-06T09:00:00.000Z"
}
```

GCP takes `{ cloud: "gcp", gcp: { projectId, projectNumber, poolId, providerId, serviceAccountEmail } }`, Azure `{ cloud: "azure", azure: { tenantId, clientId, subscriptionId, resourceGroup } }`.

### `DELETE /connections`

```text
DELETE /api/v1/connections?id=c4d1e9a0b2c
```

Remove a connection (204).

### `POST /locations`

```text
POST /api/v1/locations
```

Create a bucket in a connected cloud and region (`kind: "created"`), or add an S3 endpoint (`kind: "s3"`, below). Both are probed with a put, a ranged get, a multipart upload and a delete before they are saved.

Request:

```json
{
  "kind": "created",
  "connection": "c4d1e9a0b2c",
  "region": "us-east-1"
}
```

Response, 201:

```json
{
  "id": "l7f3k2q9d0a",
  "name": "aws us-east-1",
  "kind": "created",
  "cloud": "aws",
  "region": "us-east-1",
  "protocol": "s3",
  "endpoint": "https://s3.us-east-1.amazonaws.com",
  "bucket": "o41art-7f3k2q9d0a1b",
  "path": "",
  "connection": "c4d1e9a0b2c",
  "fallback": false,
  "default": true,
  "state": "active",
  "created": "2026-10-06T09:05:00.000Z"
}
```

An S3 endpoint:

```json
{
  "kind": "s3",
  "endpoint": "https://t3.storage.dev",
  "bucket": "my-checkpoints",
  "path": "artifacts",
  "signingRegion": "auto",
  "pathStyle": true,
  "accessKeyId": "tid_…",
  "secretAccessKey": "tsec_…",
  "cloud": "tigris",
  "region": "global",
  "fallback": true
}
```

### `PATCH /locations`

```text
PATCH /api/v1/locations?id=l7f3k2q9d0a
```

Make a location the fallback (`fallback: true`) or its cloud's default (`default: true`), retire it (`state: "retired"`), or rename it.

Request:

```json
{
  "fallback": true
}
```

Response, 200:

```json
{
  "id": "l7f3k2q9d0a",
  "name": "aws us-east-1",
  "kind": "created",
  "cloud": "aws",
  "region": "us-east-1",
  "protocol": "s3",
  "endpoint": "https://s3.us-east-1.amazonaws.com",
  "bucket": "o41art-7f3k2q9d0a1b",
  "path": "",
  "connection": "c4d1e9a0b2c",
  "fallback": true,
  "default": true,
  "state": "active",
  "created": "2026-10-06T09:05:00.000Z"
}
```

`409 conflict` when retiring the fallback; make another location the fallback first.

### `GET /usage`

```text
GET /api/v1/usage?days=30
```

Bytes and counts by day, kind, the writer's or reader's place, location, placement match and transfer.

Response, 200:

```json
{
  "rows": [
    {
      "day": "2026-10-06",
      "kind": "write",
      "placeCloud": "gcp",
      "placeRegion": "europe-west4",
      "location": "l2b8x0c1e5f",
      "match": "region",
      "transfer": "local",
      "bytes": 83802395150,
      "count": 10
    },
    {
      "day": "2026-10-06",
      "kind": "read",
      "placeCloud": "gcp",
      "placeRegion": "europe-west4",
      "location": "l7f3k2q9d0a",
      "match": null,
      "transfer": "cross-cloud",
      "bytes": 8380239515,
      "count": 1
    }
  ]
}
```

## API keys and identity

Organization API keys are for boxes and scripts. Making and revoking one needs a signed-in person.

### `GET /api-keys`

```text
GET /api/v1/api-keys
```

The organization's API keys.

Response, 200:

```json
{
  "keys": [
    {
      "id": "ak_31…",
      "name": "spot boxes",
      "created": "2026-10-06T09:10:00.000Z",
      "createdBy": "user_2y…",
      "lastUsed": null
    }
  ]
}
```

### `POST /api-keys`

```text
POST /api/v1/api-keys
```

Create a key. The secret is in this response only.

Request:

```json
{
  "name": "spot boxes"
}
```

Response, 201:

```json
{
  "key": {
    "id": "ak_31…",
    "name": "spot boxes",
    "created": "2026-10-06T09:10:00.000Z",
    "createdBy": "user_2y…",
    "lastUsed": null
  },
  "secret": "ak_…"
}
```

### `DELETE /api-keys`

```text
DELETE /api/v1/api-keys?id=ak_31…
```

Revoke a key (204).

### `GET /whoami`

```text
GET /api/v1/whoami
```

The organization and user a credential acts as.

Response, 200:

```json
{
  "orgId": "org_2x…",
  "userId": null,
  "credential": "api_key"
}
```

---

Artifacts by 041 documentation. Every page: https://artifacts.041.io/llms.txt
