All pages
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 thex-o41-organizationheader; 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/1234Open 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-00000510Begin 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®ion=europe-west4&source=envOne 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/1234Committed 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¬e=evaluated.jsonA 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/orgThe 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/connectionsStart 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=c4d1e9a0b2cComplete 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=c4d1e9a0b2cRemove a connection (204).
POST /locations
POST /api/v1/locationsCreate 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=l7f3k2q9d0aMake 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=30Bytes 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-keysThe 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-keysCreate 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/whoamiThe organization and user a credential acts as.
Response, 200:
{
"orgId": "org_2x…",
"userId": null,
"credential": "api_key"
}