Billing credits

A billing credit is a prepaid balance attached to a customer or account. When a billing meter records usage, Frame can decrement the customer's matching credit balance rather than (or in addition to) generating an invoice line. This is the primitive behind prepaid pricing models — buy 10,000 API calls upfront, use them down, repurchase when depleted.

Owner: customer or account

A credit attaches to exactly one of:

  • customer_id — B2C balance. The customer's individual prepaid pool.
  • account_id — B2B balance. The account's pool, typically shared across the account's users.

Mutually exclusive. Frame's validator rejects a credit with both set.

Fields

curl --request POST \
  --url https://api.framepayments.com/v1/billing/billing_credit \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "customer": "<customer_id>",
    "product": "<product_id>",
    "billing_credits": 10000,
    "limited": true,
    "expires": "2027-06-05T00:00:00Z"
  }'

The interesting fields:

  • customer or account — owner. Mutually exclusive.
  • product — FK to a Product. Credits are product-scoped: usage against a different product can't consume them. The Product's BillingProductDetail carries the billing_type (prepaid / postpaid / etc.) and credit-related configuration.
  • billing_credits — the prepaid balance, in whatever unit the meter measures (calls, gigabytes, minutes — whatever the meter aggregates).
  • limited — boolean. When true, usage is capped at the credit balance; when false, credits decrement but additional usage continues at on-demand pricing. Defaults vary by BillingProductDetail.
  • expires — ISO-8601 timestamp. Credits in active state past this time transition to expired.

On create, Frame populates derived fields:

  • available_credits — starts equal to billing_credits; decrements as usage events arrive.
  • used_credits — starts at 0; increments as usage flows through meters.

The math invariant: billing_credits = available_credits + used_credits.

Lifecycle

Credits walk through three states:

  • inactive (initial) — credit exists but isn't yet consumable. Useful for staging credits ahead of activation, or for credits awaiting payment confirmation.
  • active — consumable. Usage events that match the product decrement available_credits.
  • expired — past the expires timestamp, or manually expired. Terminal.

Transitions:

  • inactive → active via activate.
  • active → inactive via deactivate.
  • active → expired via expire (automatic when expires passes, or manual).

expired is terminal — credits don't come back to active once expired. Issue a new credit if you need to extend the balance.

How credits consume

When a BillingMetricEvent lands for a customer who has active credits scoped to the meter's product:

  1. Frame computes the cost (per the meter's aggregation + value).
  2. If credits are available, deducts from available_credits, increments used_credits.
  3. If credits aren't enough and limited: false, the remainder bills at on-demand rates (invoice line on the next cycle).
  4. If credits aren't enough and limited: true, the usage is rejected at the application layer (your platform enforces; Frame surfaces the threshold-progress signal).

The threshold-progress endpoint surfaces depletion state:

curl --request GET \
  --url "https://api.framepayments.com/v1/billing/report/threshold_progress?customer_id=<customer_id>" \
  --header "Authorization: Bearer $FRAME_SECRET_KEY"

Returns the remaining credits per active credit record. Use this for customer-facing usage dashboards ("you have 2,317 calls remaining this cycle").

Webhooks

Credit-related events emit through the broader billing webhook set:

  • billing.credits_usage — credits were consumed against a usage event.
  • billing.threshold.approaching — customer is close to running out. Threshold typically configured server-side; reach out to Frame ops if you need a custom threshold.
  • billing.credits_expiration — credits expiring soon or just expired.

Subscribe to billing.threshold.approaching to give customers a heads-up before they hit zero — this is the most leverage-positive signal in the billing webhook surface for customer retention.

Composition with subscriptions

Credits + subscriptions compose in two common shapes:

  • Subscription bundles credits. A monthly subscription product includes N credits per cycle. On renewal, issue a fresh credit; on cancellation, expire the remaining balance (or let it expire naturally).
  • Credits replace subscription. Customer purchases credits a la carte; no recurring subscription. Customer's checkout flow is "buy more credits" rather than "manage subscription."

The Product's BillingProductDetail.billing_type and billing_credits fields control this composition.

Gotchas

Symptom: you issued a credit but available_credits stays at zero. Why: the credit was created in inactive (the initial state) and never activated. Fix: explicitly call activate, or check whether your creation flow auto-activates depending on BillingProductDetail.billing_type.

Symptom: customer's usage isn't decrementing their credit. Why: the credit is product-scoped; usage that doesn't match the credit's product (i.e., events on a meter whose attached Product is different) won't consume. Fix: either issue a credit on the right product, or restructure your meters/products so the usage matches.

Symptom: duplicate active credits for the same customer and product. Why: IssueBillingCreditService rejects this on create, but if you bypassed the service (direct DB insert, ops console) you could end up with two. Fix: always use the API; for cleanup, expire the duplicates manually.

Symptom: expired credits are still showing up on the customer's dashboard. Why: getBillingThresholdProgress returns active credits only, but if you're listing credits independently, you'll see expired ones too. Fix: filter by status: active in your dashboard query.

Reference

For the full API surface, see POST/v1/billing/billing_credit and GET/v1/billing/report/threshold_progress.