# REST API

> The plain HTTP API behind the SDKs, for curl, Make, Zapier or any HTTP step.

## Base URL and authentication

```
https://api.artefaktum.dev
```

Every request except `/health/live` and `/health/ready` needs an API key, created in the [console](https://artefaktum.dev/console/), sent as:

```
Authorization: Bearer <key>
```

Requests and responses are JSON: send `Content-Type: application/json` on any body.

The OpenAPI document is at `https://api.artefaktum.dev/openapi.json`; interactive docs from it are at `https://api.artefaktum.dev/docs`, useful for a typed client in a language the SDKs don't cover.

## Limits

Limits apply when you write. Artifacts that are already stored are never re-validated.

| Field | Limit |
|---|---|
| `title` | 1 to 500 characters |
| `description` | 10,000 characters |
| `tags` | 50 per artifact |
| one tag | 1 to 64 characters, not blank |
| `metadata` | 16,384 bytes as compact JSON, nested at most 5 levels deep |
| `external_key` | 512 characters |
| `filename` | 1,024 characters |
| `content_type` | 255 characters |
| `run_id` | 128 characters |
| request body | 262,144 bytes (256 KB) |

A field over its limit answers `400 invalid_request`. `detail` starts with the field's
path, then says what was sent and what the limit is, for example
`body.metadata: Value error, metadata is 20011 bytes as JSON; the limit is 16384`. A
tag's path carries its position in the list, as in `body.tags.0`. Branch on `code`, not
`detail`: its wording can change. A request body over 256 KB answers
`413 request_too_large` before it is read, for example
`the request body exceeds the limit of 262144 bytes`. File bytes never pass through
the API, so this limit does not apply to your files: those go straight to storage, up to
the file size of your plan.

Metadata depth counts containers: `{}` is one level, `{"a": {"b": 1}}` is two. Lists
count the same as objects.

Storage, monthly API calls and file size depend on your plan; see
[pricing](/pricing/).

## Errors

Errors are `application/problem+json` (RFC 9457):

```json
{
  "type": "https://artefact.ai/errors/artifact_not_found",
  "title": "Artifact not found",
  "status": 404,
  "detail": "artifact 0199198a-... not found",
  "instance": "/v1/artifacts/0199198a-...",
  "code": "artifact_not_found",
  "request_id": "..."
}
```

`code` is the stable field to branch on; `type`, `title` and `detail` are for humans. Codes you'll actually see:

| Code | Status | Meaning |
|---|---|---|
| `invalid_request` | 400 | Body or query param failed validation. |
| `unauthorized` | 401 | Missing or invalid bearer token. |
| `insufficient_scope` | 403 | The key lacks the required scope. |
| `artifact_not_found` | 404 | No such artifact, or it belongs to another tenant. |
| `run_not_found` | 404 | No such run. |
| `artifact_not_ready` | 409 | The artifact's state doesn't support this call. |
| `external_key_conflict` | 409 | The external key is already used in this project. |
| `idempotency_conflict` | 409 | `Idempotency-Key` reused with a different body. |
| `run_sealed` | 409 | The run no longer accepts writes. |
| `upload_expired` | 410 | The signed upload window closed before completion. |
| `object_verification_failed` | 422 | The uploaded object failed verification. |
| `quota_exceeded` | 413 | The file or tenant storage exceeds the plan limit. |
| `quota_exceeded` | 429 | The monthly call limit is exhausted. |
| `request_too_large` | 413 | The request body is over 256 KB. File bytes never count. |
| `upstream_unavailable` | 502 | A dependency ArtefactAI calls did not answer. |
| `embedding_unavailable` | 503 | The embedding provider could not produce a vector. |

`quota_exceeded` is deliberately the same code at two statuses: clients branch on the code, and 429 (not 402) is used so it doesn't collide with a payment-required response.

## Endpoints

| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | `/health/live` | none | Liveness probe. |
| GET | `/health/ready` | none | Liveness plus a database check. |
| GET | `/health/whoami` | any | Return the tenant and project the key resolves to. |
| GET | `/v1/projects` | `artifacts:read` | List the projects this credential can use. |
| POST | `/v1/artifacts/search` | `artifacts:search` | Find artifacts by text, semantic similarity or filters. |
| POST | `/v1/artifacts/resolve` | `artifacts:write` | Reuse a fresh artifact by external key, or reserve it. |
| POST | `/v1/artifacts/uploads` | `artifacts:write` | Reserve a new artifact; get a signed upload URL. |
| POST | `/v1/artifacts/{artifact_id}/versions/{version_id}/complete` | `artifacts:write` | Confirm the bytes were uploaded. |
| GET | `/v1/artifacts` | `artifacts:read` | List artifacts in a project, with filters and a cursor. |
| GET | `/v1/artifacts/by-external-key/{external_key}` | `artifacts:read` | Look up an artifact by external key. |
| GET | `/v1/artifacts/{artifact_id}` | `artifacts:read` | Read one artifact's metadata and lifecycle state. |
| GET | `/v1/artifacts/{artifact_id}/versions` | `artifacts:read` | List an artifact's versions. |
| GET | `/v1/artifacts/{artifact_id}/download` | `artifacts:read` | Get a short-lived signed download URL. |
| PATCH | `/v1/artifacts/{artifact_id}` | `artifacts:write` | Update title, description, tags, metadata or TTL. |
| POST | `/v1/artifacts/{artifact_id}/uploads` | `artifacts:write` | Start a new version; get a signed upload URL. |
| POST | `/v1/artifacts/{artifact_id}/relations` | `artifacts:write` | Add an explicit lineage relation. |
| GET | `/v1/artifacts/{artifact_id}/relations` | `artifacts:read` | List an artifact's direct relations, both directions. |
| DELETE | `/v1/artifacts/{artifact_id}` | `artifacts:delete` | Request deletion (`202 Accepted`). |
| POST | `/v1/runs` | `artifacts:write` | Create or register a run id. |
| POST | `/v1/runs/{run_id}/seal` | `artifacts:write` | Reject further writes to a run. |
| GET | `/v1/runs/{run_id}/artifacts` | `artifacts:read` | List artifacts produced in a run. |
| POST | `/v1/api-keys` | `artifacts:admin` | Mint a key; the secret is returned once. |
| GET | `/v1/api-keys` | `artifacts:admin` | List keys, never their secrets. |
| DELETE | `/v1/api-keys/{key_id}` | `artifacts:admin` | Revoke a key. |
| GET | `/v1/quota` | `artifacts:read` | Plan, limits and current usage for the tenant. |
| GET | `/v1/usage` | `artifacts:admin` | Time-bucketed request and byte counts. |

A key can never be granted a scope its creator lacks.

## Worked examples

### Search

Exactly the call the homepage's REST panel shows:

```sh
curl -s -X POST https://api.artefaktum.dev/v1/artifacts/search \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \
  -d '{"project_id": "…", "query": "latest enriched competitor list", "mode": "hybrid"}'
```

Request body fields:

| Field | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `query` | string | `""` |
| `mode` | `"exact"` \| `"text"` \| `"semantic"` \| `"hybrid"` | `"hybrid"` |
| `filters.tags_all` | string[] | `[]` |
| `filters.content_types` | string[] | `[]` |
| `filters.status` | string[] | `["ready"]` |
| `filters.external_key` | string | none |
| `filters.created_after` / `filters.created_before` | date-time | none |
| `filters.exclude_superseded` | boolean | `true` |
| `limit` | integer, 1-200 | `20` |
| `cursor` | string | none |

Response items carry the artifact, a `score` and a `match_mode`; scores aren't comparable across modes.

### Create upload, PUT bytes, complete

```sh
curl -s -X POST https://api.artefaktum.dev/v1/artifacts/uploads \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "project_id": "'"$PID"'",
    "title": "Acme enrichment response",
    "filename": "acme.json",
    "content_type": "application/json",
    "size_bytes": 5120,
    "tags": ["company", "enrichment"]
  }'
```

Request body fields:

| Field | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `filename` | string | required |
| `content_type` | string | required |
| `size_bytes` | integer | required |
| `title` | string | required |
| `description` | string | `""` |
| `tags` | string[] | `[]` |
| `metadata` | object | `{}` |
| `external_key` | string | none |
| `expires_at` | date-time | none |
| `run_id` | string | none |
| `infer_lineage` | boolean | `true` |

`summary` was removed on 2026-09-27. A request that still sends the key is refused with `400 invalid_request`, like any other unknown key, and responses no longer carry it. Clients published before then (Python SDK 0.1.1, TypeScript SDK 0.1.0, n8n node 0.1.3) send the key only when you set a summary; stop setting it or upgrade.

This is the one endpoint that accepts an `Idempotency-Key` header, so a retried request doesn't create a second reservation.

Response:

```json
{
  "artifact": { "id": "...", "version_id": "...", "status": "pending_upload" },
  "upload": { "method": "PUT", "url": "...", "headers": {"content-type": "application/json"}, "expires_at": "..." }
}
```

`PUT` the bytes to `upload.url` with the given headers, then complete:

```sh
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/json" --data-binary @acme.json

curl -s -X POST "https://api.artefaktum.dev/v1/artifacts/$AID/versions/$VID/complete" \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY"
```

Safe to repeat. The artifact is `ready` once the server verifies the object; embedding for semantic search happens afterwards, tracked separately as `semantic_ready`.

### Get a download URL

```sh
curl -s "https://api.artefaktum.dev/v1/artifacts/$AID/download" \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY"
```

Optional params: `version_id` (defaults to latest) and `run_id` (for lineage inference on a later upload). The response is a short-lived signed URL; the API never proxies the bytes, and fetching it records a usage event.

### Resolve by external key

```sh
curl -s -X POST https://api.artefaktum.dev/v1/artifacts/resolve \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "project_id": "'"$PID"'",
    "external_key": "company-api/acme/2026-09-17",
    "max_age_seconds": 86400,
    "filename": "acme.json", "content_type": "application/json",
    "size_bytes": 5120, "title": "Acme enrichment"
  }'
```

Request body fields:

| Field | Type | Default |
|---|---|---|
| `project_id` | UUID | required |
| `external_key` | string | required |
| `max_age_seconds` | integer | none (any existing artifact counts as fresh) |
| `filename` | string | required |
| `content_type` | string | required |
| `size_bytes` | integer | required |
| `title` | string | required |
| `description` | string | `""` |
| `tags` | string[] | `[]` |
| `metadata` | object | `{}` |
| `run_id` | string | none |

`status` is `hit` (an artifact is returned, no upload needed), `create` (you hold the reservation: `PUT` to `upload.url`, then complete), or `pending` (retry after `retry_after_seconds`). For a plain lookup with no side effect, use `GET /v1/artifacts/by-external-key/{external_key}?project_id=...`.

### Add a relation

```sh
curl -s -X POST "https://api.artefaktum.dev/v1/artifacts/$OUT_ID/relations" \
  -H "Authorization: Bearer $ARTEFAKTUM_API_KEY" -H "Content-Type: application/json" \
  -d '{"to_artifact_id": "'"$IN_ID"'", "relation_type": "derived_from"}'
```

Body: `to_artifact_id` (required), `relation_type` (required: `derived_from`, `supersedes`, `attachment_of`, `generated_by` or `related_to`), optional `to_version_id`, optional `metadata`. Explicit relations replace inferred ones. When an upload names a `run_id`, the server infers `derived_from` edges from whatever that key downloaded earlier in the run; pass `"infer_lineage": false` on the upload to suppress it.

## Quota and usage

`GET /v1/quota` never counts against the monthly call limit, so a key can always see why it was refused. It returns the plan name, `storage: {used_bytes, limit_bytes}`, `calls: {used, limit, resets_at}`, and `max_file_bytes`. `GET /v1/usage` takes `start`, `end`, `granularity` (`hour` or `day`), `metric`, `project_id` and `principal_id`, returning time-bucketed counts for `requests`, `requests_error`, `storage_bytes`, `bytes_uploaded` and `bytes_downloaded`.
