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

# Usage API

> Query aggregated request counts, fee totals, and pipeline/model breakdowns for your app, powered by OpenMeter.

The Usage API is a read-only endpoint that exposes aggregated usage data for a developer application. It is backed by **OpenMeter** (`OPENMETER_URL` is required) — all responses include `"source": "openmeter"`. It is designed for **billing dashboards**, **cost analytics**, **per-user attribution**, and **pipeline/model breakdown** workflows.

All monetary values are expressed in **wei** as decimal strings, with USD micro equivalents available via `include=retail`.

## Authentication

Two auth modes are accepted. The tenant boundary is enforced identically in both: the `clientId` in the URL path must match the authenticated principal's app.

### Confidential client (recommended for server-to-server)

HTTP Basic auth with your M2M credentials:

```http theme={null}
GET /api/v1/apps/{clientId}/usage HTTP/1.1
Authorization: Basic base64(m2m_id:m2m_secret)
```

No additional scope is required beyond possessing valid M2M credentials — the endpoint only returns data for the authenticated client's own app.

### Provider dashboard session

A logged-in provider session whose user is the app's owner, a platform admin, or a `providerAdmins` team member may call the endpoint without Basic auth.

<Note>
  Requests that satisfy neither auth mode, or whose authenticated principal does not match the path `clientId`, receive **`404 Not Found`**. The endpoint deliberately does not distinguish "unauthenticated" from "not found" to avoid leaking app existence.
</Note>

## Endpoint

```http theme={null}
GET /api/v1/apps/{clientId}/usage
```

### Path parameters

| Parameter  | Type   | Description                                                                            |
| ---------- | ------ | -------------------------------------------------------------------------------------- |
| `clientId` | string | OAuth `client_id` of the developer app (`app_…`). Must match the authenticated client. |

### Query parameters

All query parameters are optional.

| Parameter          | Type                                                     | Default | Description                                                                                                   |
| ------------------ | -------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `startDate`        | ISO 8601 timestamp                                       | —       | Inclusive lower bound on record creation time.                                                                |
| `endDate`          | ISO 8601 timestamp                                       | —       | Inclusive upper bound on record creation time.                                                                |
| `groupBy`          | `none` \| `user` \| `pipeline_model` \| `daily_pipeline` | `none`  | Grouping dimension for the response.                                                                          |
| `userId`           | string                                                   | —       | Filter to a single user by their **internal** PymtHouse `endUserId`. Required for `daily_pipeline`.           |
| `gatewayRequestId` | string                                                   | —       | Filter billing events to a specific gateway request. May include `events` detail in the response.             |
| `include`          | `retail`                                                 | —       | When `retail`, adds `endUserBillableUsdMicros` and fiat estimates when the active plan has retail rate cards. |

**Date format:** `Date.parse`-compatible strings are accepted (e.g. `2026-01-01T00:00:00.000Z` or `2026-01-01`). Invalid values return `400 Bad Request`.

**`userId` vs `externalUserId`:** The `userId` parameter accepts the **internal** PymtHouse user id (`endUserId`), not your system's `externalUserId`. Resolve an `externalUserId` to an `endUserId` via a prior `groupBy=user` response.

### `groupBy` values

| Value            | Description                                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `none` (default) | App-level totals only.                                                                                                               |
| `user`           | Adds `byUser[]` — one entry per distinct end-user.                                                                                   |
| `pipeline_model` | Adds `byPipelineModel[]` — aggregated from validated `usage_billing_events` rows that have a full `pipeline` + `modelId` constraint. |
| `daily_pipeline` | Adds `byDailyPipeline[]` — OpenMeter DAY windows per pipeline/model. Requires `userId` filter.                                       |

## Response

### 200 OK

```json theme={null}
{
  "clientId": "app_f4c21e7ac5f35d3e91bfad7f",
  "source": "openmeter",
  "period": {
    "start": "2026-01-01T00:00:00.000Z",
    "end": "2026-12-31T23:59:59.999Z"
  },
  "totals": {
    "requestCount": 1423,
    "totalFeeWei": "128750000000000000",
    "totalFeeEth": "0.128750",
    "networkFeeUsdMicros": "386250000",
    "ownerChargeWei": "141625000000000000",
    "ownerChargeUsdMicros": "424875000",
    "platformFeeWei": "12875000000000000"
  },
  "byUser": [
    {
      "endUserId": "5d2b1234-uuid-...",
      "externalUserId": "user-123",
      "requestCount": 42,
      "feeWei": "3750000000000000"
    },
    {
      "endUserId": "unknown",
      "externalUserId": null,
      "requestCount": 7,
      "feeWei": "625000000000000"
    }
  ],
  "byPipelineModel": [
    {
      "pipeline": "text-to-image",
      "modelId": "stabilityai/sdxl",
      "requestCount": 820,
      "networkFeeUsdMicros": "246000000",
      "endUserBillableUsdMicros": "369000000"
    }
  ]
}
```

