# OpenAttribution API reference

> Single-file reference for AI coding assistants. Drop this URL (https://openattribution.org/docs/api.md) into Claude, ChatGPT, Cursor, or any other LLM and ask it to build the integration in your stack.

OpenAttribution is an open standard for content attribution in the age of AI. The telemetry API has two sides:

- **Write endpoints** for agents, marketplaces, and infrastructure emitters reporting events.
- **Read endpoints** for content owners (and their delegated measurement partners) querying their data.

The site is at https://openattribution.org. The full HTML version of this page is at https://openattribution.org/docs/api. The protocol specification is at https://github.com/openattribution-org/telemetry/blob/main/SPECIFICATION.md.

---

## Base URLs

| Environment | URL | Purpose |
|---|---|---|
| Production - reads + control | `https://api.openattribution.org` | Content-owner reads, identity, delegations, click tokens |
| Production - ingest | `https://telemetry.openattribution.org` | High-volume event ingestion (`/events`, `/session/*`) |
| Local development | `http://localhost:8007` | Both surfaces share the dev server |

All endpoint paths below are relative to the base URL. Read endpoints use `api.openattribution.org`; write endpoints (events, sessions) use `telemetry.openattribution.org` in production.

**Path-prefix convention.** Telemetry and read endpoints are flat: `/events`, `/sessions/start`, `/content-owners/summary`, `/agent/events`, `/click-tokens`, `/ctx/{token}`, `/resolve`. Versioning is carried in the event payload (`schema_version`), not the URL. Identity endpoints keep an explicit `/api/v1/identity/*` prefix.

---

## Authentication

All authenticated endpoints use an API key in the `X-API-Key` header. There are two key types:

| Key type | Prefix | Use case |
|---|---|---|
| Platform | `oat_pk` | Any platform org - agents, content marketplaces, networks, attribution vendors, analytics tools - whether it writes telemetry events (`telemetry:write`) or reads telemetry content owners delegate to it (`telemetry:read` + `delegated_from`). The prefix is the org type, not the scope. |
| Content owner | `oat_pub` | Content owners querying their own telemetry (and, with `telemetry:write` scope, reporting events for their own verified domains - e.g. a self-hosted edge worker or origin middleware) |

Example:

```bash
curl -H "X-API-Key: oat_pub_..." https://api.openattribution.org/content-owners/summary
```

Content-owner keys are scoped to specific verified domains. A key for `wirecutter.com` can only query data for that domain. There is no cross-content-owner aggregation.

Read paths live at `/content-owners/*` (for content-owner dashboards) and `/agent/*` (for agent dashboards). The `oat_pub` prefix denotes a content-owner key; what it can do is set by its scopes (`telemetry:read`, `telemetry:write`), and either way it is bound to that owner's verified domains.

---

## Content-owner read endpoints

Query telemetry data for your verified domains. Requires a content-owner API key (`oat_pub`).

### GET /content-owners/summary

Aggregate metrics across your domains - total events, total sessions, breakdown by event type, and per-agent activity.

**Query parameters**

| Parameter | Type | Description |
|---|---|---|
| `since` | ISO 8601 | Start of period (optional) |
| `until` | ISO 8601 | End of period (optional) |
| `domain` | string | Filter to a specific domain (optional) |

**Response**

```json
{
  "organization_id": "...",
  "domains": ["wirecutter.com"],
  "total_events": 14720,
  "total_sessions": 3891,
  "events_by_type": [
    { "event_type": "content_retrieved", "count": 14720 }
  ],
  "events_by_source": [
    { "source_role": "edge", "count": 9100, "sessions": 0 },
    { "source_role": "agent", "count": 5620, "sessions": 3891 }
  ],
  "agents": [
    { "platform_id": "openai", "agent_id": "chatgpt-browse", "event_count": 6200, "session_count": 1580 },
    { "platform_id": "anthropic", "agent_id": "claude-web", "event_count": 4100, "session_count": 1020 },
    { "platform_id": "perplexity", "agent_id": "perplexity", "event_count": 4420, "session_count": 1291 }
  ],
  "period_start": "2026-03-01T00:00:00Z",
  "period_end": null
}
```

### GET /content-owners/events

Paginated list of individual events for your domains.

**Query parameters**

| Parameter | Type | Description |
|---|---|---|
| `since` | ISO 8601 | Start of period (optional) |
| `until` | ISO 8601 | End of period (optional) |
| `domain` | string | Filter to a specific domain (optional) |
| `limit` | integer | Page size (default: 100) |
| `offset` | integer | Offset for pagination (default: 0) |

**Response**

```json
{
  "items": [
    {
      "event_id": "550e8400-...",
      "session_id": "7c9e6679-...",
      "event_type": "content_retrieved",
      "content_url": "https://wirecutter.com/reviews/best-headphones",
      "event_timestamp": "2026-03-18T10:00:00Z",
      "event_data": { "user_agent": "ClaudeBot/1.0" },
      "platform_id": "anthropic",
      "agent_id": "claude-web"
    }
  ],
  "total": 14720,
  "limit": 100,
  "offset": 0
}
```

### GET /content-owners/urls

Per-URL metrics - which content is being retrieved most, by how many sessions, and when it was last seen.

**Query parameters**

| Parameter | Type | Description |
|---|---|---|
| `since`, `until`, `domain` | - | Same as above |
| `limit`, `offset` | integer | Pagination (default: limit 20, offset 0) |

**Response**

```json
{
  "items": [
    {
      "content_url": "https://wirecutter.com/reviews/best-headphones",
      "total_events": 842,
      "unique_sessions": 391,
      "event_types": [
        { "event_type": "content_retrieved", "count": 842 }
      ],
      "last_seen": "2026-03-18T14:22:00Z"
    }
  ],
  "total": 156,
  "limit": 20,
  "offset": 0
}
```

---

## Agent read endpoint

For agent organisations querying their own session and event activity. Requires an agent-org API key.

### GET /agent/events

Paginated list of events emitted by your agent. Same query-parameter shape as `/content-owners/events` (`since`, `until`, `limit`, `offset`).

---

## Delegated access (measurement partners)

Content owners can grant measurement partners (affiliate networks, dashboard tools, analytics platforms) read access to their telemetry data. Partners query the same content-owner endpoints, adding the `delegated_from` parameter.

### Querying as a delegate

Append `delegated_from` to any content-owner GET endpoint with the content owner's organisation ID:

```
GET /content-owners/summary?delegated_from={grantor_org_id}&since=2026-03-01
X-API-Key: oat_pk_yourkey...
```

The gateway checks for an active delegation from the content owner to your organisation. If valid, you receive their telemetry data exactly as they would see it. If not, you get a `403`.

All three content-owner endpoints support delegation: `/content-owners/summary`, `/content-owners/events`, and `/content-owners/urls`.

### Managing delegations

Content owners and partners both interact with delegations via the identity API. Auth is via the same `X-API-Key` header.

| Endpoint | Description |
|---|---|
| `GET /api/v1/identity/delegations` | List active delegations. Optional `?role=grantor` (delegations you have granted) or `?role=grantee` (delegations granted to you) |
| `POST /api/v1/identity/delegations` | Grant access. Body: `grantee_org_id` (required), `scopes` (optional, default `["telemetry:read"]`) |
| `DELETE /api/v1/identity/delegations/{id}` | Revoke a delegation. Only the content owner (grantor) can revoke |

**Grant example**

```json
POST /api/v1/identity/delegations
{
  "grantee_org_id": "uuid-of-partner-org",
  "scopes": ["telemetry:read"]
}
```

**List response (as grantee)**

```json
[
  {
    "id": "...",
    "grantor_org_id": "...",
    "grantee_org_id": "your-org-id",
    "grantor_name": "Wirecutter",
    "grantee_name": "Your Network",
    "scopes": ["telemetry:read"],
    "created_at": "2026-03-24T...",
    "revoked_at": null
  }
]
```

Use the `grantor_org_id` from each delegation as the `delegated_from` parameter when querying content-owner endpoints.

---

## Click tokens

When an agent generates an outbound link, it can create a click token that ties the click back to the full session - which content was retrieved, cited, and by which agent. Networks and retailers use the token to look up session context on the landing page.

### POST /click-tokens

Requires a platform API key.

**Request**

```json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "content_url": "https://retailer.com/headphones/sony-wh1000xm5"
}
```

**Response**

```json
{
  "token": "ctx_abc123def456",
  "session_id": "550e8400-...",
  "content_url": "https://retailer.com/headphones/sony-wh1000xm5",
  "expires_at": "2026-06-18T10:00:00Z"
}
```

The agent appends the token to the outbound URL:

```
https://retailer.com/headphones/sony-wh1000xm5?ctx=ctx_abc123def456
```

Click tokens expire after 90 days.

### GET /ctx/{token}

Requires a platform API key. Networks and retailers capture the `ctx` parameter from the landing page URL and look up the session context.

Lookup is two-sided opt-in. The endpoint returns `404` unless:

- the agent that issued the token has `share_sessions_via_click_tokens` enabled, **and**
- the content owner whose URLs were cited has `visible_in_click_token_lookups` enabled

Both default to `false`. Toggle either flag in the dashboard settings page or via `PATCH /api/v1/identity/settings`.

**Response**

```json
{
  "session_id": "550e8400-...",
  "started_at": "2026-03-18T10:00:00Z",
  "click_content_url": "https://retailer.com/headphones/sony-wh1000xm5",
  "content_urls_cited": [
    "https://wirecutter.com/reviews/best-headphones",
    "https://techradar.com/best/wireless-headphones"
  ],
  "content_urls_retrieved": [
    "https://wirecutter.com/reviews/best-headphones",
    "https://techradar.com/best/wireless-headphones",
    "https://rtings.com/headphones/reviews/sony/wh-1000xm5"
  ]
}
```

---

## Domain resolution

### GET /resolve

Check whether a domain maps to a registered content owner. No authentication required. Accepts either a `domain` or a `url` parameter (the domain is extracted from the URL).

```
GET /resolve?domain=wirecutter.com
```

**Response**

```json
{
  "domain": "wirecutter.com",
  "handled": true,
  "organization": {
    "id": "...",
    "name": "Wirecutter"
  }
}
```

If the domain is not registered, `handled` is `false` and `organization` is `null`.

---

## Write endpoints

For agents, content marketplaces, and infrastructure emitters reporting telemetry. Requires a key with `telemetry:write` scope - a platform key (`oat_pk`) for agents, marketplaces, and third-party emitters, or a content-owner key (`oat_pub`) when the emitter is reporting events about its own verified domains (e.g. a self-hosted Cloudflare Worker). In production, send write traffic to `https://telemetry.openattribution.org`.

The `source_role` field on each event identifies the emitter:

- `agent` - the AI agent
- `index` - a content marketplace or licensed repository
- `edge` - a CDN or edge network
- `origin` - the content owner's web server

### POST /sessions/start

Open a session. Returns a server-assigned `session_id` you carry on subsequent events.

**Request**

```json
{
  "initiator_type": "user",
  "agent_id": "shopping-assistant",
  "external_session_id": "ext-123",
  "prior_session_ids": ["previous-session-uuid"]
}
```

**Response**

```json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

**Fields**

| Field | Required | Description |
|---|---|---|
| `initiator_type` | No | `"user"` or `"agent"` (default: `"user"`) |
| `agent_id` | No | Identifier for the responding agent |
| `manifest_ref` | No | URL of a manifest served at `/.well-known/openattribution.json` (root or path-prefixed). Links the session to the publishing participant |
| `content_scope` | No | Opaque identifier for content access context |
| `external_session_id` | No | Your own session identifier |
| `prior_session_ids` | No | Previous session UUIDs for multi-session attribution |

### POST /events

Each event carries its own `session_id`. A top-level `session_id` on the request body is shorthand for "apply this to every event in the batch." Either form is accepted.

`session_id` is **optional** in two cases:

- **Origin / edge / index emitters** at the retrieval conformance level - they have no session context and correlate via `content_telemetry_id` instead.
- **Engagement events bound to a click token** - networks and retailers reporting a `link_click` from a landing page can supply a `ctx_token` on the event in place of `session_id`. The server resolves the token to the session.

**Request - agent reporting a session**

```json
{
  "session_id": "550e8400-...",
  "events": [
    {
      "id": "event-uuid-1",
      "type": "content_retrieved",
      "timestamp": "2026-03-18T10:00:00Z",
      "source_role": "agent",
      "content_url": "https://wirecutter.com/reviews/best-headphones",
      "content_telemetry_id": "telemetry-correlation-uuid"
    },
    {
      "id": "event-uuid-2",
      "type": "content_cited",
      "timestamp": "2026-03-18T10:00:05Z",
      "source_role": "agent",
      "content_url": "https://wirecutter.com/reviews/best-headphones"
    }
  ]
}
```

**Request - marketplace retrieval (no session)**

```json
{
  "events": [
    {
      "id": "event-uuid-1",
      "type": "content_retrieved",
      "timestamp": "2026-03-18T10:00:00Z",
      "source_role": "index",
      "content_url": "https://example.com/article/best-headphones",
      "content_id": "doi:10.1000/example.123",
      "content_telemetry_id": "telemetry-correlation-uuid"
    }
  ]
}
```

**Request - network reporting a click via ctx**

```json
{
  "events": [
    {
      "id": "event-uuid-1",
      "type": "content_engaged",
      "timestamp": "2026-03-18T10:01:00Z",
      "source_role": "index",
      "content_url": "https://retailer.com/headphones/sony-wh1000xm5",
      "ctx_token": "ctx_abc123def456",
      "data": { "engagement_type": "link_click" }
    }
  ]
}
```

**Response**

```json
{
  "status": "ok",
  "events_created": 2
}
```

### POST /sessions/end

Close a session and (optionally) attach an outcome.

**Request**

```json
{
  "session_id": "550e8400-...",
  "outcome": {
    "type": "conversion",
    "value_amount": 34900,
    "currency": "USD"
  }
}
```

`value_amount` is in minor currency units (cents for USD). Outcome types: `"conversion"`, `"abandonment"`, `"browse"`.

### POST /sessions/bulk

Upload a complete session - start, events, and outcome - in a single request. Useful for batch reporting or replaying historical data.

The `/events` and `/sessions/*` request bodies above are a loose API envelope. The canonical archival and interchange representation of a complete session is the JSON session *document* defined by [`telemetry-session.json`](https://github.com/openattribution-org/telemetry/blob/main/telemetry-session.json) (with `document_type` and `schema_version`) - that is what the server materialises and what a conformance validator checks against. Report against the envelope; the document is the assembled result.

**Request**

```json
{
  "session_id": "550e8400-...",
  "agent_id": "shopping-assistant",
  "started_at": "2026-03-18T10:00:00Z",
  "ended_at": "2026-03-18T10:05:00Z",
  "events": [
    {
      "id": "event-uuid-1",
      "type": "content_retrieved",
      "timestamp": "2026-03-18T10:00:00Z",
      "source_role": "agent",
      "content_url": "https://wirecutter.com/reviews/best-headphones"
    }
  ],
  "outcome": {
    "type": "conversion",
    "value_amount": 34900,
    "currency": "USD"
  }
}
```

**Response**

```json
{
  "session_id": "550e8400-...",
  "events_created": 1,
  "outcome_recorded": true
}
```

---

## Event types

| Type | When | Source role |
|---|---|---|
| `content_retrieved` | Content fetched from source | origin, edge, index, or agent |
| `content_grounded` | Content loaded into the agent's context to inform the response | agent |
| `content_cited` | Content referenced in an agent's response | agent |
| `content_displayed` | Content shown to user in a card or sidebar | agent |
| `content_engaged` | User interacted with cited content | agent |
| `turn_started` | User initiated a conversation turn | agent |
| `turn_completed` | Agent finished responding | agent |
| `checkout_completed` (ACP extension) | Purchase completed with value. Defined in the [ACP commerce extension](https://github.com/openattribution-org/telemetry/tree/main/acp), not the core schema | index, agent |

The first seven rows are the core event types. `checkout_completed` is **not** core - it ships with the [ACP commerce extension](https://github.com/openattribution-org/telemetry/tree/main/acp) and is accepted on the same `/events` endpoint. Networks and marketplaces typically report it with `source_role: "index"` and a `ctx_token` in place of a `session_id`. Core-schema results are recorded as session outcomes (`conversion`, `browse`, `abandonment`).

## Source roles

The `source_role` field identifies who is reporting the event. Multiple observers can report the same retrieval from different vantage points - the server deduplicates via `content_telemetry_id`.

| Role | Reporter |
|---|---|
| `origin` | Content owner's web server (e.g. WordPress plugin) |
| `edge` | CDN or edge network (Cloudflare, Fastly, etc.) |
| `index` | Content marketplace or licensed content repository |
| `agent` | The AI agent itself |

---

## Identity and onboarding

Org creation is session-authed - API keys cannot create new principals. Sign in via the website (magic link or Google OAuth) and call these endpoints with your session cookie or `Authorization: Bearer {session-id}`.

### POST /api/v1/identity/organizations

Self-serve for `content_owner` only. Calls with `org_type: "platform"` or `"agent"` are rejected with a 403 pointing to the matching access-request endpoint below.

```json
{
  "name": "Acme Media",
  "org_type": "content_owner",
  "domain": "acme.example"
}
```

`platform` and `agent` orgs go through approval while the network is bedding in. Content owners stay self-serve. Both gated paths use the same submit-then-approve flow.

### POST /api/v1/identity/platform-access-requests

```json
{
  "org_name": "Acme Marketplace",
  "admin_email": "ops@acme.example",
  "use_case": "Affiliate network surfacing AI-cited content..."
}
```

**Response (201)**

```json
{
  "id": "uuid",
  "user_id": "uuid",
  "org_name": "Acme Marketplace",
  "admin_email": "ops@acme.example",
  "use_case": "...",
  "status": "pending",
  "created_at": "2026-05-10T12:00:00Z",
  "reviewed_at": null,
  "reviewed_by": null,
  "reviewer_note": null,
  "approved_org_id": null
}
```

List your own pending and reviewed requests with `GET /api/v1/identity/platform-access-requests`. Once an operator admin approves, `approved_org_id` is populated and you receive owner membership in the new platform org.

### POST /api/v1/identity/agent-access-requests

Identical body and response shape to the platform endpoint, written against the agent waitlist instead. List your own with `GET /api/v1/identity/agent-access-requests`.

---

## Health

| Endpoint | Auth | Purpose |
|---|---|---|
| `GET /health` | None | Returns `{"status": "ok"}` |
| `GET /ready` | None | Database connectivity check |

---

## Common integration recipes

### A measurement-partner dashboard

1. Register your platform org (sign up at `/signup/platform`; access is by approval) and obtain an `oat_pk` API key with `telemetry:read` scope.
2. Share your `organization_id` with content owners. They grant you access via dashboard or `POST https://api.openattribution.org/api/v1/identity/delegations`.
3. List delegations granted to you: `GET https://api.openattribution.org/api/v1/identity/delegations?role=grantee`.
4. For each delegation, call `GET https://api.openattribution.org/content-owners/summary?delegated_from={grantor_org_id}` to populate per-owner views.
5. Drill down with `/content-owners/events` and `/content-owners/urls` using the same `delegated_from` pattern.

### An agent reporting retrieval and citation

1. Get an `oat_pk` platform API key after agent-access approval.
2. `POST https://telemetry.openattribution.org/sessions/start` at the start of a user turn - capture the returned `session_id`.
3. `POST https://telemetry.openattribution.org/events` with one or more `content_retrieved` and `content_cited` events tied to the `session_id`.
4. `POST https://telemetry.openattribution.org/sessions/end` when the conversation closes (optionally with an outcome).
5. For outbound retailer/affiliate links, call `POST https://api.openattribution.org/click-tokens` and append `?ctx=...` to the URL.

### A content marketplace reporting retrievals

1. `POST /events` to `https://telemetry.openattribution.org/events` with `source_role: "index"` and a stable `content_telemetry_id` per logical retrieval.
2. No `session_id` required - the agent that consumed your content reports its own session.
3. Provide `content_id` (a stable identifier such as a DOI) alongside `content_url` so the server can dedupe across observers.
