# Usage, quotas & webhooks

> After every session your backend learns what it consumed, for which user, and why it ended — by the sessions API (pull) and signed webhooks (push), on every plan. Plus Quotas: fixed on Free, adjustable on Starter, programmable per session on Growth.

After every voice session your backend can learn what it consumed — how many minutes, for which of your users, and how it ended — two ways: **pull** (a sessions API you call when you like) and **push** (a signed webhook we send the moment a session ends). Both ship on **every plan**, Free included — you wire and test the integration before you pay. You are billed in minutes, so you get minutes: no tokens, no provider names, no per-minute cost.

## The session id

The mint response now carries a `sessionId` — an opaque `ses_…` handle. Store it against your user; it is the one identifier for the session everywhere you can see it (the mint response, the record, the API, the webhook).

**`the mint response`**

```ts
{ token, serverUrl, cloudUrl, language, supportedLanguages,
  sessionId: "ses_01J9…",              // store this
  session: { budgetMinutes: 7, closing: "cut", region: "europe" } }  // what actually applied
```

## The record

Both the pull API and the push webhook return the same object:

**`the session record`**

```ts
{ sessionId, userId, startedAt, endedAt, billedMinutes, turns,
  endReason: "complete" | "user_away" | "out_of_credit"
           | "session_limit" | "daily_limit" | "budget_limit" | "error",
  region, status: "recorded" | "unrecorded",
  session: { budgetMinutes?, closing?, region? } }
```

`billedMinutes` is `null` (never `0`) for a session that appears in the transport log but never wrote a record (`status: "unrecorded"`).

## Pull: the sessions API

Server-to-server, on the same secret `x-api-key` as `/session` (never from the browser). Fetch one session, or list them:

**`GET /sessions`**

```
GET {base}/sessions/{sessionId}       → the record above
GET {base}/sessions?userId=&from=&to=&cursor=&limit=
    → { sessions: [ …records… ], nextCursor: "…" | null }
      newest first · limit 1–100 (default 50) · from/to are ISO-8601 instants on startedAt
```

## Push: webhooks

Configure one endpoint under [Settings → Webhooks](/settings/webhooks) (every plan). We `POST` a signed event the moment a session ends (`session.ended`, `data` = the record) and when your balance runs low (`credits.low`). Failed deliveries retry with backoff (1 min, 5 min, 30 min, 2 h, 12 h); five consecutive failed events auto-disable the endpoint (we email you, re-enable in the portal). Verify the signature with `verifyWebhook` — read the **raw** body:

**`app/api/coworkkit/webhook/route.ts`**

```ts
import { verifyWebhook } from "@coworkkit/server";

export async function POST(req: Request) {
  const raw = await req.text(); // the RAW bytes — a re-serialised body won't match
  const event = verifyWebhook(raw, req.headers.get("coworkkit-signature"), process.env.COWORKKIT_WEBHOOK_SECRET!);
  if (event.type === "session.ended") {
    // idempotent on sessionId — deliveries are at-least-once
    await db.deductMinutes(event.data.userId, event.data.billedMinutes, event.data.sessionId);
  }
  return new Response("ok");
}
```

## Quotas — keep one user from draining your balance

A *quota* is a standing per-user wall; a *budget* is what your code hands one session. Three depths, one per tier:

- **Fixed** (Free) — 30 min per session, 60 min per user per day. We protect; you can't change them.
- **Adjustable** (Starter) — tune both walls (or switch them off) in the coworker's Quotas card. One allowance for every user.
- **Programmable** (Growth) — pass `session.budgetMinutes` at mint from your own code, so different users get different allowances.

The layers stack; the tightest wins. When a wall is hit the user hears a short goodbye (or a clean cut, per `session.closing`) — never "out of credit"; the record's `endReason` says which wall it was, and a refused mint returns `reason: "daily_limit"`.

## The session container echoes what applied

Read the response's `session` to know what will actually govern the session: `budgetMinutes` is the ceiling that will bind (absent when unbounded), `closing` is `"goodbye"` (default) or `"cut"`, and `region` is the basin honoured (present only when a preference was). On Free and Starter the plan's wall is echoed whatever you requested — nothing is refused, the response just tells you what applies.