### Response fields

| Field                                        | Type           | Description                                                                                      |
| -------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------ |
| `clientId`                                   | string         | Echo of the path `clientId`.                                                                     |
| `source`                                     | `"openmeter"`  | Always present. Indicates usage reads from OpenMeter meters.                                     |
| `period.start`                               | string \| null | Echo of `startDate`, or `null` if omitted.                                                       |
| `period.end`                                 | string \| null | Echo of `endDate`, or `null` if omitted.                                                         |
| `totals.requestCount`                        | integer        | Total number of usage records matching the filter.                                               |
| `totals.totalFeeWei`                         | string         | Sum of all network fees in wei, as a base-10 decimal string.                                     |
| `totals.totalFeeEth`                         | string         | Decimal ETH equivalent of `totalFeeWei`.                                                         |
| `totals.networkFeeUsdMicros`                 | string         | Transaction-time USD micros for network cost.                                                    |
| `totals.ownerChargeWei`                      | string         | Network fee plus platform cut, in wei.                                                           |
| `totals.ownerChargeUsdMicros`                | string         | Transaction-time USD micros for owner charge.                                                    |
| `totals.platformFeeWei`                      | string         | PymtHouse platform cut in wei.                                                                   |
| `byUser`                                     | array          | Present only when `groupBy=user`. One entry per distinct user.                                   |
| `byUser[].endUserId`                         | string         | Internal PymtHouse user id, or `"unknown"` for unattributed records.                             |
| `byUser[].externalUserId`                    | string \| null | Your system's user identifier, when resolvable.                                                  |
| `byUser[].requestCount`                      | integer        | Requests attributed to this user.                                                                |
| `byUser[].feeWei`                            | string         | Network fees for this user in wei.                                                               |
| `byPipelineModel`                            | array          | Present only when `groupBy=pipeline_model`. Aggregated from validated `usage_billing_events`.    |
| `byPipelineModel[].pipeline`                 | string         | Pipeline name (e.g. `text-to-image`).                                                            |
| `byPipelineModel[].modelId`                  | string         | Model identifier (e.g. `stabilityai/sdxl`).                                                      |
| `byPipelineModel[].requestCount`             | integer        | Validated billing event count for this pipeline/model.                                           |
| `byPipelineModel[].networkFeeUsdMicros`      | string         | Network cost in USD micros.                                                                      |
| `byPipelineModel[].endUserBillableUsdMicros` | string \| null | Retail estimate (present when `include=retail` and the plan has rate cards).                     |
| `byDailyPipeline`                            | array          | Present only when `groupBy=daily_pipeline`. OpenMeter DAY window breakdowns (requires `userId`). |

