URL: https://wattfare.com/docs/api-reference
# HTTP API

> The raw REST API the SDK wraps. You rarely call this directly — the SDK handles auth, token caching, and error typing — but it's here for debugging, non-JS stacks, and custom integrations.

> **Base URL**
>
> All endpoints live under `https://wattfare.com/api/v1/`. The proxy is OpenAI-compatible, so existing OpenAI client libraries can talk to `/chat/completions` with the right headers.

## Authentication

Two bearer schemes, for two trust levels:

| Scheme | Header | Used by |
| --- | --- | --- |
| **Secret key** | `Authorization: Bearer sk_live_{appId}_{secret}` | Server-to-server: sessions, status, chat completions |
| **Session token** | `Authorization: Bearer {jwt}` | Browser: the `/connections` endpoints (via the SDK) |

Session tokens are JWTs with a 10-minute TTL carrying the app id, your user id, and the requested limits. Mint them with `POST /sessions`; the SDK caches and refreshes them for you.

## POST /sessions

Mints a frontend session token. Requires secret-key auth.

```http
POST /api/v1/sessions
Authorization: Bearer sk_live_{appId}_{secret}
Content-Type: application/json

{
  "appUserId": "user_123",
  "requestLimit": { "monthlyUsd": 20 }   // optional
}
```

```json
200 OK
{
  "token": "eyJhbGciOi…",   // short-lived JWT for the frontend
  "expiresAt": 1718400000   // unix seconds (default TTL: 10 min)
}
```

Rate limited per app via `RL_SESSIONS` — 120 requests/minute.

## /connections

The consent surface, authed with a **session token** (not the secret key). The SDK's `connect()`, `status()`, and `disconnect()` map onto these.

```http
# Approve / update — body { monthlyUsd? }, returns ConnectionStatus
POST   /api/v1/connections     Authorization: Bearer {session-jwt}

# Read current status
GET    /api/v1/connections     Authorization: Bearer {session-jwt}

# Revoke
DELETE /api/v1/connections     Authorization: Bearer {session-jwt}
```

| Method | Body | Returns |
| --- | --- | --- |
| POST | `{ monthlyUsd? }` | ConnectionStatus — approve or update the cap |
| GET | — | ConnectionStatus |
| DELETE | — | 204 — revoke the connection |

## GET /status

Server-side connection check, keyed by your user id. Requires secret-key auth.

```http
GET /api/v1/status?appUserId=user_123
Authorization: Bearer sk_live_{appId}_{secret}
```

```json
200 OK
{
  "connected": true,
  "limits":   { "monthlyUsd": 10 },
  "usage":    { "monthlyUsd": 3.20 },
  "remainingUsd": 6.80
}
```

## POST /chat/completions

The OpenAI-compatible inference proxy. Requires secret-key auth **and** the `x-wattfare-user` header so Wattfare resolves the right connection and budget.

```http
POST /api/v1/chat/completions
Authorization: Bearer sk_live_{appId}_{secret}
x-wattfare-user: user_123
Content-Type: application/json

{
  "model": "openai/gpt-4o-mini",
  "messages": [{ "role": "user", "content": "Hello" }],
  "stream": true
}
```

Responds with the standard OpenAI chat-completion format — JSON, or an SSE stream when `"stream": true`. Usage is metered automatically: cost is read from the provider's usage object and stored per-user, per-period. The stream is teed, never buffered.

```bash
curl https://wattfare.com/api/v1/chat/completions \
  -H "Authorization: Bearer $WATTFARE_SECRET_KEY" \
  -H "x-wattfare-user: user_123" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [{ "role": "user", "content": "Say hi" }]
  }'
```

> **Note**
>
> `model` accepts any [OpenRouter model id](https://openrouter.ai/models) — hundreds across Anthropic, OpenAI, Google, Meta, and open-weight providers.

## One-time grants

For apps without a user system, grants provide a simpler flow: the browser requests a `grantToken` via the consent popup, and the backend redeems it with the `x-wattfare-grant` header. See [One-time grants](https://wattfare.com/docs/grants) for endpoints and examples.

## Errors

Every error responds with the same body shape and a meaningful status:

```json
{ "error": { "code": "budget_exceeded", "message": "Spending cap reached for this period." } }
```

| Status | code | Meaning |
| --- | --- | --- |
| 400 | invalid_request | Malformed body or parameters. |
| 401 | auth | Missing/invalid secret key or session token. |
| 402 | not_connected | User hasn't connected a budget. |
| 402 | budget_exceeded | Monthly cap reached. |
| 402 | grant_invalid | The grant token is unknown, expired, fully used, or over its dollar cap. |
| 402 | funding_invalid | User's funding source rejected upstream (reconnect needed). |
| 429 | rate_limited | Per-user inference limit — 30 requests/min per app-user. |
| 502 | upstream | The model provider returned an error. |

## Rate limits

| Limiter | Scope | Limit |
| --- | --- | --- |
| RL_SESSIONS | per app | 120 / minute |
| RL_CHAT | per app + user | 30 / minute |

> **Prefer the SDK**
>
> The SDK already implements token caching, the popup origin handshake, streaming, and typed error recovery over these endpoints. Hand-rolling against the raw API means re-implementing all of that — only do it when the SDK genuinely can't run in your environment.
