Token-Based Billing Quick Start

Selling a fixed bundle of tokens turns unpredictable usage into predictable revenue, which is why token-based pricing has become popular with AI products. A customer can only ever consume tokens they have paid for, and anything they leave unspent is yours to keep or roll over as you choose. Building that yourself means holding a balance per customer, decrementing correctly when requests are sent concurrently, expiring or carrying over the remainder at the end of each cycle, and granting more when someone tops up. Salable does all of it for you: a Token Line Item grants the balance on purchase, one consume call deducts from the balance atomically, and the Reset Behaviour you pick decides what tokens carry over into the next cycle.

What you'll build

By the end of this guide you'll have a Product with a Starter Plan at $29/month granting 1,000 image tokens per cycle, a one-off 1,000 Token Top-Up Plan at $29 for customers who run out mid-cycle, and an application that deducts tokens per generation and refuses the request when the balance reaches zero. Unused Starter tokens reset at the start of each cycle, while topped-up tokens stack on top of the remaining balance. An owner-scoped Entitlement check automatically returns the image_tokens Token slug for access checks.

┌─ Product ──────────────────────────────────────────────────┐
│                                                            │
│  Plan: Starter ($29/month, recurring)                      │
│    Token Line Item  image_tokens, qty 1,000, reset         │
│    Entitlement      export_hd                              │
│                                                            │
│  Plan: 1,000 Token Top-Up ($29, one-off)                   │
│    Token Line Item  image_tokens, qty 1,000                │
│    Perpetual Subscription optional for standalone access   │
│                                                            │
└────────────────────────────────────────────────────────────┘

1. Create and configure your Product

Before you can start implementing Salable into your codebase, you need to sign up for an account.

In the Salable dashboard, create a Product and a Plan called "Starter"; this is what users will subscribe to. Add Entitlements only for features that aren't already represented by a Meter or Token.

For an AI image generation tool, add export_hd as an Entitlement. The image_tokens Token slug added in the next step will control access to image generation, so you don't create a separate image_tokens Entitlement for it.

Tip To return Meter and Token slugs as entries in an entitlement check, pass the owner query param in the request. When gating features with these entries, check both the type and value, a separate Entitlement is then not required to gate these features.

2. Add the Token Line Item

To grant tokens on your "Starter" Plan, create a Line Item with a Pricing Type of "Token" and set values from the table below. This Line Item is what tells the system to create tokens on purchase.

FieldValueNotes
Tokenimage_tokensThe Line Item needs to be linked to a Token in the system. Type image_tokens into the input, this will be your Token slug. Tokens are organisation-scoped and reusable across Plans.
Token Quantity1000The amount of tokens which will be granted to the customer, per unit per cycle. You can allow a customer to buy multiple units of tokens e.g. 2 units purchased receives 2,000 tokens.
Reset BehaviourResetOnly appears on recurring Line Items, where it defaults to Reset. Reset clears the remaining balance at the start of each cycle and issues a fresh 1,000.
Token Expiry Days(not shown)Only appears when Reset Behaviour is Accumulate, or on a one-off Line Item. Under Reset the balance already expires with the billing cycle.

Set the Price to $29 with a monthly Interval. This Price is the per-unit cost, if the customer wanted 2,000 tokens, they would pay $58 for two units.

3. Create a one-off top-up Plan

Customers who exhaust their allowance mid-cycle need a way to buy more without upgrading, which is what a Plan with a one-off Line Item gives them.

Create a second Plan on your Product named "Token Top-Up". Add a one-off Token Line Item using the same image_tokens slug with a Token Quantity of 1,000, priced at $29.

Note One-off Token Line Items always use accumulate behaviour because there is no renewal to reset against.

Because both Plans grant tokens for the same image_tokens slug, a customer holding 200 Starter tokens who then purchases a top-up will have 1,200 available, spread across two Token Records with different expiry dates. Salable spends whichever record expires soonest first, and leaves records with no expiry until last.

Tip The customer's valid Starter Subscription continues to grant access to image_tokens and export_hd after a top-up. The top-up Plan only needs a perpetual Subscription for standalone access or access after the Starter Subscription ends. In that case, check "Create a perpetual subscription" and add the export_hd Entitlement if top-up buyers should receive it.

Purchases of this Plan produce a Receipt and a Token Record and optionally a Subscription if "Create a perpetual subscription" was checked.

Important Entitlement checks only grant Entitlements, Token slugs, and Meter slugs through a valid Subscription. The top-up tokens can be spent without their own Subscription while the Starter Subscription is valid, but they don't grant access by themselves.

Generate a Checkout link for the Starter Plan and redirect the customer to it.

ParameterRequiredDescription
planIdYesThe Plan being purchased.
ownerYesThe Owner is the user's tenant like a team ID or organisation ID. For single-user applications use the user ID. Token balances are tracked per Owner.
granteeNoThe Grantee receiving access. Accepts a Grantee's ID within your system like a user ID or a Group ID in Salable. If omitted, Salable creates an empty Group for the Owner.
intervalYesThe billing Interval. Set to null for one-off purchases such as the top-up Plan.
intervalCountYesThe number of Intervals for each cycle, so 1 with month cycles monthly. Set to null for one-off purchases such as the top-up Plan.
currencyNoThree-letter ISO 4217 code. Falls back to geolocation when omitted. For geolocation to work all the Line Items on the Plan must have the same default currency.
successUrlNoWhere the customer is redirected to after paying. Required if this is not set in the Plan's Product's settings.
cancelUrlNoWhere the customer returns if they abandon checkout. Required if this is not set in the Plan's Product's settings.
import { Salable } from '@salable/sdk';
const salable = new Salable('secret-api-key');
 
