Billing, Usage & Add-ons

Subscription management, usage tracking, plan limits enforcement, add-on purchases, and Stripe integration.

Subscription Status#

Returns the current user's subscription plan and status.

Get Subscription#

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_slugstring

One of: extra_entities, extra_employees, ocr_bundle

quantitynumber

Number 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.

priceIdstring

Stripe Price ID for the plan

planstring

Plan slug: starter, essential, pro, business, or practice

intervalstring

Billing 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.url

The 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.url

Webhooks#

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.

Was this page helpful? Share it.