# Billing (/api-v2/billing)



Stripe is the source of truth for money; Zumik keeps the projection it needs to enforce caps inline with inference. The console reads the account, sets budgets, opts into overage, and starts Checkout; Stripe webhooks credit prepaid balances and refresh subscription state. Money is tracked in integer micro-USD (1e-6 USD). See the [billing and budgets guide](/guides/billing-budgets).

All endpoints except the webhook require a bearer API key. See [authentication](/api-reference/authentication).

## Read the account [#read-the-account]

`GET /v2/billing/account`

```bash
curl https://api.zumik.ai/v2/billing/account \
  -H "Authorization: Bearer $ZUMIK_API_KEY"
```

```json
{
  "id": "prj_01jy7n0a4c8m2t6v9q3wrxk7bd",
  "object": "billing_account",
  "project_id": "prj_01jy7n0a4c8m2t6v9q3wrxk7bd",
  "plan": "base",
  "subscription_status": "active",
  "credit_balance_micros": 4250000,
  "cycle_spend_micros": 750000,
  "monthly_budget_micros": 20000000,
  "overage_mode": "pause",
  "created_at": "2026-06-01T00:00:00Z",
  "updated_at": "2026-06-15T16:40:11Z"
}
```

<ResponseField name="object" type="string">
  Always `billing_account`.
</ResponseField>

<ResponseField name="project_id" type="string">
  The owning project.
</ResponseField>

<ResponseField name="plan" type="string">
  The plan tier: `free`, `base`, or `pilot`.
</ResponseField>

<ResponseField name="stripe_customer_id" type="string">
  The Stripe customer id, omitted until a payment method is linked. Never a card number.
</ResponseField>

<ResponseField name="subscription_status" type="string">
  `none`, `active`, `past_due`, or `canceled`. Inference is allowed only when `active`.
</ResponseField>

<ResponseField name="credit_balance_micros" type="integer">
  Remaining prepaid plus allowance credits this cycle, in micro-USD.
</ResponseField>

<ResponseField name="cycle_spend_micros" type="integer">
  Spend accumulated this cycle, in micro-USD.
</ResponseField>

<ResponseField name="monthly_budget_micros" type="integer">
  The hard monthly cap, omitted when none is set.
</ResponseField>

<ResponseField name="overage_mode" type="string">
  `pause` (default) or `allow`.
</ResponseField>

<ResponseField name="created_at" type="string">
  RFC 3339 timestamp.
</ResponseField>

<ResponseField name="updated_at" type="string">
  RFC 3339 timestamp.
</ResponseField>

## Set a monthly budget [#set-a-monthly-budget]

`POST /v2/billing/budget`

Sets the hard monthly cap and re-arms the 50/80/100% alert ladder for the cycle.

<ParamField body="monthly_budget_usd" type="number">
  The cap in whole USD. Must be non-negative. `null` removes the cap.
</ParamField>

```bash
curl https://api.zumik.ai/v2/billing/budget \
  -H "Authorization: Bearer $ZUMIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "monthly_budget_usd": 20 }'
```

Returns the updated billing account.

## Opt into overage [#opt-into-overage]

`POST /v2/billing/overage`

When overage is off (the default), inference pauses at the cap with `quota_exceeded`. Enabling it bills past the cap at pay-as-you-go rates and requires explicit confirmation.

<ParamField body="allow_overage" type="boolean">
  `true` to bill past the cap, `false` to pause at it.
</ParamField>

<ParamField body="confirm" type="boolean">
  Must be `true` when `allow_overage` is `true`.
</ParamField>

```bash
curl https://api.zumik.ai/v2/billing/overage \
  -H "Authorization: Bearer $ZUMIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "allow_overage": true, "confirm": true }'
```

Returns the updated billing account with `overage_mode` set.

## Add credits (PaymentIntent) [#add-credits-paymentintent]

`POST /v2/billing/payment-intent`

