Base URL
- Cloud
- Local
agentmark dev) and Cloud implement the same /v1/* endpoints. What differs is which handlers run where:
- Both surfaces:
/v1/traces(ingest + read),/v1/sessions,/v1/spans,/v1/scores(full CRUD + batch),/v1/capabilities,/v1/pricing,/v1/filter-schema, and the root/healthliveness check. - Cloud-only (local returns
404or a501 not_available_locallystub):/v1/metrics,/v1/scores/aggregations, the structured-search endpoints (/v1/traces/search,/v1/spans/search,/v1/scores/search), and the dependency health endpoints (/v1/health/*).
GET /v1/capabilities to probe which features a server supports at runtime.
Every endpoint carries the /v1/ prefix except the root health check.
Available endpoints
The generated pages in this sidebar, built from the OpenAPI spec, are the authoritative per-endpoint reference for parameters, schemas, and responses; this table is a map. The Where column shows which environments implement each route. “Cloud + Local” means the same handler semantics on both; “Cloud only” / “Local only” mean the other side returns501 (with a not_available_on_cloud / not_available_locally error code) or 404.
Use the sidebar to browse interactive documentation for each endpoint.
Response format
All responses are JSON unless otherwise noted (for example, CSV exports). Error responses follow a consistent canonical envelope:error.code field is the programmatic discriminator: use it to branch on specific error cases. The error.message field is the human-readable description to show to users. Additional context (for example, retry_after_seconds, jobId) appears as extra fields directly inside error, alongside code and message. The one exception is 400 validation errors, which nest per-field messages in an error.details map (see Authentication):
Rate limiting
Requests are rate-limited per tenant. When you exceed your rate limit, the API returns a429 status code.
Trace ingestion has additional monthly span and storage quotas depending on your plan.
See Authentication for details.
Versioning
Every endpoint carries the/v1/ prefix. Breaking changes ship under new version prefixes (/v2/, etc.) with a 90+ day deprecation window, so /v1/ keeps working while you migrate.
See API versioning & stability for the full policy on what’s breaking, what’s additive, and how AgentMark announces deprecations.
Why there is no PATCH /v1/traces
Traces are immutable in AgentMark. Once AgentMark stores a span, the row representing what happened during that execution becomes permanent. No endpoint mutates it.
Other observability platforms expose a “patch trace” endpoint that lets clients backfill metadata, attach a label, or correct a field after ingestion. AgentMark covers those workflows through two separate, append-only resources instead:
- Scores (
POST /v1/scores,POST /v1/scores/batch): attach a graded value (numeric, categorical, or boolean) to a trace or span after the fact. AgentMark versions scores bycreated_at, and they never overwrite the underlying span. - Comments: free-form human notes on a trace or span, stored alongside the trace as a separate resource.
PATCH /v1/traces won’t ship in /v1/, /v2/, or any future version.
The filter query grammar
GET /v1/traces and GET /v1/spans accept a filter query parameter, a human-readable string expression. One expression composes across both surfaces: write it once, reuse it for trace and span listings.
A filter is one or more clauses combined with and. A clause is a single predicate, or a parenthesized OR-group of predicates:
or is only valid inside parentheses and and only outside them, so there are no precedence rules to learn. Groups don’t nest.
URL-encode the whole expression. For example, you send filter=metadata.env = "prod" and status = ERROR as:
Operators
Quote any value containing spaces or special characters (
"..." or '...'). Bare values are fine for simple tokens (status = ERROR, cost > 0.01).
Fields
<key> matches [a-zA-Z_][a-zA-Z0-9_]{0,63}. Up to 20 predicates per request, counting every predicate inside OR-groups.
Examples
400 with an invalid_filter code and a message describing the problem. It’s never silently ignored.
Structured JSON filters (search endpoints)
POST /v1/traces/search, POST /v1/spans/search, and POST /v1/scores/search accept the same filters as a JSON request body, the form to use when building filters programmatically (SDKs, agents) instead of string-assembling DSL expressions:
equals, notEquals, contains, notContains, startsWith, endsWith, gt, gte, lt, lte, exists, doesNotExist). The JSON form additionally supports membership and range operators that have no DSL syntax:
Trace/span search uses the field set above; score search filters on
name, score, source, user_id, resource_id, label, and created_at. GET /v1/filter-schema returns the full machine-readable schema (fields, operators, and limits per resource), so a client (or an agent via MCP) can construct valid filters without trial-and-error.
Search endpoints apply guardrails the frozen GET contracts don’t: requests default to the last 7 days when start_date is unset, the maximum window is 90 days, and the routes are rate-limited per tenant.
The search endpoints are Cloud-only (the local dev server answers them with 501 not_available_locally), but the gateway serves GET /v1/filter-schema on both surfaces from the same generated contract.
The
filter string grammar (GET endpoints) and the JSON form (POST search endpoints) are the same filter language in two encodings; use whichever fits the call site. JSON filters are only accepted in POST request bodies, never as a GET query parameter.Reading custom metadata
The read endpoints return custom metadata you attach at ingestion (OTLPagentmark.metadata.* attributes, or the SDK’s metadata option):
GET /v1/traces/{traceId}: a trace-levelmetadataobject (the root span’s metadata) and a per-spanmetadataobject on each entry ofspans.GET /v1/traces/{traceId}/spans/{spanId}: ametadataobject alongside the span’sinput/output.GET /v1/spansandGET /v1/spans/{spanId}:metadataon each span.
metadata object excludes reserved internal namespaces (such as graph.node.*, which GET /v1/traces/{traceId}?fields=graph surfaces separately).
Reading traces requires an API key with the
trace.read permission (and span.read / session.read for the span/session endpoints). Write-only SDK keys (trace.write + score.write) can ingest traces and post scores but can’t read traces back. A programmatic consumer (CI pipeline, agent) that needs readback must use a key minted with the read permissions (the read-only or full-access preset, or check trace.read in the key’s permission picker). This is least-privilege by design: it keeps a leaked write-only SDK key from exfiltrating trace contents./v1/scores accepts session_id (scope scores to a session), alongside start_date, end_date, and source.