Skip to content

Usage & Quotas

These endpoints power usage-based billing: your app reports usage events per metric, Bridge aggregates them into the workspace’s current billing period, compares them against the plan’s quotas, and (for metered metrics) replicates the rollups to Stripe. A workspace is your app’s customer account (also called a tenant); you manage plans and quotas in Control Center, Bridge’s admin dashboard.

Most apps never call these endpoints directly. The framework SDKs wrap them: bridge.usage.report(metric) batches events to the ingest endpoint through a durable, crash-safe queue, and useBridge().quota(metric) hydrates from the quota endpoint and then stays live over the quota.updated push channel. Call REST directly when you report usage from a backend without a Bridge SDK.

Authentication. Every endpoint on this page acts on the signed-in user’s own workspace and authenticates with that user’s access token, plus your app ID:

Authorization: Bearer <USER_ACCESS_TOKEN>
x-app-id: <YOUR_APP_ID>

The workspace is always derived from the token, never from the request body. The two operator endpoints that take a workspaceId path parameter additionally require that it matches the caller’s own workspace, otherwise they return HTTP 403.

Two quota policies exist, set per metric on the plan:

  • hard: the metric has a cap. When usage crosses the limit, the linked entitlement flips to false and the SDK gates the feature.
  • metered: overage bills automatically through Stripe. Nothing is ever blocked.

See Usage limits for the SDK-side guide and how to configure quotas on a plan.


Record one usage event for a metric on the caller’s workspace. Ingest is always-accept: the server never rejects an event because of plan state or quota level (no HTTP 402), so your product code can fire and forget. Enforcement of hard limits happens on the entitlement layer, not here.

Duplicate submissions with the same idempotencyKey are deduplicated: the event is stored once and the aggregate is bumped once. Retries are therefore always safe.

POST https://api.thebridge.dev/usage/ingest

Body Parameters

ParameterTypeRequiredDescription
metricstringRequiredThe metric key to bump, e.g. ai_completions. Metric keys are defined on the plan's quotas in Control Center
valueintegerOptionalHow much to add. Integer >= 1. Defaults to 1
idempotencyKeystringOptionalUnique key for safe retries. Sending the same key twice stores the event once and returns deduped: true. Auto-generated when omitted

| Field | Type | Description | |---|---|---| | accepted | boolean | Always true when the request validates | | deduped | boolean | true when the idempotencyKey was seen before; the aggregate was not bumped again | | eventId | string | The stored usage event’s ID (the original event’s ID on a dedupe) |

Request example

curl --request POST 'https://api.thebridge.dev/usage/ingest' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metric": "ai_completions",
  "value": 1,
  "idempotencyKey": "0d6f2c9e-6d1b-4f4e-9d1a-6a1f0b8c2e77"
}'

Response example:

{
  "accepted": true,
  "deduped": false,
  "eventId": "665f2c9e6d1b4f4e9d1a6a1f"
}

The SDKs generate the idempotency key at enqueue time and persist it with the queued event, so a crash-restart replay sends the exact same key and the server drops the duplicate. Do the same if you build your own retry queue.


The live quota snapshot for one metric on the caller’s workspace. This is the SDK’s initial-hydration endpoint: it reads this once, then keeps the value current from quota.updated pushes on the workspace channel.

Returns null (with HTTP 200) when the workspace’s plan has no quota configured for the metric. The SDK treats that as unmetered.

GET https://api.thebridge.dev/usage/quota/METRIC

Path Parameters

ParameterTypeRequiredDescription
METRICstringRequiredThe metric key to read, e.g. ai_completions

| Field | Type | Description | |---|---|---| | metric | string | The metric key | | used | number | Usage recorded in the current billing period | | limit | number | The plan’s quota for this metric | | remaining | number | limit - used (can go negative on metered metrics) | | warningLevel | string \| null | null, approaching (>= 80% of limit), or critical (>= 95% or over the limit) | | policy | string | hard (entitlement flips to false at cap) or metered (overage bills through Stripe) |

Request example

curl --request GET 'https://api.thebridge.dev/usage/quota/ai_completions' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

{
  "metric": "ai_completions",
  "used": 820,
  "limit": 1000,
  "remaining": 180,
  "warningLevel": "approaching",
  "policy": "hard"
}

All metrics tracked for a workspace in the current billing period, with quota fields merged in where the plan defines one. This is what powers the usage view in Control Center’s admin UI.

The caller must belong to the workspace in the path: a mismatch returns HTTP 403.

GET https://api.thebridge.dev/usage/WORKSPACE_ID

Path Parameters

ParameterTypeRequiredDescription
WORKSPACE_IDstringRequiredThe workspace to read. Must be the caller's own workspace

| Field | Type | Description | |---|---|---| | workspaceId | string | The workspace ID | | periodStart | string | ISO-8601 start of the current billing period | | periodEnd | string | ISO-8601 end of the current billing period | | metrics | array | One entry per tracked metric (see below) |

Each entry in metrics:

| Field | Type | Description | |---|---|---| | metric | string | The metric key | | used | number | Usage recorded this period | | limit | number? | The plan’s quota. Omitted when the plan has no quota for the metric | | remaining | number? | limit - used. Omitted without a quota | | warningLevel | string? | approaching, critical, or null. Omitted without a quota | | policy | string? | hard or metered. Omitted without a quota | | lastSyncedAt | string? | When this metric last synced to Stripe, or null | | lastSyncStatus | string? | Result of the last Stripe sync, or null | | lastSyncError | string? | Error message from the last failed sync, or null |

Request example

curl --request GET 'https://api.thebridge.dev/usage/650cd8510ccba777cc9623d0' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

{
  "workspaceId": "650cd8510ccba777cc9623d0",
  "periodStart": "2026-07-01T00:00:00.000Z",
  "periodEnd": "2026-08-01T00:00:00.000Z",
  "metrics": [
    {
      "metric": "ai_completions",
      "used": 820,
      "limit": 1000,
      "remaining": 180,
      "warningLevel": "approaching",
      "policy": "hard",
      "lastSyncedAt": null,
      "lastSyncStatus": null,
      "lastSyncError": null
    },
    {
      "metric": "api_requests",
      "used": 15230,
      "limit": 10000,
      "remaining": -5230,
      "warningLevel": "critical",
      "policy": "metered",
      "lastSyncedAt": "2026-07-07T02:00:00.000Z",
      "lastSyncStatus": "ok",
      "lastSyncError": null
    }
  ]
}

Push the workspace’s current-period usage rollups to Stripe immediately instead of waiting for the scheduled sync. The sync uses action: 'set' semantics on Stripe usage records, so re-syncing the same period is idempotent: it replaces the Stripe-side quantity rather than adding to it.

The caller must belong to the workspace in the path: a mismatch returns HTTP 403. Workspaces without a Stripe subscription sync zero metrics.

POST https://api.thebridge.dev/usage/sync/WORKSPACE_ID

Path Parameters

ParameterTypeRequiredDescription
WORKSPACE_IDstringRequiredThe workspace to sync. Must be the caller's own workspace

| Field | Type | Description | |---|---|---| | syncedMetrics | number | How many metrics were pushed to Stripe | | failed | string[] | Metric keys that failed to sync this run |

Request example

curl --request POST 'https://api.thebridge.dev/usage/sync/650cd8510ccba777cc9623d0' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

{
  "syncedMetrics": 2,
  "failed": []
}