All API endpoints (except health checks, capabilities, and pricing) require two headers:
| Header | Description |
|---|
Authorization | Your API key, as Bearer sk_agentmark_... |
X-Agentmark-App-Id | Your application ID, a UUID (for example, a1b2c3d4-5678-90ab-cdef-1234567890ab). Find it on the app’s Settings page or in the Dashboard URL. |
The Authorization value can be either an API key (sk_agentmark_...) or a session JWT (used by the Dashboard and agentmark login).
The REST API accepts the Authorization value with or without the Bearer prefix. The one exception is OpenTelemetry trace ingestion (POST /v1/traces via an OTLP exporter): send the raw key with no Bearer prefix, or the request fails with 401. See OpenTelemetry → Endpoint.
Creating an API key
- Open the AgentMark Dashboard
- Switch to the app you want to scope the key to (shown in the breadcrumb)
- Navigate to the app’s Settings → API keys page (app-level, not org-level)
- Click Create API key
- Select a role (SDK, Read-Only, or Full Access) or choose Custom to toggle individual permissions
- Copy the key. It’s only shown once
API keys enforce per-endpoint permissions. A key with SDK access can ingest traces and read templates but can’t delete anything. See Users and access control for the permission catalog and role presets.
API key environment scope
When you create a key, the Environment scope choice in the create-key dialog decides which environments the key can reach:
- This environment (the default) pins the key to the environment selected in the breadcrumb. Its traces always land in that one environment, and the key can’t reach any other.
- Environment kinds scopes the key to one or more kinds (Development, Preview, and Production) instead of pinning it. A kind-scoped key has no environment pin. Each request names the target environment (by environment name, or by the pull request number whose preview environment it targets), and the gateway authorizes that environment against the key’s allowed kinds before stamping it. The gateway rejects a selection that resolves to a kind the key can’t write to, so those traces stamp no environment rather than crossing kinds.
The kind-scoped option exists so that one key reaches every environment of the chosen kinds, including future PR preview environments. A single Preview-scoped key authorizes every PR’s preview environment as it’s created, so CI never has to provision a key per pull request. The SDK selects the target per request through initTracing({ environment, prNumber }), or through the AGENTMARK_ENVIRONMENT and AGENTMARK_PR_NUMBER variables in CI. See Attributing traces to an environment.
You create kind-scoped keys through the Dashboard’s create-key dialog. The programmatic POST /v1/api-keys route mints pinned keys only.
Endpoint permissions
Every API endpoint requires a specific permission. If your API key lacks the required permission, the request returns 403 Forbidden.
Traces and spans
| Endpoint | Permission |
|---|
POST /v1/traces | trace.write |
GET /v1/traces | trace.read |
POST /v1/traces/search | trace.read |
GET /v1/traces/{traceId} | trace.read |
GET /v1/traces/{traceId}/graph | trace.read |
GET /v1/traces/{traceId}/spans | span.read |
GET /v1/traces/{traceId}/spans/{spanId} | span.read |
GET /v1/spans | span.read |
POST /v1/spans/search | span.read |
GET /v1/spans/{spanId} | span.read |
GET /v1/filter-schema | trace.read |
Sessions
| Endpoint | Permission |
|---|
GET /v1/sessions | session.read |
GET /v1/sessions/{sessionId}/traces | session.read |
Scores
| Endpoint | Permission |
|---|
POST /v1/scores | score.write |
POST /v1/scores/batch | score.write |
GET /v1/scores | score.read |
POST /v1/scores/search | score.read |
GET /v1/scores/{scoreId} | score.read |
GET /v1/scores/aggregations | score.read |
GET /v1/scores/names | score.read |
DELETE /v1/scores/{scoreId} | score.delete |
GET /v1/score-configs | score_config.read |
GET /v1/score-configs/{name} | score_config.read |
Metrics, datasets, experiments, prompts
| Endpoint | Permission |
|---|
GET /v1/metrics | metrics.read |
GET /v1/config | template.read |
GET /v1/datasets | dataset.read |
POST /v1/datasets/{datasetName}/rows | dataset.write |
POST /v1/datasets/{datasetName}/rows/from-traces | dataset.write |
POST /v1/datasets/{datasetName}/rows/from-spans | dataset.write |
GET /v1/experiments | experiment.read |
GET /v1/experiments/{experimentId} | experiment.read |
GET /v1/experiments/baseline | experiment.read |
GET /v1/prompts | template.read |
GET /v1/runs/{runId}/traces | trace.read |
GET /v1/templates?path={filePath} | template.read |
Annotation queues
| Endpoint | Permission |
|---|
GET /v1/annotation-queues | annotation_queue.read |
POST /v1/annotation-queues | annotation_queue.write |
GET /v1/annotation-queues/{queueId} | annotation_queue.read |
PATCH /v1/annotation-queues/{queueId} | annotation_queue.write |
DELETE /v1/annotation-queues/{queueId} | annotation_queue.delete |
GET /v1/annotation-queues/{queueId}/items | annotation_queue.read |
POST /v1/annotation-queues/{queueId}/items | annotation_queue.write |
GET /v1/annotation-queues/{queueId}/items/{itemId} | annotation_queue.read |
PATCH /v1/annotation-queues/{queueId}/items/{itemId} | annotation_queue.write |
DELETE /v1/annotation-queues/{queueId}/items/{itemId} | annotation_queue.delete |
POST /v1/annotation-queues/{queueId}/items/{itemId}/reviews | annotation_queue.review |
Unauthenticated
| Endpoint | Notes |
|---|
GET /v1/capabilities | No auth required |
GET /v1/pricing | No auth required |
GET /health | No auth required |
GET /v1/health/ingestion | No auth required |
GET /v1/health/files | No auth required |
Making requests
curl https://api.agentmark.co/v1/traces \
-H "Authorization: Bearer sk_agentmark_your_key_here" \
-H "X-Agentmark-App-Id: a1b2c3d4-5678-90ab-cdef-1234567890ab"
const response = await fetch("https://api.agentmark.co/v1/traces", {
headers: {
"Authorization": `Bearer ${process.env.AGENTMARK_API_KEY}`,
"X-Agentmark-App-Id": process.env.AGENTMARK_APP_ID!,
},
});
const data = await response.json();
import os
import requests
response = requests.get(
"https://api.agentmark.co/v1/traces",
headers={
"Authorization": f"Bearer {os.environ['AGENTMARK_API_KEY']}",
"X-Agentmark-App-Id": os.environ["AGENTMARK_APP_ID"],
},
)
data = response.json()
Rate limiting
API requests are rate-limited per tenant. Limits vary by plan; see Billing and usage. When you exceed your rate limit, the API returns 429 Too Many Requests with a Retry-After header indicating how long to wait before retrying.
Span and storage limits
Trace ingestion (POST /v1/traces) enforces monthly quotas depending on your plan:
- Span limit: maximum number of spans per month (Hobby/Growth tiers)
- Storage cap: maximum storage used (certain plans only)
When exceeded, the API returns 429 with a span_limit_exceeded or storage_cap_exceeded error code and a Retry-After header. Upgrade your plan at Settings → Billing.
Error responses
Every /v1 error body follows the same shape, { error: { code, message } }:
{
"error": {
"code": "unauthorized",
"message": "Not authorized"
}
}
Some errors add machine-readable context as fields alongside code and message. Quota errors, for example, spread their context as flat siblings:
{
"error": {
"code": "span_limit_exceeded",
"message": "Monthly span limit of 20000 reached. Upgrade your plan to raise this limit.",
"currentCount": 20000,
"limit": 20000,
"upgradeUrl": "https://app.agentmark.co/settings/billing"
}
}
400 validation errors are the one case that nests: error.message is a generic summary (Invalid request body / Invalid query parameters / Invalid path parameters), and the error.details map collects the per-field messages keyed by field name:
{
"error": {
"code": "invalid_request_body",
"message": "Invalid request body",
"details": {
"score": "Expected number, received string"
}
}
}
| Status | Meaning |
|---|
400 | Invalid request. error.message is a generic summary; per-field errors are in the error.details map |
401 | Missing or invalid Authorization / X-Agentmark-App-Id header |
403 | API key is valid but doesn’t have the required permission for this endpoint |
429 | Rate limited or plan quota exceeded |
503 | Service temporarily overloaded. Retry after the interval in the Retry-After header |