Creates a Stripe PaymentIntent for a `amount_usd` credit top-up and returns its client secret plus the publishable key. The console mounts the Stripe **Payment Element** with these, so the card is entered inline on Zumik — the customer never leaves the app, and card data goes from the browser straight to Stripe (it never touches Zumik's servers). The card is saved for off-session [auto-recharge](#auto-recharge). The webhook credits the balance on `payment_intent.succeeded`.

<ParamField body="amount_usd" type="number">
  Credits to add, in whole USD. Must be between $5 and $10,000.
</ParamField>

```bash
curl -X POST https://api.zumik.ai/v2/billing/payment-intent \
  -H "Authorization: Bearer $ZUMIK_API_KEY" \
  -H "content-type: application/json" \
  -d '{"amount_usd": 25}'
```

```json
{
  "client_secret": "pi_..._secret_...",
  "publishable_key": "pk_live_...",
  "amount_usd": 25
}
```

<ResponseField name="client_secret" type="string">
  The PaymentIntent client secret. Pass it to Stripe.js to confirm the payment client-side.
</ResponseField>

<ResponseField name="publishable_key" type="string">
  The Stripe publishable key for mounting the Payment Element.
</ResponseField>

## Auto-recharge [#auto-recharge]

`POST /v2/billing/auto-recharge`

Configure balance-triggered auto top-up. When on and the credit balance falls to or below `threshold_usd`, Zumik charges the saved card for `amount_usd` off-session. Enabling requires a saved card (made by a prior top-up). Returns the updated billing account.

<ParamField body="enabled" type="boolean">
  Turn auto-recharge on or off.
</ParamField>

<ParamField body="threshold_usd" type="number">
  Recharge when the balance falls to or below this (USD). Default $5.
</ParamField>

<ParamField body="amount_usd" type="number">
  How much to add per recharge (USD), $5-$10,000. Default $25.
</ParamField>

```bash
curl -X POST https://api.zumik.ai/v2/billing/auto-recharge \
  -H "Authorization: Bearer $ZUMIK_API_KEY" \
  -H "content-type: application/json" \
  -d '{"enabled": true, "threshold_usd": 5, "amount_usd": 25}'
```

## Open the customer portal [#open-the-customer-portal]

`POST /v2/billing/portal`

Opens the Stripe customer portal so the user can update their card or view invoices. Requires an existing Stripe customer (i.e. they have made a top-up).

```bash
curl -X POST https://api.zumik.ai/v2/billing/portal \
  -H "Authorization: Bearer $ZUMIK_API_KEY"
```

```json
{ "url": "https://billing.stripe.com/p/session/..." }
```

<ResponseField name="url" type="string">
  The hosted Stripe billing-portal URL.
</ResponseField>

## Stripe webhook [#stripe-webhook]

`POST /v2/billing/stripe-webhook`

The endpoint Stripe posts events to. It is unauthenticated at the bearer-token layer and authenticates by verifying the `Stripe-Signature` header against the configured webhook secret before any state change. It is not called by your application.

It acts on `checkout.session.completed`, `invoice.payment_succeeded`, `invoice.payment_failed`, and `customer.subscription.deleted` for subscriptions, and `invoice.payment_succeeded`/`invoice.paid`, `invoice.voided`, and `invoice.finalized` for [enterprise contracts](#enterprise-contracts) (routed by the invoice's `contract_id` metadata). Unknown event types are acknowledged and ignored.

```json
{ "received": true }
```

<ResponseField name="received" type="boolean">
  Always `true` once a verified event is accepted.
</ResponseField>

## Enterprise contracts [#enterprise-contracts]

Negotiated annual deals are billed with **Stripe Invoicing*&#x2A;, not the self-serve credit top-up. These endpoints require an API key with the &#x2A;*`admin`** scope.

A contract moves `open` (invoice issued) → `paid` (the contract invoice is paid, which sets the account to the `enterprise` plan, marks it active, and grants the contract's credit allowance for the term) or `void`. The term is `term_months` from the paid date. Renewal issues a fresh invoice on the same terms.

### Create a contract [#create-a-contract]

`POST /v2/billing/enterprise/contracts`

Creates (or reuses) the project's Stripe customer, adds an invoice item for the annual value, then finalizes and sends a collection invoice carrying the project and contract metadata.

<ParamField body="annual_price_usd" type="number">
  The negotiated annual contract value billed on the invoice.
</ParamField>

<ParamField body="credit_allowance_usd" type="number">
  Inference credit granted to the account when the invoice is paid.
</ParamField>

<ParamField body="term_months" type="integer" default="12">
  Contract term, 1–60 months.
</ParamField>

<ParamField body="customer_email" type="string">
  Billing email. Required when the project has no Stripe customer yet.
</ParamField>

<ParamField body="description" type="string">
  Line-item description shown on the invoice.
</ParamField>

```json
{
  "id": "ent_01jy...",
  "object": "enterprise_contract",
  "project_id": "prj_01jy...",
  "status": "open",
  "annual_price_micros": 120000000000,
  "credit_allowance_micros": 50000000000,
  "term_months": 12,
  "stripe_customer_id": "cus_...",
  "stripe_invoice_id": "in_...",
  "hosted_invoice_url": "https://invoice.stripe.com/i/...",
  "created_at": "2026-06-15T16:00:00Z",
  "updated_at": "2026-06-15T16:00:00Z"
}
```

### List / retrieve [#list--retrieve]

`GET /v2/billing/enterprise/contracts` returns `{ "object": "list", "data": [...] }`, newest first. `GET /v2/billing/enterprise/contracts/{id}` returns one contract.

### Void [#void]

`POST /v2/billing/enterprise/contracts/{id}/void` voids the contract's open Stripe invoice and marks the contract `void`. A `paid` contract can't be voided (issue a credit note in Stripe instead).

### Renew [#renew]

`POST /v2/billing/enterprise/contracts/{id}/renew` issues the next term's invoice on the same price, allowance, and term, returning the new contract. The account is re-credited when that invoice is paid.

## Errors [#errors]

| Status | Code                    | When                                                                                                                                                                  |
| ------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request_error` | Negative budget, overage without `confirm: true`, billing not configured, a non-positive `annual_price_usd`, voiding a paid contract, or Stripe rejected the request. |
| 401    | `invalid_api_key`       | Missing or invalid API key, or an invalid Stripe signature on the webhook.                                                                                            |
| 403    | `insufficient_scope`    | An enterprise endpoint called without an `admin`-scoped key.                                                                                                          |
| 404    | `invalid_request_error` | The contract does not exist in this project.                                                                                                                          |

See the full table on [errors](/api-reference/errors).
