Client Contracts & Invoicing
Create billing contracts, manage recurring items, and send invoices
Overview
Client contracts define the commercial terms between your agency and each client: what they pay, how often, and how payment is collected. The billing system supports Stripe-managed subscriptions, send-invoice flows, and fully manual invoicing.
A client can have more than one live recurring contract running side by side: one monthly group, and a separate group for each annual renewal month that has services on it. Each group is its own card in the admin UI with its own services, its own Stripe subscription or schedule, its own next-invoice date, and its own term — editing or ending one never touches the others. This is what lets a client run monthly and annual billing at the same time without one change accidentally affecting the other.
Creating a Contract
- Open a client record.
- Go to the Billing tab.
- Create or update the billing contract.
A contract contains:
| Field | Description |
|---|---|
| Collection mode | charge_automatically, send_invoice, or manual_only |
| Recurring items | Products/services billed every cycle |
| Scheduled additions | One-time or future-dated line items |
| Discounts | Percentage or fixed-amount reductions |
| Billing Day | The anchor day (1–28) monthly items are charged on |
| Start Date | When the contract's billing periods begin |
| Stripe subscriptions | One entry per active Stripe subscription driving the contract — see below |
Recurring Items
Recurring items are the core of the contract — services that renew every billing cycle. Each item references a product from your catalog and carries a quantity and unit price.
Editing Services On A Live Contract
Once a contract is converted to Stripe, each recurring-contract card in Client → Billing → Contract Services has its own controls:
| Action | What It Does |
|---|---|
| Edit a service | Opens Review Service Change — a confirmation step showing exactly what will change before you commit. Syncs to that card's Stripe subscription and takes effect on the next unissued invoice — never a mid-cycle charge or credit. |
| Add Service / Remove | Adds or removes a line item on that specific card. Because the action lives inside the card, the service always lands on the right cadence and renewal date — it can't accidentally attach to the client's other active contract. |
| End Contract | Ends that one monthly or annual card after its current paid cycle. The client's other recurring contract, if they have one, keeps billing normally. |
| Extend | Adds invoice cycles to that card's term — turns an open-ended contract into a timed one, or adds more cycles to an existing timed contract. |
Each card also shows when it started, its next invoice date and status (upcoming, scheduled, draft, or issued), its end date (or Ongoing if open-ended), and how many invoices remain.
An invoice that has already been issued is immutable — a service edit is called out as starting with the next unissued invoice instead. Confirmed edits update future Stripe phases or subscription items and any unsent local scheduled invoices.
Scheduled Additions
Scheduled additions let you plan one-off charges in advance (e.g., a setup fee in month 1, a campaign boost in month 3). They attach to a specific billing cycle and do not renew.
Discounts
Apply discounts at the contract level to reduce the invoice total. Discounts can be a fixed amount or a percentage, and can be set to expire after a number of cycles.
Stripe-Managed Recurring Billing
For charge_automatically and some send_invoice contracts, you can convert the contract to Stripe subscriptions:
- On the client contract, use the Convert to Stripe Subscription action.
- The system creates Stripe subscriptions bucketed by cadence rather than a single subscription: one subscription covers all monthly recurring items, and each yearly item gets its own subscription (yearly items that share the same anniversary date bundle into one subscription/invoice together).
- Each subscription bucket is tracked as its own entry on the contract (subscription ID, schedule ID, its own billing anchor day, and status), so monthly and yearly items can be billed, cancelled, or re-created independently of each other.
- Stripe generates invoices automatically each cycle and fires webhook events.
- The webhook handler mirrors invoice and charge state into MongoDB and refreshes the lifetime ledger.
charge_automatically contracts rely on Stripe Smart Retries. The local reminder sweep skips these contracts so customers do not receive duplicate dunning messages.
Configure retry behavior in the Stripe Dashboard under Revenue Recovery.
Ending One Cadence Without Ending Everything
Because monthly and yearly items bill on separate Stripe subscriptions, you can end one without touching the other:
- On the client contract's Contract Services list, open the specific monthly or annual card.
- Use End Contract on that card (see Editing Services On A Live Contract above).
- Only that card's Stripe subscription is cancelled after its current paid cycle — the client's other active recurring contract, if they have one, keeps billing normally.
This is useful when a client drops a yearly add-on but keeps their monthly plan, or vice versa.
Billing Timezone & Anchors
Monthly and yearly billing-cycle anchors are computed against your agency's configured billing timezone (see Billing Setup) so a charge lands on the intended calendar day locally, rather than shifting a day earlier under a UTC anchor.
Send-Invoice Flow
For send_invoice contracts, the billing scheduler materializes invoices locally and then you send them through Stripe:
- The daily scheduler runs
materializeContractInvoiceForClientto create the local invoice record. - From the admin UI, use the Send Invoice action to call
sendStripeInvoice. - Stripe creates a hosted invoice page and returns the URL.
- The local invoice record stores
stripeInvoiceIdandstripeHostedInvoiceUrl. - The client receives an email from Stripe with a link to view and pay the invoice.
Hosted Invoice Page
Stripe hosts the payment page and handles payment collection. The portal can link clients directly to the hosted invoice page so they can view, pay, and download PDFs.
Reference: Stripe Hosted Invoices
Manual Invoices
For manual_only contracts:
- Create the invoice from the admin UI with the line items and amounts.
- Record payments manually when received.
- The lifetime ledger tracks all manual payments alongside Stripe payments.
Manual invoices do not go through Stripe — they exist only in MongoDB. Use this for clients who pay by check, bank transfer, or through an external billing system.
One-Time Invoices
Need to bill a client for something outside their normal contract — like a rush project fee or a one-off job? You can create a one-time invoice whenever you need to.
One-time invoices are fully standalone from the client's recurring contract:
- Creating one does not touch the client's ongoing recurring services.
- It is not blocked by an existing invoice for that billing period — you can create as many one-time invoices as you need in a given month.
This makes one-time invoices a safe way to bill for extra work without disturbing the client's regular recurring billing.
Reminder Sweep
The billing scheduler runs sweepOutstandingInvoiceReminders daily:
- Applies to
send_invoiceandmanual_onlycontracts, plus anycharge_automaticallycontract with no card on file. Acharge_automaticallyclient with a saved card is handled by Stripe's own retry logic instead. Without a saved card, Stripe has nothing to retry, so the reminder sweep covers that client until they add one. - Sends reminders based on the policy configured in Config → Billing (e.g., 3 days before due, 1 day after, 7 days after).
- Each reminder is recorded on the invoice's
reminderHistoryso you can see exactly what was sent and when.
If you change the reminder policy, it only affects future reminders. Already-sent reminders are not retroactively updated.
Collecting an Overdue Invoice Manually
Admin → Billing → Overview shows two lists: the Overdue Ledger (past due) and the Upcoming Ledger (not due yet). Each invoice row has an action button, and each invoice number is a link you can click to open that invoice's full detail view.
The invoice detail view now leads with a plain notice summary — how many reminders went out and when the last one was sent, with a badge if one failed — instead of a raw list you had to count yourself. The full history is still there behind a disclosure if you need it. It also names where the invoice came from: which contract it was generated from, how that contract collects payment, and a button through to Contract Services if the billing itself needs to change. An invoice minted from a contract can't be re-aimed on its own — the note makes that clear instead of leaving you to guess why editing the invoice directly doesn't do anything.
| Button | When it shows | What it does |
|---|---|---|
| Charge Now | The client already has a card on file | Charges that card for the invoice's outstanding balance right away |
| Request Card | No card is on file yet | Emails the client a secure Stripe link to save a card. Nothing is charged when they add it |
Clicking Charge Now opens a confirm step showing the exact amount and client name before anything happens. This moves money immediately and cannot be undone from the app — if you need to reverse it, issue a refund in Stripe.
If the charge doesn't go through, the app tells you why instead of a generic error:
- No card on file — use Request Card instead.
- Bank verification needed — some banks require the customer to approve the charge themselves. You'll get a copyable payment link to send them.
- Card declined — the card issuer turned down the charge.
If the client's card request email fails to send (a bad address, an email provider hiccup), the app gives you a copyable link so you can send it yourself instead of losing the request.
Charge Now and Send Invoice are limited to agency admins. A Sales Rep can still send a reminder notice or request a card from a client, but cannot trigger a charge themselves — this keeps the person who benefits from commission separate from the person who can move the client's money. See Team Members for role details.
Lifetime Ledger
Every client has a lifetime ledger on their BillingAccount document. It aggregates:
- Total revenue from all invoices (Stripe and manual)
- Total charges mirrored from Stripe
- Total refunds
- Current balance
The ledger is refreshed:
- On every webhook event related to charges, refunds, and payments (
refreshBillingAccountLedger) - By the daily charge reconciliation job, which repairs mirror and ledger drift
Charge Mirroring
Stripe charge.succeeded and charge.refunded events create or update BillingCharge records in MongoDB. These mirrors are the source for ledger reconciliation and reporting.
If webhooks or reconciliation detect drift, run:
node scripts/test-billing-engine.mjs --phase ledger
before modifying billing code.
Tax Handling
New Stripe invoices and subscriptions are created with automatic_tax: { enabled: true }. This means Stripe calculates tax based on the customer's location data.
For existing Stripe subscriptions created before automatic tax was enforced:
- Confirm the customer has sufficient tax location data in Stripe.
- Use the client contract Enable Tax action.
Local tax snapshots remain on invoice lines for historical reporting and preview consistency, even when Stripe is calculating tax automatically.
Verification
node --check utils/stripe-billing.js
node --check utils/billing-scheduler.js
node --check routes/admin-billing.js
node scripts/test-billing-engine.mjs
