Artifacts
All pages
Docs · ReferenceMarkdown

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 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:

{
  "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

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:

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

Response, 200:

{
  "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

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:

{
  "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:

{
  "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

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:

{
  "epoch": 2
}

Response, 200:

{
  "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

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:

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

Response, 200:

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

409 superseded when another writer opened the artifact:

{
  "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

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:

{
  "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

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:

{
  "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

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:

{
  "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

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

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:

{
  "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

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:

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

Response, 201:

{
  "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

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:

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

Response, 200:

{
  "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

DELETE /api/v1/connections?id=c4d1e9a0b2c

Remove a connection (204).

POST /locations

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:

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

Response, 201:

{
  "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:

{
  "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

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:

{
  "fallback": true
}

Response, 200:

{
  "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

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:

{
  "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

GET /api/v1/api-keys

The organization's API keys.

Response, 200:

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

POST /api-keys

POST /api/v1/api-keys

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

Request:

{
  "name": "spot boxes"
}

Response, 201:

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

DELETE /api-keys

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

Revoke a key (204).

GET /whoami

GET /api/v1/whoami

The organization and user a credential acts as.

Response, 200:

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