Skip to content

Billing and Subscriptions

Two route families deal with "subscriptions", and they are unrelated:

Family What it is Section
/billing/* DayZero's own bill for the business (or its advisory firm): plan, add-ons, trial, Stripe Checkout and Billing Portal, invoices, AI quota Everything up to Subscription spend
/subscriptions/* Recurring spend detected in the business's bank transactions (SaaS, rent, insurance). Nothing here talks to Stripe or changes the DayZero plan Subscription spend

All paths are relative to https://api.ondayzero.com/api/v1. Authenticated calls send Authorization: Bearer dz_your_token_here and x-business-id: YOUR_BUSINESS_ID. Responses use the {"success": true, "data": ...} envelope; the examples below show the contents of data. Money is integer cents unless a field says otherwise. Errors follow Error Handling; branch on code. Add an Idempotency-Key header to POSTs you might retry (see Idempotent retries).

Who can call what

Permissions come from the caller's role on the business (see Authentication for tokens):

Role Permissions
Business owner read, write, delete, admin, manage_users, billing
Business admin read, write, delete, admin, manage_users (no billing)
Business member read, write
Business viewer read
Advisory-firm owner (on the firm's clients) all six, including billing
Advisory-firm admin same as business admin (no billing)
Advisory-firm member read, write, delete, admin
Advisory-firm client, viewer read

What each family requires:

  • /billing/* needs the billing permission on every route, reads included, so in practice only the business owner or a firm owner can call it. A business admin gets 403 AUTH_012 ("Permission required: 'billing'"), even on GET /billing/status. The one exception is the two public catalog routes (/billing/catalog, /billing/pricing-config), which need no token.
  • /subscriptions/*: GET routes need read; PUT /subscriptions/ and POST /subscriptions/analyze need write; dismiss, restore and PUT /subscriptions/settings need admin.
  • Expired trial. Write calls on most of the API fail with 403 BILLING_001 once a business is neither trialing nor active. The /billing/* routes are exempt so an expired business can still check out. /subscriptions/* writes are not exempt.
  • Two-factor for firm owners and admins. Every POST under /billing/* also runs the firm MFA gate. If the caller is an owner or admin of an advisory firm that enforces MFA, a long-lived API token cannot satisfy it and gets 403 AUTH_050; use a Clerk session or an OAuth token minted from one.
  • read_only API tokens can call only GET; a POST returns 403 AUTH_048.
  • Partner agreements. A business on the partner_program tier is billed under an agreement, not Stripe. Checkout, plan change, add-on preview/toggle and grandfather migration are refused with 400 BILLING_023. The portal stays open (it is how an older subscription is cancelled).
  • Demo businesses cannot start checkout (400 BUSINESS_064).

Concepts

plan_tier on the business:

Value Meaning
free Entry tier
self_directed Self-service plan
managed Plan with a dedicated DayZero accountant; unlimited AI
firm_client A client business of an advisory firm; billed through the firm
grandfathered Early-adopter access with no payment; keeps its included add-ons
partner_program Billed under a partner agreement; always includes CFO Suite

billing_status: trialing, active, past_due, canceled, unpaid. Stripe statuses map to these (incomplete becomes past_due, incomplete_expired becomes canceled).

Add-on slugs (the addon_slug you send): cfo_suite, commerce, qoe_diligence, tax_advisory, fixed_assets, ap_automation, ai_pro.

Firm tiers (firm_plan_tier): firm_starter, firm_professional, firm_enterprise.

Firm tier Client cap AI requests per client per month Add-ons bundled for every client
firm_starter 5 200 none
firm_professional 25 500 cfo_suite, fixed_assets, tax_advisory
firm_enterprise unlimited 1500 all seven

Source: FIRM_CLIENT_LIMITS, FIRM_AI_QUOTAS and FIRM_BUNDLED_ADDONS in app/enums/billing.py.

Plans and add-ons

Both routes are public and cached for 10 minutes.

Price catalog

bash
curl "https://api.ondayzero.com/api/v1/billing/catalog"

data maps a catalog slug to its Stripe price:

json
{
  "dayzero_self_directed": {
    "stripe_price_id": "price_XXXXXXXX",
    "stripe_product_id": "prod_XXXXXXXX",
    "price_amount": 4200,
    "interval": "month",
    "is_metered": false
  },
  "dayzero_self_directed_annual": {
    "stripe_price_id": "price_YYYYYYYY",
    "stripe_product_id": "prod_ZZZZZZZZ",
    "price_amount": 42000,
    "interval": "year",
    "is_metered": false
  }
}

price_amount is cents per interval. Use stripe_price_id as the price_id for checkout and plan changes. The live values come from the database, so always read them from this route rather than hard-coding them; the amounts in these examples are illustrative.

The slugs are fixed:

Group Slugs
Plans dayzero_self_directed, dayzero_managed
Firm tiers dayzero_firm_starter_client, dayzero_firm_pro_platform, dayzero_firm_pro_client, dayzero_firm_enterprise_platform, dayzero_firm_enterprise_client (plus the legacy flat-firm slugs dayzero_firm_platform, dayzero_firm_client)
Add-ons addon_cfo_suite, addon_commerce, addon_qoe_diligence, addon_tax_advisory, addon_fixed_assets, addon_ap_automation, addon_ai_pro
Metered AI overage ai_usage_overage, ai_usage_overage_light, ai_usage_overage_heavy (is_metered: true)

Every monthly entry except the two legacy flat-firm slugs also has a <slug>_annual entry billed yearly; pick the monthly or _annual slug to choose the cadence.

Annual discount

bash
curl "https://api.ondayzero.com/api/v1/billing/pricing-config"
json
{ "annual_discount_pct": 0.1, "annual_discount_label": "Save with annual billing" }

annual_discount_pct is the fraction taken off twelve months of the monthly price for the _annual entries, and annual_discount_label is the text to show next to it. Read both from the route; the values above are illustrative.

Current subscription and entitlements

bash
curl "https://api.ondayzero.com/api/v1/billing/status" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "billing_status": "active",
  "plan_tier": "self_directed",
  "firm_plan_tier": null,
  "trial_ends_at": null,
  "trial_days_remaining": 0,
  "is_billing_active": true,
  "has_stripe_subscription": true,
  "active_addons": ["ai_pro"],
  "grandfathered_addons": [],
  "firm_bundled_addons": [],
  "unlocked_addons": ["ai_pro"],
  "subscription_period_end": "2026-11-03T00:00:00+00:00",
  "monthly_total": 4200,
  "annual_total": 50400,
  "billing_interval": "month",
  "base_plan_price": 3000,
  "is_demo": false
}
Field Notes
active_addons Paid add-ons from the Stripe subscription, plus the firm tier's bundled add-ons for a firm_client
grandfathered_addons Add-ons included free by grandfathering or a partner agreement
firm_bundled_addons Add-ons the firm tier bundles; they also appear in active_addons but cannot be toggled by the client
unlocked_addons The entitlement set to gate on. Folds in an active trial or demo (all seven), paid add-ons, grandfathered add-ons (grandfathered and partner tiers), CFO Suite for partner tiers, and firm-bundled add-ons
monthly_total, annual_total Cents, summed from the live Stripe subscription's recurring, non-metered items. monthly_total is monthly-equivalent for annual subscriptions. Both are 0 with no subscription or if Stripe could not be reached
base_plan_price Cents, the monthly catalog price for the tier (a firm client's tier price for firm_client). free and grandfathered report the dayzero_self_directed list price; partner_program reports 0. It is a list price, not what Stripe bills
billing_interval month or year; month when there is no subscription
subscription_period_end ISO timestamp of the current period end; null without a subscription

The response is cached for 5 minutes per business. Writes in this guide normally evict it, but a change made in the Stripe portal arrives through Stripe's webhook, and a firm tier change can show up late in firm_plan_tier, so allow a short delay.

Trial state

A business gets a 30-day trial from creation (TRIAL_DURATION_DAYS in app/enums/billing.py). The status fields that describe it:

  • billing_status is trialing (a missing status is read as trialing) and trial_ends_at is in the future: the trial is active. A missing trial_ends_at never counts as an active trial.
  • trial_days_remaining is whole days left, rounded down, and 0 when there is no trial_ends_at or it has passed.
  • is_billing_active is true when billing_status is active, the business is a demo, or the trial is active. Grandfathered, partner and firm-client tiers skip the write gate regardless.
  • During an active trial every add-on appears in unlocked_addons, and AI is unmetered (see AI usage).
  • When the trial ends without a subscription, is_billing_active turns false and write calls elsewhere return 403 BILLING_001 until checkout completes. The same applies to past_due, canceled and unpaid.

Checkout

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/checkout" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "price_id": "price_XXXXXXXX",
    "success_url": "https://app.ondayzero.com/settings?section=billing&checkout=success",
    "cancel_url": "https://app.ondayzero.com/settings?section=billing"
  }'
json
{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_...", "session_id": "cs_..." }

Requires billing. Send the user to checkout_url.

  • price_id must be a Stripe price from the catalog; anything else is 400 BILLING_016. Pick the monthly or _annual slug to choose the cadence.
  • success_url and cancel_url must be absolute http(s) URLs on DayZero's own app or website origin; anything else is 400 BILLING_017.

In Stripe: the call creates a Customer for the business if it has none (named after the business, with the caller's email and dayzero_business_id metadata) and a Checkout Session in subscription mode with one line item, quantity 1. Promotion codes are allowed, billing address and tax IDs are collected, and automatic tax is on (it is retried without automatic tax if the price has no tax behavior set). No trial period is added.

In DayZero: only the new Stripe customer ID is saved on the business during the call. The subscription ID, billing_status, items and active_addons are written later, when Stripe's customer.subscription.created webhook arrives, so poll GET /billing/status after the redirect rather than assuming the change is instant. Plain checkout does not rewrite plan_tier.

Billing Portal link

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/portal" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"return_url": "https://app.ondayzero.com/settings?section=billing"}'
json
{ "portal_url": "https://billing.stripe.com/p/session/..." }

Requires billing. The business must already have a Stripe customer (400 BILLING_022 otherwise); return_url follows the same origin rule as checkout. Creating the link changes nothing. Whatever the user does in the portal (card, cancellation) happens in Stripe, and DayZero follows through webhooks: a deleted subscription sets billing_status to canceled and clears the subscription ID (not for grandfathered or partner tiers), a failed payment sets past_due, and a successful invoice moves past_due or unpaid back to active.

Add-ons

Add-on changes act on the business's existing Stripe subscription, so the business needs one (400 BILLING_007 otherwise; the preview is the one exception below). The add-on is priced at the same interval as the subscription, so an annual subscription gets the _annual add-on price. Preview and toggle both refuse an add-on that is included free by grandfathering (400 BILLING_008) and an unknown slug (400 BILLING_010). (A grandfathered business with no subscription may still preview an add: prorated_amount is then null.)

Preview

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/addon/preview" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"addon_slug": "ai_pro", "action": "add"}'
json
{ "prorated_amount": 1210, "addon_price": 2500, "action": "add" }

Read-only (it is a POST only because it takes a body). action is add or remove.

  • add: prorated_amount is the proration Stripe would invoice now, in cents. It is null when Stripe could not produce a preview; show "calculated by Stripe", never $0.00. addon_price is the catalog price for the subscription's interval. 400 BILLING_008 if already active.
  • remove: {"prorated_amount": 0, "addon_price": 2500, "effective_date": "2026-11-03T00:00:00+00:00", "action": "remove"}; effective_date is the subscription's current period end. 400 BILLING_009 if the add-on is not active.

Add or remove

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/addon" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"addon_slug": "ai_pro", "action": "add"}'
json
{ "status": "ok", "action": "add", "addon_slug": "ai_pro", "active_addons": ["ai_pro"], "effective_date": null }

Requires billing. effective_date is only set for remove.

Action In Stripe In DayZero
add Adds the add-on's price as a subscription item; the proration is invoiced immediately (always_invoice) active_addons and the stored items are rewritten from the updated subscription in the same request
remove Deletes the item with no proration and no refund Same rewrite, in the same request. effective_date is the end of the already-paid period

400 BILLING_008 / BILLING_009 if the add-on is already active / not active, 400 BILLING_011 if Stripe rejects the change. A firm client has no subscription of its own, so it cannot toggle add-ons (BILLING_007); its bundled add-ons arrive through the firm tier.

Changing the base plan

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/change-plan" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"price_id": "price_XXXXXXXX"}'
json
{ "status": "ok", "subscription_id": "sub_..." }

Requires billing and an existing subscription (400 BILLING_007). In Stripe it swaps the base item (the one item that is neither an add-on nor metered) to price_id, moves every add-on to the new price's interval (Stripe cannot mix monthly and annual items), and invoices the proration immediately. In DayZero it rewrites the stored items and active_addons from the updated subscription; it does not write plan_tier. price_id must come from the catalog (400 BILLING_016).

Grandfathered accounts

bash
curl "https://api.ondayzero.com/api/v1/billing/grandfathered-summary" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Returns plan_tier, is_grandfathered, current_access (all_addons_included, addons, ai_monthly_limit, has_ai_pro, billing_status, no_payment_required) and available_plans, a list of {plan_tier, catalog_slug, price_amount, interval} for self_directed and managed (the monthly catalog entries; price_amount in cents). Callable by any business; is_grandfathered is false for others. Cached for 5 minutes.

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/migrate-from-grandfathered" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "target_plan": "managed",
    "billing_interval": "year",
    "success_url": "https://app.ondayzero.com/settings?section=billing&checkout=success",
    "cancel_url": "https://app.ondayzero.com/settings?section=billing"
  }'

Returns {"checkout_url": "...", "session_id": "cs_..."}. target_plan is self_directed or managed (400 BILLING_016 otherwise); billing_interval is month (default) or year. The business must be grandfathered (400 BILLING_014). It creates the same kind of Checkout Session as above, tagged with the migration target. Only when Stripe reports the checkout complete does DayZero set plan_tier to the target and billing_status to active.

Firm tiers and client usage

Client usage

bash
curl "https://api.ondayzero.com/api/v1/billing/firm/client-usage" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "firm_id": "019ab37c-b051-7000-8000-000000000010",
  "is_advisory_firm": true,
  "firm_plan_tier": "firm_starter",
  "active_clients": 4,
  "client_limit": 5,
  "seats_remaining": 1,
  "can_add_client": true,
  "tiers": [
    { "value": "firm_starter", "client_limit": 5 },
    { "value": "firm_professional", "client_limit": 25 },
    { "value": "firm_enterprise", "client_limit": null }
  ]
}

Resolved from the firm that x-business-id belongs to. client_limit and seats_remaining are null for an unlimited tier. A business with no active firm gets is_advisory_firm: false, firm_plan_tier: null, active_clients: 0, can_add_client: true and tiers: [] (and no firm_id). Like every /billing route it needs billing.

Change tier

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/firm/change-tier" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"firm_plan_tier": "firm_professional"}'
json
{ "status": "ok", "firm_plan_tier": "firm_professional", "previous_tier": "firm_starter", "active_clients": 4 }

Who may call it. The handler checks that the caller is an owner or admin of the firm, but the route first needs the billing permission, which firm admins do not have. In practice only a firm owner gets through; an admin receives 403 AUTH_012. The MFA rule above applies.

What it changes.

  • DayZero: the firm's firm_plan_tier, which sets the client cap, the AI quota and the bundled add-ons for every client business.
  • Stripe (only if the firm's anchor business already has a subscription): one Subscription.modify that swaps the per-client item to the new tier's price (same interval as the current subscription), sets its quantity to the active client count (at least 1), adds, replaces or removes the platform-fee item, and invoices the proration immediately. With no subscription yet (still trialing) only the stored tier changes.

Errors. 400 BILLING_019 for a tier outside the three values, or a firm that is not on an advisory tier. 400 BILLING_018 when the target tier's cap is below the current client count (downgrade blocked). 400 when x-business-id is not linked to a firm. 400 BILLING_005 if the target tier's price is missing from the catalog. Adding a client beyond the cap fails the same way with BILLING_018; the client count is pushed to Stripe with create_prorations, billed on the next invoice. See Advisory Firms for firm and client management.

AI usage and quotas

Current month

bash
curl "https://api.ondayzero.com/api/v1/billing/ai-usage" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "requests_used": 540,
  "monthly_limit": 500,
  "remaining": 0,
  "is_unlimited": false,
  "plan_tier": "self_directed",
  "has_ai_pro": false,
  "is_trial": false,
  "overage_enabled": true,
  "overage_requests": 40,
  "overage_rates_cents": { "light": 1, "standard": 3, "heavy": 9 },
  "overage_breakdown": { "light": 10, "standard": 25, "heavy": 5 },
  "estimated_overage_cost_cents": 150,
  "projected_month_end_overage_cents": 300,
  "ai_paused_at": null,
  "ai_paused_by": null,
  "upgrade_suggestion": {
    "addon_or_plan_slug": "addon_ai_pro",
    "label": "AI Pro",
    "monthly_quota": 2000,
    "monthly_price_cents": 2500,
    "would_save_cents": 0
  },
  "disabled_features_when_paused": ["Regent — chat, command bar, and agent actions ...", "..."]
}

requests_used counts the current calendar month (UTC). The monthly limit comes from the plan:

Plan Monthly AI requests
free 50
self_directed, grandfathered, partner_program 500
managed unlimited
firm_client by firm tier: 200 / 500 / 1500
Any plan with ai_pro (or a firm tier that bundles it) 2000

(_MONTHLY_QUOTAS in app/api/v1/services/billing_services.py, FIRM_AI_QUOTAS in app/enums/billing.py.) An active trial and the managed plan report is_unlimited: true with monthly_limit: 0 and remaining: 0; 0 there means "no limit", not "none left".

Overage. overage_enabled is true on self_directed and firm_client, and on any business with AI Pro, and false for free, grandfathered and unlimited cases (those stop at the quota). Overage is charged per request by size: overage_rates_cents gives the rate in cents for a light, standard and heavy request. overage_breakdown splits overage_requests across the three sizes in proportion to the month's request weights, estimated_overage_cost_cents prices that split, and projected_month_end_overage_cents extrapolates linearly to month end.

upgrade_suggestion is null for unlimited or trial businesses and when there is nothing to suggest. Otherwise it names the add-on or plan (addon_ai_pro, dayzero_managed or dayzero_self_directed) and a would_save_cents hint computed from fixed list prices in the service, not from the live catalog, so treat it as a nudge rather than a quote.

Details

bash
curl "https://api.ondayzero.com/api/v1/billing/ai-usage/details?scope=firm" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Returns summary, top_users and daily. summary has requests_used, monthly_limit, is_unlimited, total_tokens, estimated_cost_usd (a float in dollars, unlike the cent fields), unique_users, plan_tier, has_ai_pro and is_trial. top_users entries are {user_id, name, requests, tokens, pct} for the current month (name is "System" for requests with no user). daily is {date, requests, tokens} for roughly the last 40 days. A per-model breakdown (by_model) is computed but removed from the response for everyone except superusers, so API callers do not receive it.

scope=firm aggregates the totals, top users and daily series over every active business of the firm that the caller can access (400 if the business is not linked to a firm). plan_tier, monthly_limit and the other plan fields still describe the business in x-business-id. Any other scope value is ignored. Cached for 5 minutes.

Pause and resume

bash
curl -X POST "https://api.ondayzero.com/api/v1/billing/ai/pause" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

curl -X POST "https://api.ondayzero.com/api/v1/billing/ai/resume" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Both need billing (so a business admin cannot pause), take no body and return the refreshed ai-usage payload, with ai_paused_at and ai_paused_by (the pausing user's name, or email if they have no name) set or cleared. They are idempotent: pausing an already-paused business keeps the original timestamp and user. This is DayZero-side only; nothing changes in Stripe. While paused, AI calls are refused by the quota check, AI usage tracking (and with it metered overage reporting) stops, and bank-sync cleaning falls back to rules only. disabled_features_when_paused lists, in display order, the feature groups that stop. Both calls evict the cached billing status and AI usage.

Invoices

bash
curl "https://api.ondayzero.com/api/v1/billing/invoices?limit=12" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "invoices": [
    {
      "id": "in_...",
      "number": "ABCD1234-0001",
      "status": "paid",
      "amount_due": 4200,
      "amount_paid": 4200,
      "currency": "usd",
      "created": 1761955200,
      "period_start": 1761955200,
      "period_end": 1764547200,
      "hosted_invoice_url": "https://invoice.stripe.com/i/...",
      "invoice_pdf": "https://pay.stripe.com/invoice/.../pdf"
    }
  ]
}

These are the Stripe subscription invoices for DayZero's own bill, not the customer invoices in Invoices. limit is 1 to 100 (default 24); there is no cursor, so raise limit to see older ones. status is Stripe's invoice status; created, period_start and period_end are Unix seconds; amounts are cents. hosted_invoice_url and invoice_pdf are Stripe-hosted links. A business with no Stripe customer gets {"invoices": []}. Cached for 5 minutes. For cards and a full history, use the portal link.

Subscription spend (/subscriptions)

DayZero scans the business's bank outflows over a lookback window and reports vendors that charge on a steady cadence. A vendor qualifies with at least 3 charges whose amounts are consistent (coefficient of variation at most 0.25) and whose average gap fits weekly, biweekly, monthly, quarterly, semi-annual or annual. Customer-typed counterparties are excluded. Monthly cost is the average charge times a cadence multiplier, capped by what the data supports (total spend divided by months observed). /subscriptions has no add-on check in code, but its writes are subject to the expired-trial gate above. For the in-app view see the Detected Subscriptions article in the Knowledge Base.

Method Path Permission Purpose
GET /subscriptions/ read Detected list with totals
GET /subscriptions/summary read Total, budget, top 5
GET /subscriptions/history read Monthly totals for charting
PUT /subscriptions/ write Override name, amount, cadence, notes
POST /subscriptions/dismiss admin Hide a vendor as not recurring
POST /subscriptions/restore admin Undo a dismissal
GET /subscriptions/settings read Read the monthly budget
PUT /subscriptions/settings admin Set or clear the monthly budget
POST /subscriptions/analyze write AI savings suggestions

None of these are paginated.

List

bash
curl "https://api.ondayzero.com/api/v1/subscriptions/?lookback_days=180&sort_by=cost_desc" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

lookback_days is 30 to 730 (default 180). sort_by is cost_desc (default), cost_asc, frequency, confidence_desc, recent or occurrences_desc; an unknown value silently falls back to cost_desc.

json
{
  "subscriptions": [
    {
      "counterparty_key": "notion labs",
      "counterparty_display": "Notion Labs",
      "category": "Software",
      "registry_id": null,
      "avg_amount_cents": -4800,
      "last_amount_cents": -4800,
      "estimated_monthly_cents": 4800,
      "period_total_cents": 28800,
      "frequency": "monthly",
      "occurrence_count": 6,
      "last_date": "2026-10-02",
      "next_expected_date": "2026-11-01",
      "confidence": 1.0,
      "custom_name": null,
      "custom_amount_cents": null,
      "custom_frequency": null,
      "notes": null,
      "has_overrides": false
    }
  ],
  "total_monthly_cents": 4800,
  "period_total_cents": 28800,
  "monthly_limit_cents": 10000,
  "over_limit": false,
  "subscription_count": 1,
  "lookback_days": 180
}

Charge amounts (avg_amount_cents, last_amount_cents) are negative because they are outflows; estimated_monthly_cents and period_total_cents are positive. counterparty_key is the trimmed, lower-cased counterparty name and is the identifier every write takes. over_limit is true when total_monthly_cents exceeds monthly_limit_cents.

GET /subscriptions/summary returns total_monthly_cents, monthly_limit_cents, over_limit, subscription_count and top_subscriptions (the top 5, same shape as above) using the default 180-day window. GET /subscriptions/history?lookback_days=365 (30 to 730, default 365) returns {"months": [{"month": "2026-09", "total_cents": 4800, "breakdown": [{"counterparty_key": "notion labs", "cents": 4800}]}], "lookback_days": 365}, oldest month first, for the vendors in the current detected list.

Override a detected item

bash
curl -X PUT "https://api.ondayzero.com/api/v1/subscriptions/" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "counterparty_key": "notion labs",
    "custom_name": "Notion (team plan)",
    "custom_amount_cents": 5200,
    "custom_frequency": "monthly",
    "notes": "Renews on the 1st"
  }'

Returns {"status": "updated", "counterparty_key": "notion labs"}. Limits: custom_name 500 characters, custom_frequency 50, notes 2000. custom_amount_cents replaces the item's estimated monthly cost and custom_name its display name. The four override fields are replaced together: any you omit (or send empty) are cleared. Overrides are display-only; no transaction is changed.

Dismiss and restore

bash
curl -X POST "https://api.ondayzero.com/api/v1/subscriptions/dismiss" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"counterparty_key": "notion labs"}'

Returns {"status": "dismissed", "counterparty_key": "notion labs"}. A dismissed vendor stays out of every list, summary and history until restored. registry_id is optional. POST /subscriptions/restore with {"counterparty_key": "..."} returns {"status": "restored", ...} and deletes the vendor's whole override record, so any custom name, amount, cadence and notes are lost too.

Monthly budget

bash
curl -X PUT "https://api.ondayzero.com/api/v1/subscriptions/settings" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"monthly_limit_cents": 250000}'

Returns {"monthly_limit_cents": 250000}. Send null to remove the limit. GET /subscriptions/settings returns the same shape.

AI analysis

POST /subscriptions/analyze (no body) sends up to 40 detected items to the AI model and returns {"insights": [...], "total_monthly_spend": 4800}. Each insight has type (duplicate, price_increase, optimization or unused), title, description, and optional subscription_id (the counterparty_key) and potential_savings (cents). With nothing detected it returns {"insights": [], "total_monthly_spend": null} without calling the model. It makes a model call on each request.

Error summary

Status Code When
400 BILLING_005 Stripe or the catalog is not configured for the requested price
400 BILLING_007 No Stripe subscription (add-on, plan change)
400 BILLING_008 / BILLING_009 Add-on already active (or grandfathered) / not active
400 BILLING_010 / BILLING_011 Unknown add-on slug / Stripe rejected the add-on change
400 BILLING_014 Business is not grandfathered
400 BILLING_016 price_id not in the catalog, or invalid target_plan
400 BILLING_017 Checkout could not be created, including a redirect URL on another origin
400 BILLING_018 / BILLING_019 Firm client cap reached or exceeded / invalid firm tier
400 BILLING_022 No Stripe customer yet (portal)
400 BILLING_023 Business is billed under a partner agreement
400 BUSINESS_064 Demo business cannot check out
403 AUTH_012 Caller lacks the billing (or admin) permission
403 AUTH_050 Firm owner/admin needs a two-factor session (API tokens never qualify)
403 AUTH_048 read_only token on a write
403 BILLING_001 Write blocked: trial ended or subscription not active

Not covered

  • Stripe's inbound billing webhook and the superuser-only grandfathering route are not part of the public API.
  • Firm and client management, assignments and invitations are in Advisory Firms.
  • Creating an advisory firm's first subscription happens during onboarding, not through these routes.