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

# Billing, usage, and subscription state

> Read live credit balances and subscription state from a single endpoint, and accept RevenueCat subscription lifecycle events via signed webhook.

The billing endpoints expose the current user's credit balance and subscription state, and accept lifecycle events from RevenueCat. iOS reads `GET /v1/usage` on every app launch and after every paywall presentation — it is the single source of truth for "what does this user have access to right now".

## Get current usage

Returns the current monthly billing period, the user's tier with caps, current consumption, and remaining allowance.

Rate limit: **120 requests per minute**.

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.manticscore.com/v1/usage \
    -H "Authorization: Bearer <session_token>"
  ```

  ```python Python theme={null}
  import httpx

  resp = httpx.get(
      "https://api.manticscore.com/v1/usage",
      headers={"Authorization": f"Bearer {session_token}"},
  )
  data = resp.json()
  print(f"Tier: {data['tier']['id']}")
  print(f"Credits: {data['remaining']['credits']} remaining")
  ```

  ```swift Swift theme={null}
  let url = URL(string: "https://api.manticscore.com/v1/usage")!
  var request = URLRequest(url: url)
  request.setValue("Bearer \(sessionToken)", forHTTPHeaderField: "Authorization")
  let (data, _) = try await URLSession.shared.data(for: request)
  ```
</CodeGroup>

```json 200 response theme={null}
{
  "period": {
    "key": "2026-05",
    "resets_at": "2026-06-01T00:00:00+00:00"
  },
  "tier": {
    "id": "free",
    "product_id": null,
    "caps": {
      "questions": 50,
      "tts_minutes": 5.0,
      "credits": 20
    }
  },
  "used": {
    "questions": 0,
    "tts_seconds": 0.0,
    "credits": 6
  },
  "remaining": {
    "questions": 50,
    "tts_minutes": 5.0,
    "credits": 14
  }
}
```

<ResponseField name="period" type="object" required>
  <Expandable title="properties">
    <ResponseField name="key" type="string" required>
      Monthly period key in `YYYY-MM` format. Use to invalidate per-period local caches.
    </ResponseField>

    <ResponseField name="resets_at" type="string" required>
      ISO 8601 timestamp of the next month rollover (UTC). Drives "resets in N days" UX without polling.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="tier" type="object" required>
  <Expandable title="properties">
    <ResponseField name="id" type="string" required>
      One of `free`, `explorer`, `plus`, `pro`, `early_access`. The active paid subscription tier wins; otherwise the user's plan field on the profile row is returned.
    </ResponseField>

    <ResponseField name="product_id" type="string">
      Gateway product identifier (e.g. `com.bairisland.manticscore.pro.monthly`), or `null` for non-paid tiers.
    </ResponseField>

    <ResponseField name="caps" type="object" required>
      Per-period allowance for the tier.

      <Expandable title="properties">
        <ResponseField name="questions" type="number" required>
          Maximum questions per period.
        </ResponseField>

        <ResponseField name="tts_minutes" type="number" required>
          Maximum text-to-speech minutes per period.
        </ResponseField>

        <ResponseField name="credits" type="number" required>
          Maximum credits per period. A non-zero `credits_total` override on the user's profile (for example, an early-access lifetime grant) replaces the tier default.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="used" type="object" required>
  <Expandable title="properties">
    <ResponseField name="questions" type="number" required>
      Questions consumed in the current period. Returns `0` until the usage aggregator is wired.
    </ResponseField>

    <ResponseField name="tts_seconds" type="number" required>
      Text-to-speech seconds consumed in the current period. Returns `0` until the usage aggregator is wired.
    </ResponseField>

    <ResponseField name="credits" type="number" required>
      Credits consumed in the current period.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="remaining" type="object" required>
  <Expandable title="properties">
    <ResponseField name="questions" type="number" required>
      `max(caps.questions - used.questions, 0)`.
    </ResponseField>

    <ResponseField name="tts_minutes" type="number" required>
      `max(caps.tts_minutes - used.tts_seconds / 60, 0)`.
    </ResponseField>

    <ResponseField name="credits" type="number" required>
      `max(caps.credits - used.credits, 0)`.
    </ResponseField>
  </Expandable>
</ResponseField>

### Tier caps reference

| Tier           | Questions | TTS minutes | Credits |
| -------------- | --------- | ----------- | ------- |
| `free`         | 50        | 5           | 20      |
| `explorer`     | 500       | 60          | 200     |
| `plus`         | 1500      | 180         | 300     |
| `pro`          | 2500      | 300         | 400     |
| `early_access` | 100000    | 10000       | 100000  |

<Note>
  New users with no profile row receive a default response: `tier.id: "free"`, the free-tier caps, and zeroed counters. The profile row is created lazily on first call to `GET /profile`.
</Note>

<Info>
  The previous `/v1/usage` shape (`plan`, `credits.{used,total,remaining,resets_at,weights}`, `subscription.{...}`) has been replaced. Subscription lifecycle data now lives on `GET /profile` and individual webhook events; tier identity and entitlement are derived from `tier.id` here. Per-action credit weights are documented under [Credits](/account/credits).
</Info>

## RevenueCat webhook

Endpoint that RevenueCat calls to deliver subscription lifecycle events. You do not call this endpoint directly — configure it once in the RevenueCat dashboard under **Project → Integrations → Webhook**.

```
POST /api/v1/billing/rc-webhook
```

Rate limit: **600 requests per minute**.

### Authentication

Two layers, both required when configured:

1. **Bearer token** (required). The value of the `Authorization` header is compared in constant time to the secret configured in the RevenueCat dashboard.

   ```
   Authorization: Bearer <revenuecat_webhook_secret>
   ```

2. **HMAC-SHA256 body signature** (optional, defense-in-depth). When `revenuecat_signing_secret` is configured server-side, requests must include a hex digest of the raw body in the `X-RevenueCat-Signature` header. The verification path is currently dormant because RevenueCat removed the signing-secret field from current dashboards; leave the secret unset to skip this check.

If the bearer is invalid the endpoint returns `401`. If the JSON body is malformed it returns `400`. All other outcomes — including event-handler exceptions — return `200` so RevenueCat does not enter retry backoff for issues that require a code fix. The event row is always written to the audit table first, so failed events can be replayed from there.

### Handled event types

| RevenueCat event        | Subscription status                              |
| ----------------------- | ------------------------------------------------ |
| `INITIAL_PURCHASE`      | `active`                                         |
| `RENEWAL`               | `active`                                         |
| `NON_RENEWING_PURCHASE` | `active`                                         |
| `UNCANCELLATION`        | `active`                                         |
| `PRODUCT_CHANGE`        | `active`                                         |
| `CANCELLATION`          | `active` (auto-renew off; entitled until expiry) |
| `EXPIRATION`            | `expired`                                        |
| `BILLING_ISSUE`         | `in_billing_retry`                               |
| `SUBSCRIPTION_PAUSED`   | `paused`                                         |
| `SUBSCRIBER_ALIAS`      | audit-only                                       |
| `TRANSFER`              | audit-only                                       |
| `TEST`                  | audit-only                                       |

After processing the event, the server fetches the canonical subscription list from `GET /v2/projects/{project_id}/customers/{customer_id}/subscriptions` and reconciles the user's stored state against RevenueCat's authoritative view. Reconciliation is best-effort — a transient failure does not fail the webhook.

Each successful entitlement state flip also fires a `usage.changed` silent control envelope over the realtime gateway so the user's iOS client refreshes `/v1/usage` immediately without waiting for the next foreground poll.

```json 200 response (test event) theme={null}
{
  "ok": true,
  "audit_only": true,
  "type": "TEST"
}
```

```json 200 response (deferred after handler exception) theme={null}
{
  "ok": true,
  "deferred": true,
  "reason": "internal_error"
}
```

<Tip>
  Every webhook event is appended to an audit table before any state mutation. If the handler is updated later, historical events can be re-processed from the audit trail without asking RevenueCat to replay.
</Tip>
