MyBotBoxMyBotBox
Billing & Usage

Billing API

Programmatic access to usage, invoices, and Stripe portal/checkout sessions.

The Billing API powers the workspace Usage, Billing, and Spending pages and is fully usable from your own code.

Authentication & gating

GateStatus code on failure
Authenticated session (getSession() returns a user)401
Email verified (session.user.emailVerified)403 { code: "EMAIL_NOT_VERIFIED" }
Stripe credentials present (hasValidStripeCredentials())503 (Stripe-touching routes only)

All five routes below enforce the first two gates; the Stripe-touching ones (/portal, /invoices, /checkout) also require valid Stripe keys.

GET /api/billing/usage

Paginated per-execution usage rows for a workspace.

GET /api/billing/usage
  ?workspaceId=ws_abc
  &from=2026-04-01T00:00:00Z
  &to=2026-05-01T00:00:00Z
  &limit=100
  &cursor=<base64-encoded-cursor>

Required: workspaceId. Optional: from, to (ISO timestamps), limit (1–500, default 100), cursor.

{
  "data": [
    {
      "id": "log_123",
      "executionId": "exec_456",
      "workflowId": "wf_789",
      "workflowName": "Daily summary",
      "startedAt": "2026-05-01T10:00:00.000Z",
      "trigger": "manual",
      "model": "gpt-4o",
      "tokens": 312,
      "cost": 0.0042
    }
  ],
  "nextCursor": "<base64-encoded-cursor-or-omitted>"
}

tokens is computed from the execution's cost JSONB; falls back to input + output when tokens.total is absent.

GET /api/billing/usage/export

CSV stream of the same query (no pagination; capped at 10,000 rows).

GET /api/billing/usage/export?workspaceId=ws_abc&from=...&to=...

Response is text/csv; charset=utf-8 with header: Date,Execution,Workflow,Trigger,Model,Tokens,Cost (USD).

Filename pattern: usage-<workspaceId>-<YYYY-MM-DD>.csv.

GET /api/billing/invoices

Live Stripe invoice list for the signed-in user's stripeCustomerId.

GET /api/billing/invoices?starting_after=in_xyz&limit=10

Optional: starting_after (Stripe cursor), limit (1–50, default 10).

{
  "data": [
    {
      "id": "in_1...",
      "number": "INV-001",
      "created": 1700000000,
      "status": "paid",
      "amountPaid": 2000,
      "amountDue": 2000,
      "currency": "usd",
      "hostedInvoiceUrl": "https://invoice.stripe.com/i/...",
      "invoicePdf": "https://invoice.stripe.com/i/...?pdf=1",
      "description": "Pro monthly",
      "periodStart": 1699000000,
      "periodEnd": 1701000000
    }
  ],
  "hasMore": true,
  "nextCursor": "in_1..."
}

If the user has no Stripe customer on file (e.g. Free tier), the response is { "data": [], "hasMore": false, "nextCursor": null }.

POST /api/billing/portal

Create a Stripe Customer Portal session and return the URL.

{
  "context": "user",
  "organizationId": "org_xyz",
  "returnUrl": "https://app.mybotbox.com/workspace/<id>/billing"
}

Optional fields:

  • context: "user" (default) or "organization". Org context looks up the customer via subscription.referenceId.
  • organizationId: required when context = "organization".
  • returnUrl: defaults to /workspace?billing=updated.
{ "url": "https://billing.stripe.com/p/session/..." }

POST /api/billing/checkout

Create a Stripe Checkout session for a plan upgrade.

{
  "plan": "pro",
  "billingPeriod": "monthly",
  "successUrl": "https://app.mybotbox.com/workspace/<id>/billing?checkout=success",
  "cancelUrl": "https://app.mybotbox.com/workspace/<id>/spending?checkout=cancelled"
}

Required: plan ("starter" | "pro" | "team" | "enterprise"). Optional: billingPeriod ("monthly" default or "annual"), successUrl, cancelUrl.

{
  "success": true,
  "sessionId": "cs_test_...",
  "url": "https://checkout.stripe.com/c/pay/..."
}

Plan → Stripe price ID resolution lives in apps/sat/app/api/billing/checkout/route.ts:14-33 and reads the STRIPE_<PLAN>_PRICE_ID{,_ANNUAL,_MONTHLY} env vars.

GET /api/billing/on-demand

Read the current user's on-demand spending state.

GET /api/billing/on-demand
{
  "enabled": true,
  "capUsd": 50
}

POST /api/billing/on-demand

Toggle on-demand spending and/or set the per-period cap. Either field is optional; only the keys you include are updated.

{
  "enabled": true,
  "capUsd": 100
}

Optional: enabled (boolean), capUsd (number, USD).

{
  "ok": true,
  "enabled": true,
  "capUsd": 100
}

Writes land on user_stats.on_demand_enabled and user_stats.on_demand_cap_usd.

GET /api/billing/check-gate

Lightweight probe used by the in-app workflow executor before invoking a run. Returns the same shape that the execute gate would on a real run, so the UI can preempt 402s with the workflow-blocked modal. Behind the EXECUTE_USAGE_GATE_ENABLED flag — when the flag is off, this endpoint always returns { allow: true }.

GET /api/billing/check-gate
{ "allow": true }
{
  "allow": false,
  "code": "ON_DEMAND_CAP_REACHED",
  "message": "On-demand cap reached.",
  "context": { "capUsd": 50, "spendUsd": 52.10 }
}

Execution gate (402 responses)

Both POST /api/workflows/[id]/execute (programmatic) and GET /api/billing/check-gate (in-app) call the same checkUsageGate(userId) helper in apps/sat/lib/billing/core/usage.ts. When the gate rejects, the response body carries one of three typed codes:

CodeWhenSurface fix
BILLING_BLOCKEDuser_stats.billing_blocked = true (a Stripe invoice.payment_failed set this)Update payment in Stripe portal
INCLUDED_USAGE_EXHAUSTEDcurrent_period_cost >= currentUsageLimit AND on-demand is OFFEnable on-demand or upgrade
ON_DEMAND_CAP_REACHEDoverage >= on_demand_cap_usdRaise the cap or upgrade

The gate is fail-open on internal errors — if checkUsageGate itself throws, the run proceeds (errors are logged separately).

Bonus: GET/PUT /api/usage

The Spending page's Monthly Limit control writes to this endpoint.

GET /api/usage?context=user

Returns { data: { currentLimit, updatedAt, ... } }.

PUT /api/usage?context=user
Content-Type: application/json

{ "limit": 75 }

Sets userStats.currentUsageLimit to the new value. Server-side validation (in lib/billing/core/usage.ts) rejects:

  • Free users (Free plan users cannot edit usage limits)
  • Team / Enterprise members (use organization limits — set the org-level cap via ?context=organization instead)
  • Values below the plan minimum
  • Values below current period usage

Org-level usage limits are settable via ?context=organization and require the caller to be an org owner or admin.