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 thebillingpermission on every route, reads included, so in practice only the business owner or a firm owner can call it. A businessadmingets403AUTH_012("Permission required: 'billing'"), even onGET /billing/status. The one exception is the two public catalog routes (/billing/catalog,/billing/pricing-config), which need no token./subscriptions/*:GETroutes needread;PUT /subscriptions/andPOST /subscriptions/analyzeneedwrite; dismiss, restore andPUT /subscriptions/settingsneedadmin.- Expired trial. Write calls on most of the API fail with
403BILLING_001once a business is neither trialing noractive. 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
POSTunder/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 gets403AUTH_050; use a Clerk session or an OAuth token minted from one. read_onlyAPI tokens can call onlyGET; aPOSTreturns403AUTH_048.- Partner agreements. A business on the
partner_programtier is billed under an agreement, not Stripe. Checkout, plan change, add-on preview/toggle and grandfather migration are refused with400BILLING_023. The portal stays open (it is how an older subscription is cancelled). - Demo businesses cannot start checkout (
400BUSINESS_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
curl "https://api.ondayzero.com/api/v1/billing/catalog"data maps a catalog slug to its Stripe price:
{
"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
curl "https://api.ondayzero.com/api/v1/billing/pricing-config"{ "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
curl "https://api.ondayzero.com/api/v1/billing/status" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"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_statusistrialing(a missing status is read astrialing) andtrial_ends_atis in the future: the trial is active. A missingtrial_ends_atnever counts as an active trial.trial_days_remainingis whole days left, rounded down, and0when there is notrial_ends_ator it has passed.is_billing_activeistruewhenbilling_statusisactive, 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_activeturnsfalseand write calls elsewhere return403BILLING_001until checkout completes. The same applies topast_due,canceledandunpaid.
Checkout
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"
}'{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_...", "session_id": "cs_..." }Requires billing. Send the user to checkout_url.
price_idmust be a Stripe price from the catalog; anything else is400BILLING_016. Pick the monthly or_annualslug to choose the cadence.success_urlandcancel_urlmust be absolutehttp(s)URLs on DayZero's own app or website origin; anything else is400BILLING_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
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"}'{ "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
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"}'{ "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_amountis the proration Stripe would invoice now, in cents. It isnullwhen Stripe could not produce a preview; show "calculated by Stripe", never$0.00.addon_priceis the catalog price for the subscription's interval.400BILLING_008if already active. - remove:
{"prorated_amount": 0, "addon_price": 2500, "effective_date": "2026-11-03T00:00:00+00:00", "action": "remove"};effective_dateis the subscription's current period end.400BILLING_009if the add-on is not active.
Add or remove
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"}'{ "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
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"}'{ "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
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.
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
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"{
"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
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"}'{ "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.modifythat 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
curl "https://api.ondayzero.com/api/v1/billing/ai-usage" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"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
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
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
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"{
"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
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.
{
"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
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
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
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.