const { data } = await salable.api.checkout.post({
    planId: 'plan_01KM4XQ8ZT7BN2VF9GDR3HC5EW',
    owner: 'company_acme',
    grantee: 'user_alice',
    interval: 'month',
    intervalCount: 1,
    currency: 'USD',
    successUrl: 'https://your-app.com/success',
    cancelUrl: 'https://your-app.com/pricing'
});
 
// Redirect user to data.url

To sell the top-up Plan, call the same endpoint with planId set to the top-up Plan's ID, and set interval and intervalCount to null. Setting these values to null is how you create a one-off purchase in the checkout.

Once the customer pays for the Starter Plan, Salable creates the Subscription and a Token Record holding 1,000 image_tokens for company_acme, and fires a tokens.accrued webhook.

Checkouts in Test Mode work without touching a real card. Pay with Stripe's test card 4242 4242 4242 4242, any future expiry date, and any three-digit CVC.

5. Consume tokens

Deduct from the balance whenever the customer consumes tokens, such as when they generate an image.

ParameterRequiredDescription
tokenYesThe Token slug to deduct from, this was set on the Plan.
ownerYesThe Owner whose balance is being spent.
amountYesTokens to deduct. Must be an integer of at least 1.

Send an Idempotency-Key header, as the request shows below. If the same request is sent again within 24 hours, Salable replays the original response and returns an Idempotent-Replayed: true header instead of deducting a second time, which is what stops a client retry from charging the customer twice for the same action.

import { Salable } from '@salable/sdk';
const salable = new Salable('secret-api-key');
 
await salable.api.tokens.consume.post(
    {
        token: 'image_tokens',
        owner: 'company_acme',
        amount: 1
    },
    {
        headers: {
            'Idempotency-Key': '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
        }
    }
);

A successful consume request returns 204 No Content and sends a tokens.consumed webhook event. When the balance is too low it returns 422 with the title "Insufficient token balance", this is the signal to block the action and point the customer at the top-up Plan from Step 3.

Consumption is atomic, so concurrent requests can't drive the balance below zero. When an Owner holds several Token Records for the same Token, Salable spends the records expiring soonest first and leaves records with no expiry until last, deducting across multiple records in a single call where needed. A record that reaches zero is marked consumed and drops out of later balance checks.

6. Check access to the Token

Pass the Owner when checking access so Salable includes the relevant Token slugs. Check both type and value, rather than creating a separate Entitlement to represent image_tokens.

import { Salable } from '@salable/sdk';
const salable = new Salable('secret-api-key');
 
const { data } = await salable.api.entitlements.check.get({
    queryParameters: {
        granteeId: 'user_alice',
        owner: 'company_acme'
    }
});
 
const hasAccess = data.entitlements.some(entry => entry.type === 'token' && entry.value === 'image_tokens');
 
if (hasAccess) {
    // Show the generation UI
}

Your application can now hide the generation UI when the Owner's active Plan doesn't include image_tokens. A Meter and Token can share the same slug, so keep the type === 'token' check.

7. Get the token balance

To show customers how many tokens they have left, query the balance endpoint. You can filter by token slug and source which will either be Receipt Item ID (ri_...) or Subscription Plan Line Item ID (spli_...) when you want the tokens from one purchase in isolation.

import { Salable } from '@salable/sdk';
const salable = new Salable('secret-api-key');
 
const { data } = await salable.api.tokens.balance.get({
    queryParameters: {
        owner: 'company_acme',
        token: 'image_tokens'
    }
});
 
const { balance, records } = data[0];
 
// balance === 750
// records === [{ recordId: 'tkr_01KM4YB3QN5WE7RX2FHT9DVC64', remaining: 750, expiresAt: '2026-09-01T00:00:00Z' }]

The aggregate balance is what you put in the UI. The records breakdown is what you use to warn customers that 200 of their tokens expire next week, which the aggregate alone can't tell them.

8. React to token webhooks

Webhooks let you act on balance changes without polling.

Salable sends tokens.accrued when tokens are granted, whether from a new Subscription, a renewal, or a top-up Receipt. Use it to refresh a cached balance or email a customer that their new allowance has landed. tokens.consumed fires on every deduction, which is useful for usage analytics and for warning customers as they approach zero. tokens.expired fires when a Token Record hits its expiry and the remaining balance is invalidated, either at the end of a cycle under reset or when the expiry days is reached under accumulate.

Treat tokens.accrued as the signal to invalidate any balance you cache, since it is the only one of the three events that increases what a customer has available.

9. Test it

Work through these to confirm the model end to end:

  • After checkout completes, GET /api/tokens/balance?owner=company_acme&token=image_tokens returns a balance of 1,000.
  • Calling consume with amount: 1 returns 204, and the balance drops to 999.
  • Replaying that consume call with the same Idempotency-Key leaves the balance at 999 rather than 998.
  • Consuming more tokens than remain returns 422 with "Insufficient token balance".
  • Buying the top-up Plan adds 1,000 tokens on top of the remainder rather than replacing it, and records shows the Starter record with an expiresAt date alongside the top-up record with null.
  • An owner-scoped Entitlement check returns image_tokens with type: 'token' after the Starter checkout.

10. Next steps

  • Token Billing Reset versus accumulate in depth, expiry rules, and how Token Records are drawn down.
  • Understanding Entitlements Entitlement patterns, signature verification, and caching.
  • Cart & Checkout Sell a Plan and a top-up together in a single transaction using a Cart.
  • Webhooks Verify signatures and handle retries for the token events above.
  • Subscriptions & Billing How renewals, cancellations, and proration affect the balances you grant.