Overview
billing.io lets you create recurring billing plans and subscribe your customers to them. Payments are collected in crypto at each billing interval through automated checkout sessions.
This guide covers the full lifecycle: creating plans, subscribing customers, handling renewals, checking entitlements, and managing lifecycle events.
Prerequisites — You need at least one active
payment method
configured before creating subscriptions. Subscription billing requires the
Growth plan or above.
Step 1: Create a Customer
Every subscription is tied to a customer. Create one first.
curl -X POST https://api.billing.io/v1/customers \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"name": "Jane Doe",
"external_id": "usr_12345",
"metadata": {
"company": "Acme Corp",
"plan_source": "website"
}
}'
Response:
{
"id" : "cus_a1b2c3d4e5f6a7b8c9d0e1f2" ,
"org_id" : "org_..." ,
"email" : "jane@example.com" ,
"name" : "Jane Doe" ,
"external_id" : "usr_12345" ,
"metadata" : {
"company" : "Acme Corp" ,
"plan_source" : "website"
},
"environment" : "live" ,
"created_at" : "2025-06-15T10:00:00Z" ,
"updated_at" : "2025-06-15T10:00:00Z"
}
Use external_id to map billing.io customers to users in your own system.
This makes it easy to look up customers by your internal ID. The combination
of (org_id, email, environment) and (org_id, external_id, environment)
must be unique.
Step 2: Create a Subscription Plan
A subscription plan defines the recurring billing terms: price, interval, token, chain, and optional trial period.
curl -X POST https://api.billing.io/v1/subscriptions/plans \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro Monthly",
"description": "Full access to all Pro features",
"amount_usd": 19.99,
"interval": "monthly",
"trial_days": 14,
"token": "USDT",
"chain": "tron"
}'
Response:
{
"id" : "plan_c3d4e5f6a7b8c9d0e1f2a3b4" ,
"org_id" : "org_..." ,
"name" : "Pro Monthly" ,
"description" : "Full access to all Pro features" ,
"amount" : 19.99 ,
"currency" : "USD" ,
"token" : "USDT" ,
"interval" : "monthly" ,
"interval_count" : 1 ,
"trial_days" : 14 ,
"status" : "active" ,
"environment" : "live" ,
"created_at" : "2025-06-15T10:05:00Z" ,
"updated_at" : "2025-06-15T10:05:00Z"
}
Available intervals: daily, weekly, monthly, yearly
Plans can be archived with PATCH /subscriptions/plans/{id} by setting
status to archived. Archived plans cannot be assigned to new subscriptions
but existing subscriptions remain active.
Step 3: Create a Subscription
Subscribe a customer to a plan. If the plan has trial_days > 0, the subscription starts in trialing status. Otherwise, the first renewal checkout is created immediately.
curl -X POST https://api.billing.io/v1/subscriptions \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cus_a1b2c3d4e5f6a7b8c9d0e1f2",
"plan_id": "plan_c3d4e5f6a7b8c9d0e1f2a3b4",
"payment_method_id": "pm_8f3a1b2c4d5e6f7a"
}'
Response:
{
"id" : "sub_d4e5f6a7b8c9d0e1f2a3b4c5" ,
"org_id" : "org_..." ,
"customer_id" : "cus_a1b2c3d4e5f6a7b8c9d0e1f2" ,
"plan_id" : "plan_c3d4e5f6a7b8c9d0e1f2a3b4" ,
"payment_method_id" : "pm_8f3a1b2c4d5e6f7a" ,
"status" : "trialing" ,
"current_period_start" : "2025-06-15T10:10:00Z" ,
"current_period_end" : "2025-06-29T10:10:00Z" ,
"trial_end" : "2025-06-29T10:10:00Z" ,
"canceled_at" : null ,
"cancel_at_period_end" : false ,
"pause_start" : null ,
"pause_end" : null ,
"environment" : "live" ,
"created_at" : "2025-06-15T10:10:00Z" ,
"updated_at" : "2025-06-15T10:10:00Z"
}
The payment_method_id field is optional. If omitted, billing.io uses your
organization’s default payment method.
Step 4: Handle the Renewal Checkout Flow
When a billing period ends, billing.io automatically creates a renewal record and a checkout for the next payment. Here is how the flow works:
┌──────────────────────────────────────────────────────────────────────┐
│ Subscription Renewal Flow │
│ │
│ Period End │
│ │ │
│ v │
│ Renewal record created (status: pending) │
│ │ │
│ v │
│ Checkout created automatically (status: checkout_created) │
│ │ │
│ v │
│ Customer pays the checkout │
│ │ │
│ ├── Payment confirmed ──> Renewal: paid ──> Period advanced │
│ │ │
│ └── Payment failed/expired ──> Renewal: failed │
│ │ │
│ v │
│ Retry (up to max_attempts) │
│ │ │
│ └── All retries exhausted ──> Subscription: past_due │
└──────────────────────────────────────────────────────────────────────┘
Listing renewals
curl "https://api.billing.io/v1/subscriptions/renewals?subscription_id=sub_d4e5f6a7b8c9d0e1f2a3b4c5&limit=10" \
-H "Authorization: Bearer sk_live_xxx"
Retrying a failed renewal
If a renewal fails, you can manually trigger a retry to create a new checkout:
curl -X POST https://api.billing.io/v1/subscriptions/renewals/ren_1a2b3c4d5e/retry \
-H "Authorization: Bearer sk_live_xxx"
Step 5: Check Entitlements
Entitlements let you attach feature flags to subscription plans. You can gate features in your application based on what plan a customer is on.
Create an entitlement on a plan
curl -X POST https://api.billing.io/v1/subscriptions/entitlements \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "plan_c3d4e5f6a7b8c9d0e1f2a3b4",
"feature_key": "api_requests",
"value_type": "numeric",
"value_numeric": 10000
}'
Check a customer’s entitlement
Use the check endpoint to determine if a customer has access to a specific feature at runtime:
curl "https://api.billing.io/v1/subscriptions/entitlements/check?customer_id=cus_a1b2c3d4e5f6a7b8c9d0e1f2&feature_key=api_requests" \
-H "Authorization: Bearer sk_live_xxx"
Response:
{
"has_access" : true ,
"feature_key" : "api_requests" ,
"value_type" : "numeric" ,
"value_boolean" : null ,
"value_numeric" : 10000 ,
"value_string" : null ,
"plan_id" : "plan_c3d4e5f6a7b8c9d0e1f2a3b4" ,
"subscription_id" : "sub_d4e5f6a7b8c9d0e1f2a3b4c5"
}
Entitlements support three value types:
Value Type Field Use Case booleanvalue_booleanFeature on/off flags (e.g., advanced_analytics: true) numericvalue_numericUsage limits (e.g., api_requests: 10000) stringvalue_stringTier labels or config (e.g., support_level: "priority")
Step 6: Handle Lifecycle Events
Subscriptions move through a defined state machine. Set up webhooks to react to lifecycle transitions.
Subscription status flow
┌────────────────────────────────────────────┐
│ │
v │
┌──────────────┐ ┌─────────┐ ┌──────────┐ │
│ trialing │ ──> │ active │ ──> │ past_due │ ────┘
└──────────────┘ └─────────┘ └──────────┘ (payment recovered)
│ │ │
│ │ v
│ │ ┌──────────┐
│ └───> │ paused │
│ └──────────┘
│ │
v v
┌──────────┐ ┌──────────┐
│ canceled │ │ expired │
└──────────┘ └──────────┘
Key webhook events
Event Description Recommended Action subscription.renewedPeriod payment confirmed Extend access for the new period subscription.past_duePayment failed after retries Warn customer, offer retry subscription.pausedSubscription paused Restrict feature access subscription.canceledSubscription canceled Revoke access at period end
Handling lifecycle transitions
Node.js (Express)
Python (Flask)
app . post ( "/webhooks/billing" , express . raw ({ type: "application/json" }), async ( req , res ) => {
const event = JSON . parse ( req . body );
switch ( event . type ) {
case "subscription.renewed" : {
// Payment was successful -- extend access
const { subscription_id , customer_id } = event . data ;
await extendAccess ( customer_id , subscription_id );
break ;
}
case "subscription.past_due" : {
// Payment failed -- notify the customer
const { customer_id , subscription_id } = event . data ;
await sendPaymentFailedEmail ( customer_id );
await flagAccountForRetry ( subscription_id );
break ;
}
case "subscription.paused" : {
// Subscription paused -- restrict access
const { customer_id } = event . data ;
await restrictAccess ( customer_id );
break ;
}
case "subscription.canceled" : {
// Subscription canceled -- schedule access revocation
const { customer_id , current_period_end } = event . data ;
await scheduleAccessRevocation ( customer_id , current_period_end );
break ;
}
}
res . status ( 200 ). json ({ received: true });
});
Managing subscription state
Cancel at end of period or immediately: # Cancel at end of current period
curl -X PATCH https://api.billing.io/v1/subscriptions/sub_d4e5f6a7b8c9d0e1f2a3b4c5 \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"action": "cancel"}'
# Cancel immediately
curl -X PATCH https://api.billing.io/v1/subscriptions/sub_d4e5f6a7b8c9d0e1f2a3b4c5 \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"action": "cancel_immediately"}'
Pause a subscription (skips future renewals): curl -X PATCH https://api.billing.io/v1/subscriptions/sub_d4e5f6a7b8c9d0e1f2a3b4c5 \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"action": "pause"}'
Resume a paused subscription: curl -X PATCH https://api.billing.io/v1/subscriptions/sub_d4e5f6a7b8c9d0e1f2a3b4c5 \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"action": "resume"}'
Next Steps