> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentmark.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How to authenticate with the AgentMark Gateway API.

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`).

<Note>
  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](/integrations/tracing/opentelemetry#endpoint).
</Note>

## Creating an API key

1. Open the [AgentMark Dashboard](https://app.agentmark.co)
2. Switch to the app you want to scope the key to (shown in the breadcrumb)
3. Navigate to the app's **Settings → API keys** page (app-level, not org-level)
4. Click **Create API key**
5. Select a **role** (SDK, Read-Only, or Full Access) or choose **Custom** to toggle individual permissions
6. 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](/deploy/users-and-access-control#api-keys) 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](/concepts/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](/observe/tracing-setup#attributing-traces-to-an-environment).

<Note>
  You create kind-scoped keys through the Dashboard's create-key dialog. The programmatic `POST /v1/api-keys` route mints pinned keys only.
</Note>

## 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

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    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"
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    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();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    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()
    ```
  </Tab>
</Tabs>

## Rate limiting

API requests are rate-limited per tenant. Limits vary by plan; see [Billing and usage](/deploy/billing-and-usage#rate-limits). 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](https://app.agentmark.co/settings/billing).

## Error responses

Every `/v1` error body follows the same shape, `{ error: { code, message } }`:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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                   |
