Skip to main content
The AgentMark Gateway API provides direct HTTP access to trace ingestion, scoring, and template retrieval.

Base URL

The local dev server (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 /health liveness check.
  • Cloud-only (local returns 404 or a 501 not_available_locally stub): /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/*).
Call 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 returns 501 (with a not_available_on_cloud / not_available_locally error code) or 404. Use the sidebar to browse interactive documentation for each endpoint.
Two programmatic surfaces, same OpenAPI spec under the hood:
  • From shell / CI: call the REST endpoints with curl and an AGENTMARK_API_KEY (or the session bearer from ~/.agentmark/auth.json after agentmark login).
  • From an IDE agent: run the agentmark-mcp MCP server. It fetches this spec at startup and exposes one MCP tool per operation (for example, list_traces, create_app, start_app_git_connect), so your Claude Code / Cursor / etc. agent can drive the gateway headlessly.

Response format

All responses are JSON unless otherwise noted (for example, CSV exports). Error responses follow a consistent canonical envelope:
The 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):
The shape matches Stripe, OpenAI, and Anthropic error conventions, so one parser works across all endpoints.

Rate limiting

Requests are rate-limited per tenant. When you exceed your rate limit, the API returns a 429 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 by created_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.
The two resources above are the migration targets for any “patch trace” workflow you’d build on a competitor. This split is intentional: it keeps the audit trail clean (you can always tell what the model did versus what a reviewer added later) and lets retention, RBAC, and export rules apply differently to raw execution data than to human-attached metadata. This is a permanent design choice, not a missing feature. 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

A malformed or unsupported filter returns 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:
Semantics mirror the string DSL exactly: the list is an AND of clauses, a clause is a predicate or a one-level OR-group, and both forms compile to the same query. Operator names are the canonical camelCase set (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 (OTLP agentmark.metadata.* attributes, or the SDK’s metadata option):
  • GET /v1/traces/{traceId}: a trace-level metadata object (the root span’s metadata) and a per-span metadata object on each entry of spans.
  • GET /v1/traces/{traceId}/spans/{spanId}: a metadata object alongside the span’s input / output.
  • GET /v1/spans and GET /v1/spans/{spanId}: metadata on each span.
The endpoints return metadata as a flat string→string object. The public 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.