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

# Conversion API and client tooling

> Use the REST API, signed OpenAPI contract, generated SDKs, and command-line clients for Signals operations.

PlainRouter exposes a server-side Signals Conversion API at `https://plainrouter.com/api/v1`. The signed OpenAPI contract describes the same operations used by generated SDKs and command-line clients.

For one fetchable comparison of REST, SDK, CLI, MCP, and management credentials,
see [Authentication and clients](/docs/auth).

## Choose an interface

| Interface                          | Use it when                                                                     |
| ---------------------------------- | ------------------------------------------------------------------------------- |
| REST API                           | You want direct control over HTTP requests and responses.                       |
| [TypeScript SDK](/docs/sdk/typescript)  | You want generated types, Zod schemas, and methods in Node.js application code. |
| [Python SDK](/docs/sdk/python)          | You want generated synchronous and asynchronous operations in Python.           |
| [Go SDK](/docs/sdk/go)                  | You want a generated client for a Go service.                                   |
| [PlainRouter CLI](/docs/cli/quickstart) | You want to test an operation or run an operational script from a terminal.     |
| OpenAPI contract                   | You want to inspect the complete schema or generate another compatible client.  |

The signed REST contract, TypeScript SDK, Python SDK, Go SDK, and CLI are published at version `0.5.0`. See [Client releases](#client-releases) for the CLI's command boundary.

## Authenticate

Every request needs the per-Signal tracker secret issued during tracker provisioning:

```http theme={null}
Authorization: Bearer YOUR_SIGNAL_TRACKER_SECRET
Accept: application/json
Content-Type: application/json
```

Keep this secret on your server or in your CLI's secure credential store. Never put it in browser code, source control, prompts, or logs.

The Conversion API does not accept an MCP OAuth token or a [workspace token](/docs/actions/workspace-tokens). Those credentials authorize agent tools, not a Signal tracker.

## Submit an event

Send a stable `event_id` so retries are idempotent. Money values use decimal strings to preserve their exact representation.

```bash theme={null}
curl https://plainrouter.com/api/v1/events \
  --request POST \
  --header "Authorization: Bearer $PLAINROUTER_SIGNAL_SECRET" \
  --header "Content-Type: application/json" \
  --data '{
    "event_id": "order-123",
    "event_name": "Purchase",
    "event_time": 1786272000,
    "event_source": "https://shop.example/thank-you",
    "action_source": "website",
    "consent_basis": "consent",
    "consent": {
      "ad_storage": "granted",
      "ad_user_data": "granted",
      "ad_personalization": "granted",
      "source": "checkout-consent"
    },
    "value_data": {
      "value": "49.90",
      "currency": "EUR",
      "order_id": "order-123"
    }
  }'
```

A new event returns HTTP `202`. A retry with the same event ID returns HTTP `200` with `duplicate: true`.

```json theme={null}
{
  "event_id": "order-123",
  "duplicate": false
}
```

For browser consent and visitor joins, see [Track events and consent](/docs/signals/track-events).

Every `POST /events` request must include `consent_basis`. Use `consent` only with granted downstream advertising state. `legitimate_interest` is restricted to allowlisted lifecycle events and does not create an attributed advertising join.

## Verify Signal ingestion

The verification operation proves that the authenticated Signal can write to the event ledger without sending identity or destination data:

```bash theme={null}
curl https://plainrouter.com/api/v1/verification-events \
  --request POST \
  --header "Authorization: Bearer $PLAINROUTER_SIGNAL_SECRET" \
  --header "Accept: application/json"
```

A new verification returns `202`; a repeat returns `200` with `duplicate: true`. The request has no body. See [Verify Signal ingestion](/docs/signals/verify-ingestion) for the onboarding workflow and MCP alternative.

## Available operations

SDKs and CLI clients derive their method and command names from these semantic operation IDs:

| Operation ID              | Method and path                                  | Purpose                                                |
| ------------------------- | ------------------------------------------------ | ------------------------------------------------------ |
| `createEvent`             | `POST /events`                                   | Submit an idempotent conversion event.                 |
| `verifySignalIngestion`   | `POST /verification-events`                      | Write the identity-free onboarding verification event. |
| `getEvent`                | `GET /events/{event}`                            | Retrieve an event, lineage, and delivery trace.        |
| `listEvents`              | `GET /dashboard/events`                          | List recent events and delivery metrics.               |
| `setDestinationTestMode`  | `PATCH /destinations/{destination}/test-mode`    | Enable or disable Meta Test Events mode.               |
| `sendTestPurchase`        | `POST /destinations/{destination}/test-purchase` | Send a controlled test purchase.                       |
| `replayDeliveries`        | `POST /deliveries/replay`                        | Replay eligible recent deliveries.                     |
| `getReconciliationReport` | `GET /reports/reconciliation`                    | Retrieve reconciliation results for a date.            |
| `getEmqReport`            | `GET /reports/emq`                               | Retrieve recent Event Match Quality snapshots.         |
| `deleteUserData`          | `DELETE /user-data`                              | Delete data matching a hashed user identifier.         |

Resource IDs remain scoped to the authenticated Signal tracker. A client cannot use its secret to read another tracker's events or destinations.

## Use the signed OpenAPI contract

Download or import the canonical contract from:

```text theme={null}
https://plainrouter.com/openapi.json
```

The document uses OpenAPI `3.1.0`, publishes API version `0.5.0`, and includes `x-signed: true`. PlainRouter serves the reviewed bytes with an ETag so automated tooling can detect a new contract without downloading an unchanged document.

Generate clients only from a signed document. Pin the generated SDK or CLI version in production and review contract changes before updating it.

## Client releases

The TypeScript package [`@plainrouter/sdk@0.5.0`](https://www.npmjs.com/package/@plainrouter/sdk), Python package [`plainrouter==0.5.0`](https://pypi.org/project/plainrouter/), and Go module [`github.com/plainrouter/sdk-go@v0.5.0`](https://github.com/plainrouter/sdk-go) are generated from the signed ten-operation contract. All three expose ingestion verification and the required event consent fields.

The CLI [`@plainrouter/cli@0.5.0`](https://www.npmjs.com/package/@plainrouter/cli) uses the current TypeScript SDK and can send the consent-aware event shape. Install it with `npm install --global @plainrouter/cli@0.5.0` or `brew install plainrouter/tap/plainrouter`. It exposes nine terminal commands but does not add a command for `verifySignalIngestion`; use an SDK, direct REST, or MCP for that operation.

## Handle errors

| Status | Meaning                                                             |
| ------ | ------------------------------------------------------------------- |
| `401`  | The tracker secret is missing or invalid.                           |
| `404`  | The requested event or destination does not belong to this tracker. |
| `422`  | Request validation failed.                                          |
| `502`  | Meta did not accept a controlled test purchase.                     |

Validation responses include a message and field-level errors. Treat `401` as a stopped credential and avoid retrying it indefinitely.
