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

Only a SHA-256 hash of each key is stored. Rotation is currently by
request (info@tahosasystems.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

Managed connections replace their (connection, window) slice atomically on
every sync. For your own pushes, the same practice applies: if you need to
restate a day, re-push the whole day's aggregates for that
`apiConnection` after deleting nothing; duplicate submission of the same
window will double-count, so push each window once or contact us to reset
a window.

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