# Metering API reference

Base URL: `https://r3ckon.com`

The metering API accepts usage **metadata** and nothing else. It exists so
gateways, self-hosted stacks, and internal pipelines can feed the ledger
without a managed provider connection.

## Authentication

Every request carries a bearer key:

```
Authorization: Bearer aiok_ingest_…
```

Keys are account-scoped and role-scoped:

- `aiok_admin_…`: full console access plus every API below. This is the key
  revealed once at signup.
- `aiok_ingest_…`: may push usage (both ingest endpoints) but cannot read
  the console or manage the account.
- `aiok_sdk_…`: may push to the SDK feed only.
- `aiok_read_…`: read-only. May call the account read surfaces (the overview
  API, certificate PDFs, and every MCP tool) and can neither push usage nor
  manage anything. The only key that belongs in an MCP client config.

Only a SHA-256 hash of each key is stored. Rotation is currently by
request (info@r3ckon.com) after billing-email verification.

## POST /api/v1/ingest/authoritative

The billing-grade feed: daily (or coarser) aggregates that estimates,
retirements, and invoices are computed from.

```bash
curl -X POST https://r3ckon.com/api/v1/ingest/authoritative \
  -H "Authorization: Bearer $R3CKON_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "apiConnection": "anthropic",
      "model": "claude-sonnet-5",
      "region": "us-east-1",
      "inputTokens": 18400000,
      "cachedInputTokens": 12100000,
      "outputTokens": 2300000,
      "reasoningTokens": 410000,
      "serviceTier": "standard",
      "timestamp": "2026-08-04T00:00:00Z"
    }]
  }'
```

Response: `{ "accepted": 1 }`

### Event fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `model` | string (1..200) | yes | The provider's model identifier, verbatim. Unknown models estimate under the registry's most conservative class. |
| `inputTokens` | integer >= 0 | yes | The FULL input stream, including any cached portion. |
| `outputTokens` | integer >= 0 | yes | Completion tokens. |
| `region` | string (1..64) | yes | Cloud region code when known (e.g. `us-east-1`). Unrecognized values estimate under conservative unknown-facility parameters. A recognized code selects that cloud's published facility profile, which can lower the estimate relative to the unknown default, so report the region truthfully; region values are recorded with the event and are auditable. |
| `timestamp` | ISO 8601 datetime with offset | yes | The period the aggregate covers (use the day start for daily buckets). |
| `cachedInputTokens` | integer >= 0 | no | The cached SUBSET of `inputTokens`. Omitted means unreported, not zero. |
| `reasoningTokens` | integer >= 0 | no | The reasoning SUBSET of `outputTokens`, where the provider reports it (never billed twice). Omitted means unreported; for reasoning-class models that triggers the methodology's conservative hidden-reasoning multiplier. |
| `serviceTier` | `standard` \| `batch` \| `priority` | no | Batch and priority tiers carry their own energy multipliers. |
| `apiConnection` | string (1..64) | no | Which source produced this usage; groups the console's per-API filters. Accounts support up to 50 distinct connections. |
| `endUserPseudoId` | string (<=128) | no | An opaque pseudonymous id if you segment end users. Never send anything identifying. |

### The content boundary (HTTP 422)

Every payload is deep-scanned before parsing. If any key anywhere in the
JSON matches a content-shaped name (`prompt`, `completion`, `content`,
`messages`, `message`, `text`, `input`, `output`, `system`, `body`,
`response`, `query`, `answer`, `transcript`), the request is rejected with
`422` and nothing is stored:

```json
{ "error": "Ingest rejected: payload contains content-shaped key \"prompt\". This platform is metadata-only and must never receive prompt or completion content." }
```

This is an architectural invariant, enforced by tests, not a policy.

### Idempotency and re-submission

By default a POST ADDS the events you send. To restate a period instead,
include a `replace` object naming the window you own:

```json
{
  "replace": { "apiConnection": "anthropic-prod", "start": "2026-08-05", "end": "2026-08-08" },
  "events": [ … ]
}
```

Everything previously recorded for that `apiConnection` in that window
(start inclusive, end exclusive) is removed, then your events are stored.
This makes an overlapping or repeated cron safe: re-running the same
window restates it rather than doubling it, and a missed day is fixed by
running again with a wider window.

The replace is scoped to the one `apiConnection` you name, so it can never
remove usage that arrived from a different source. Send `replace` on the
first request of a multi-batch upload only; later batches would otherwise
delete the rows the earlier ones just wrote.

The sync scripts do this for you.

### Errors

| Status | Meaning |
|---|---|
| `400` | Malformed JSON, missing `events` array, or field validation failure (message says which). |
| `401` | Missing or unknown bearer key, or a role without ingest rights. |
| `422` | Content boundary violation (see above). |

## POST /api/v1/usage-events

The SDK feed: identical body and validation, accepted from `sdk`,
`ingest`, or `admin` keys. It powers live views and is **never billed
from**; billing and retirement always come from the authoritative feed.

## Account APIs

The console is built on account-scoped APIs authenticated with your
`aiok_admin_…` key. The stable, supported surface today:

- `GET /api/admin/overview`: everything the dashboard shows, as JSON:
  monthly footprint with P05/P50/P90/P95 per category, retirements,
  credit position, invoices, certificates, connections, filters.
- `GET /api/admin/connections` / `POST` (create) /
  `DELETE /api/admin/connections/{id}` /
  `POST /api/admin/connections/{id}/sync` with
  `{ "periodStart": "YYYY-MM-DD", "periodEnd": "YYYY-MM-DD" }`.
- `GET /api/admin/certificates/{id}/pdf`: the certificate PDF.
- `GET /api/admin/exports/{name}`: reporting CSVs (water restoration
  summary, CDP Water and ESRS E3 inputs).
- `POST /api/admin/billing-portal`: a Stripe customer portal session URL.

These return JSON errors in the same `{ "error": "…" }` shape. Endpoints
not listed here (reconcile, seeding) are operator-only and may change.
