Quickstart
By the end of this page you have a paying subscription in test mode: a business, a product and price, a customer with a test card, and a subscription whose first invoice is already paid. No real money moves, and you need no payment provider account.
Get a test API key
- Sign up at the dashboard's
/signuppage. You start on the free plan: every feature. No credit card required. It is free while you make less than the Free plan's monthly limit. - Open Settings → Developer. Under API keys, set the mode to
testand select Create API key. Copy the secret now: it shows one time only. - Your API base URL is the host that the API reference links at the top of the same page open on.
- Run the engine and the dashboard with Docker,
or from the repository (see
SETUP.md). - Sign in to your dashboard and create a
testkey under Settings → Developer → API keys. - Your base URL is your own API host, for example
http://localhost:4000on a local install.
Export both, so every example on this page runs as written:
export TMBILLING_URL=https://your-api-host
export TMBILLING_API_KEY=sk_test_…
Test keys start with sk_test_. They reach your test businesses only: ask a test key for a live
business and you get 404. See Test and live mode.
Each step shows the TypeScript client and curl, then the response. Responses are shortened to the fields that matter here. The API returns every field of the object.
The TypeScript examples use the typed client:
import { Tmbilling } from "@two-minute-billing/sdk";
const tmb = new Tmbilling({
apiKey: process.env.TMBILLING_API_KEY!,
baseUrl: process.env.TMBILLING_URL,
});
Create a business
A business holds one catalog, its customers and its money, in one default currency. Every business has a live mode and a test mode. Create one with a test key and you get its test mode back. The live mode is created beside it.
const biz = await tmb.businesses.create({ name: "Fernhill Analytics", default_currency: "USD" });
curl -X POST $TMBILLING_URL/v1/businesses \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Fernhill Analytics", "default_currency": "USD" }'
export BIZ=biz_…
{
"object": "business",
"id": "biz_XXXXXXXXXXXXXXXXXXXXXX01",
"name": "Fernhill Analytics",
"livemode": "test",
"live_business_id": "biz_XXXXXXXXXXXXXXXXXXXXXX02",
"test_business_id": null,
"default_currency": "USD",
"status": "active",
"created_at": "2026-10-20T19:02:10.255Z"
}
Every URL below is scoped to the business id. Keep it as $BIZ.
Create a product and a price
A product is what you sell. A price is one way to charge for it: a currency, an interval and a pricing model. One product can have many prices — monthly and yearly, USD and EUR.
Catalog objects take a readable id. Omit id and tmbilling derives one from the name, so the
product below is pro. Keep the same ids in test and live mode, and the same code works in both.
const product = await tmb.products.create(biz.id, { name: "Pro" });
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Pro" }'
Now the price. flat charges a fixed amount each period. Amounts are integers in minor units, so
2900 is $29.00.
const price = await tmb.prices.create(biz.id, {
product: product.id,
name: "Pro monthly",
model: "flat",
currency: "USD",
amount_minor: 2900,
interval: "month",
});
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"product": "pro",
"name": "Pro monthly",
"model": "flat",
"currency": "USD",
"amount_minor": 2900,
"interval": "month"
}'
{
"object": "price",
"id": "pro-monthly",
"product": "pro",
"name": "Pro monthly",
"type": "recurring",
"model": "flat",
"currency": "USD",
"amount_minor": 2900,
"interval": "month",
"interval_count": 1,
"active": true
}
Other models use the same call: per_unit for seats, tiered and volume for graduated
pricing, usage for metered billing. See Catalog.
Create a customer
The customer is who pays. Their currency must match the prices you subscribe them to.
const customer = await tmb.customers.create(biz.id, {
email: "[email protected]",
first_name: "Maya",
last_name: "Castillo",
company: "Brightwater Analytics",
currency: "USD",
});
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Maya",
"last_name": "Castillo",
"company": "Brightwater Analytics",
"currency": "USD"
}'
export CUS=cus_…
{
"object": "customer",
"id": "cus_XXXXXXXXXXXXXXXXXXXXXX01",
"email": "[email protected]",
"name": "Maya Castillo",
"company": "Brightwater Analytics",
"currency": "USD",
"auto_collection": "on",
"created_at": "2026-10-20T19:02:10.426Z"
}
Save a test card
Test mode has a built-in test gateway. Give it a test card number as the token and it saves the
card. 4242424242424242 always succeeds. Other numbers decline, time out or ask for
3-D Secure on purpose.
await tmb.paymentMethods.create(biz.id, customer.id, { token: "4242424242424242" });
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/payment_methods \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "token": "4242424242424242" }'
{
"object": "payment_method",
"id": "pm_XXXXXXXXXXXXXXXXXXXXXX01",
"provider": "tmtest",
"type": "card",
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2031,
"status": "valid",
"is_default": true,
"gateway_account_label": "TM Test Gateway"
}
In live mode your checkout or portal collects the card through your payment gateway, and you never handle the number. See Checkout and portal.
Subscribe the customer
A subscription has one item per price, each with a quantity. tmbilling raises the first invoice and charges the saved card straight away.
const subscription = await tmb.subscriptions.create(biz.id, customer.id, {
items: [{ price: price.id, quantity: 1 }],
});
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/subscriptions \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "items": [{ "price": "pro-monthly", "quantity": 1 }] }'
{
"object": "subscription",
"id": "sub_XXXXXXXXXXXXXXXXXXXXXX01",
"status": "active",
"currency": "USD",
"customer": "cus_XXXXXXXXXXXXXXXXXXXXXX01",
"current_period_start": "2026-10-20T19:02:10.584Z",
"current_period_end": "2026-11-20T19:02:10.584Z",
"next_billing_at": "2026-11-20T19:02:10.584Z",
"mrr": 2900,
"items": [
{
"id": "si_XXXXXXXXXXXXXXXXXXXXXX01",
"price": "pro-monthly",
"quantity": 1,
"unit_price_minor": 2900
}
],
"latest_invoice": {
"id": "inv_XXXXXXXXXXXXXXXXXXXXXX01",
"status": "paid",
"total_minor": 2900
}
}
The subscription is active and its first invoice is paid. From now on the billing clock owns
it: it renews, invoices and collects each month without another call from you. To start with a
free trial instead, see Trials.
See the invoice
const invoices = await tmb.invoices.list(biz.id, { customer: customer.id });
curl "$TMBILLING_URL/v1/businesses/$BIZ/invoices?customer=$CUS" \
-H "Authorization: Bearer $TMBILLING_API_KEY"
{
"object": "list",
"data": [
{
"object": "invoice",
"id": "inv_XXXXXXXXXXXXXXXXXXXXXX01",
"number": "INV-1",
"status": "paid",
"currency": "USD",
"total_minor": 2900,
"amount_paid_minor": 2900,
"amount_due_minor": 0,
"period_start": "2026-10-20T19:02:10.584Z",
"period_end": "2026-11-20T19:02:10.584Z",
"paid_at": "2026-10-20T19:02:10.730Z",
"subscription": "sub_XXXXXXXXXXXXXXXXXXXXXX01"
}
],
"has_more": false
}
Every step above also emitted an event. Point a webhook endpoint at your service to receive them as they happen.
Next steps
Introduction
Two Minute Billing owns subscription state, keeps money in an append-only, double-entry ledger, and emits an event for every change. Learn the model first.
Test and live mode
Every business has a live mode and a test mode. Build against test mode with test cards for each failure and Time Travel, then switch your key to go live.