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

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