<Warning>
  `totalFeeWei` and `feeWei` are decimal strings, not numbers. They can exceed `Number.MAX_SAFE_INTEGER`. Always parse them with a BigInt-capable library (e.g. `BigInt(feeWei)` in JavaScript, `viem`'s `formatEther` for display).
</Warning>

### The `"unknown"` bucket

Usage records without a resolvable `userId` are grouped under `endUserId: "unknown"` rather than silently dropped. This guarantees that `totals.requestCount` always equals the sum of `byUser[].requestCount` (including the `"unknown"` bucket) when `groupBy=user` is requested.

## Usage balance

Check a user's remaining entitlement balance before allowing access:

```http theme={null}
GET /api/v1/apps/{clientId}/usage/balance?externalUserId={externalUserId}
Authorization: Basic base64(m2m_id:m2m_secret)
```

Response:

```json theme={null}
{
  "balanceUsdMicros": "4200000",
  "hasAccess": true,
  "remainingUsdMicros": "4200000",
  "consumedUsdMicros": "800000",
  "lifetimeGrantedUsdMicros": "5000000"
}
```

| Field                      | Description                                                                     |
| -------------------------- | ------------------------------------------------------------------------------- |
| `balanceUsdMicros`         | Current entitlement balance in USD micros from OpenMeter.                       |
| `hasAccess`                | `true` when the user has remaining balance from their active plan subscription. |
| `remainingUsdMicros`       | Remaining balance in USD micros.                                                |
| `consumedUsdMicros`        | Consumed balance in USD micros this period.                                     |
| `lifetimeGrantedUsdMicros` | Total granted allowance (Starter + manual top-ups).                             |

See [Allowances](/integration/allowances) for granting additional balance.

## Examples

```bash theme={null}
export BASE_URL="http://localhost:3001"    # or your production URL
export CLIENT_ID="app_yourClientId"
export M2M_ID="m2m_yourClientId"
export M2M_SECRET="pmth_cs_yourSecret"
```

### App-level totals (all time)

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage" | jq .
```

### Per-user breakdown

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage?groupBy=user" | jq .
```

### Pipeline/model breakdown

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage?groupBy=pipeline_model" | jq .
```

### Pipeline/model breakdown with retail estimates

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage?groupBy=pipeline_model&include=retail" | jq .
```

### Month-to-date window

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage\
?startDate=2026-04-01T00:00:00.000Z\
&endDate=2026-04-30T23:59:59.999Z" | jq .
```

### Usage balance for a user

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage/balance?externalUserId=user-123" | jq .
```

### Filter to a single gateway request

```bash theme={null}
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage?gatewayRequestId=job-abc123" | jq .
```

### Format wei as ETH in a shell script

```bash theme={null}
RESPONSE=$(curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  "${BASE_URL}/api/v1/apps/${CLIENT_ID}/usage")

TOTAL_WEI=$(echo "${RESPONSE}" | jq -r '.totals.totalFeeWei')
node -e "const w = BigInt('${TOTAL_WEI}'); const eth = Number(w) / 1e18; console.log(eth.toFixed(6) + ' ETH')"
```

### SDK helper: usage by external user

```ts theme={null}
import { summarizeUsageForExternalUser } from "@pymthouse/builder-sdk";

const usage = await client.getUsage({ groupBy: "user", startDate, endDate });
const summary = summarizeUsageForExternalUser(usage, "user-123");
// summary.requestCount, summary.feeWei (wei string)
```

## Error responses

| Status            | Condition                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request` | `startDate` or `endDate` is not parseable by `Date.parse`.                                                            |
| `404 Not Found`   | No authenticated principal, credentials valid but for a different app, or `clientId` does not resolve to a known app. |

## Security boundaries

* Tenant isolation is enforced by matching the authenticated client's app to the path `clientId`. A valid credential for a different app returns `404`.
* Provider sessions must be the app owner, a platform admin, or a recorded `providerAdmins` team member.
* No secrets, signer material, per-request payloads, or customer PII are returned.
* Confidential client secrets must stay server-side. Do not call this endpoint from the browser with Basic auth.

## Key design decisions

1. **OpenMeter-authoritative.** All usage reads come from OpenMeter meters (`network_fee_usd_micros`, `signed_ticket_count`). The `"source": "openmeter"` field is always present. Allowance balance reads use OpenMeter entitlement APIs, never Postgres.
2. **`pipeline_model` grouping from billing events.** The `groupBy=pipeline_model` dimension aggregates from validated `usage_billing_events` rows — records that have a full `pipeline` + `modelId` constraint from the gateway payment envelope. Records without this constraint appear only in `totals`.
3. **Async metering.** Signing hot-path performance is not impacted by metering writes. go-livepeer emits events to Kafka; the OpenMeter collector ingests them asynchronously.
4. **`404` for all auth and tenant-mismatch failures.** Collapsing `401`, `403`, and "wrong app" into `404` prevents enumeration of valid `client_id`s.
5. **Retail estimates are opt-in.** `include=retail` triggers retail rate computation from the active plan's rate cards. Authoritative invoicing remains OpenMeter after plan sync.

## Implementation tasks

* Parse `totalFeeWei` and `feeWei` with `BigInt` before any arithmetic.
* When displaying fees in your dashboard, convert from wei using a safe formatter (e.g. `viem`'s `formatEther`).
* For reconciliation workflows, always supply explicit `startDate`/`endDate` bounds.
* Use `groupBy=pipeline_model` to build per-pipeline attribution dashboards. Only requests with full `pipeline` + `modelId` constraints appear in this dimension.
* Check `getUsageBalance()` (or `GET .../usage/balance`) before allowing user actions that consume entitlement.
* Rotate M2M client secrets periodically via the credentials endpoint.
