One of: extra_entities, extra_employees, ocr_bundle
Billing, Usage & Add-ons
const { data } = await $fetch('https://taxmtd.uk/api/stripe/subscription')
// Response shape
interface SubscriptionState {
plan: 'starter' | 'essential' | 'pro' | 'business' | 'practice'
status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'none'
currentPeriodEnd: string | null
cancelAtPeriodEnd: boolean
stripeSubscriptionId?: string
}Users with no subscription default to the Starter plan with status: 'active'. The Starter plan is free for non-VAT registered sole traders and requires no card. Starter includes MTD quarterly submissions; the yearly Self-Assessment (SA103S) submission requires Essential or higher (POST /api/filing/submissions with status: 'submitted' returns 403 with { upgrade: true, requiredPlan: 'essential' } on Starter).
Usage Tracking#
Returns current usage counts for all limit-enforced resources. Used to display usage dashboards and client-side limit warnings.
Get Usage#
const { data } = await $fetch('https://taxmtd.uk/api/usage')
// Response shape
interface UsageCounts {
entities: number // Total business entities
invoices_monthly: number // Invoices created this calendar month
ocr_receipts_monthly: number // Receipts scanned this calendar month
team_members: number // Active team members across all entities
inventory_skus: number // Total products
payroll_employees: number // Total employees
bank_connections: number // Total bank connections
}Each key maps to a limit_key in the plan_limits table. The server enforces limits on resource creation endpoints - attempting to exceed a limit returns a 403 with upgrade guidance:
{
"statusCode": 403,
"data": {
"upgrade": true,
"limitKey": "invoices_monthly",
"currentUsage": 50,
"limit": 50,
"baseLimit": 50,
"addonGrant": 0,
"currentPlan": "pro"
}
}The limit field reflects the effective limit (base plan limit + any purchased add-on grants).
Plan Configuration#
Public endpoint - returns all feature gates and usage limits. No authentication required. Cached for 5 minutes.
Get Plan Config#
const { data } = await $fetch('https://taxmtd.uk/api/plan-config')
// Response shape
interface PlanConfig {
features: PlanFeature[]
limits: PlanLimit[]
}
interface PlanFeature {
feature_slug: string
min_plan: 'starter' | 'essential' | 'pro' | 'business' | 'practice'
category: string
label: string
description: string | null
sort_order: number
}
interface PlanLimit {
plan: 'starter' | 'essential' | 'pro' | 'business' | 'practice'
limit_key: string
limit_value: number // -1 = unlimited
}Usage Caps by Plan#
| Resource | Starter | Essential | Pro | Business | Practice |
|---|---|---|---|---|---|
| Bank connections | 1 | 2 | 5 | Unlimited | Unlimited |
| Transactions/mo | 100 | 250 | Unlimited | Unlimited | Unlimited |
| Invoices/mo | - | - | 50 | Unlimited | Unlimited |
| OCR receipts/mo | - | - | 20 | 100 | Unlimited |
| Payroll employees | - | - | - | 5 | 25 |
| Inventory SKUs | - | - | - | 200 | Unlimited |
| Entities | 1 | 1 | 1 | 2 | 25 |
| Team members | 1 | 1 | 3 | 10 | Unlimited |
All limits are enforced server-side. A - means the feature requires a higher plan entirely (gated by plan_features, not by count).
Add-ons#
Extend plan limits without changing your subscription tier. Add-ons are attached to your existing Stripe subscription as additional line items.
Available Add-ons#
| Add-on | Grants | Price |
|---|---|---|
| Extra Entities | +5 entities | £5/mo |
| Extra Employees | +10 payroll employees | £3/mo |
| OCR Receipt Bundle | +50 receipt scans/mo | £5/mo |
List Add-ons#
Returns the full catalog and the user's purchased add-ons.
const { data } = await $fetch('https://taxmtd.uk/api/addons')
// Response shape
interface AddonsResponse {
catalog: AddonDefinition[]
purchased: PurchasedAddon[]
}
interface AddonDefinition {
slug: string
label: string
description: string
unit_label: string
grant_per_unit: number
limit_key: string
price_monthly_pence: number
price_annual_pence: number
}
interface PurchasedAddon {
id: string
addon_slug: string
quantity: number
status: 'active' | 'canceled'
date_created?: string
}Purchase Add-on#
Adds the add-on to the user's active subscription. If no active subscription exists, creates a Stripe Checkout session for the add-on.
addon_slugstringquantitynumberNumber of units to purchase (default: 1)
const { data } = await $fetch('https://taxmtd.uk/api/addons', {
method: 'POST',
body: { addon_slug: 'extra_entities', quantity: 1 }
})
// With active subscription: { success: true, method: 'subscription_item' }
// Without subscription: { url: 'https://checkout.stripe.com/...' }Stripe Checkout#
Creates a Stripe Checkout session to start a new subscription.
priceIdstringStripe Price ID for the plan
planstringPlan slug: starter, essential, pro, business, or practice
intervalstringBilling interval: monthly or annual (default: monthly)
const { data } = await $fetch('https://taxmtd.uk/api/stripe/checkout', {
method: 'POST',
body: {
priceId: 'price_...',
plan: 'pro',
interval: 'annual'
}
})
// Redirect user to: data.urlThe Starter plan is free with no card required (non-VAT sole traders only). All paid plans include a 14-day free trial - the user is not charged until the trial ends.
Customer Portal#
Opens the Stripe Customer Portal where users can update payment methods, change plans, view invoices, and cancel subscriptions. Mid-cycle plan changes are prorated automatically.
const { data } = await $fetch('https://taxmtd.uk/api/stripe/portal', {
method: 'POST'
})
// Redirect user to: data.urlWebhooks#
TaxMTD processes the following Stripe webhook events:
| Event | Action |
|---|---|
checkout.session.completed |
Creates subscription record, handles add-on purchases |
customer.subscription.updated |
Updates plan (via price ID reverse-lookup), status, period end, syncs add-on items |
customer.subscription.deleted |
Marks subscription and all add-ons as canceled |
invoice.payment_failed |
Sets subscription status to past_due |
Plan changes made through the Stripe Customer Portal are automatically detected via price ID mapping - no metadata required.