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, 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.
Errors
Errors are application/problem+json (RFC 9457):
{
"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:
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
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:
{
"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:
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
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
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
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.
This page as Markdown: /docs/rest.md