Skip to main content

Documentation

No results found.
Features

Client Billing

Client Billing lets the site owner bill their clients directly from the dashboard: build a line-item invoice (or estimate), email the client a tokenized link, and get paid online through embedded Stripe checkout — or record checks, cash, an...

Client Billing lets the site owner bill their clients directly from the dashboard: build a line-item invoice (or estimate), email the client a tokenized link, and get paid online through embedded Stripe checkout — or record checks, cash, and bank transfers by hand. Recurring schedules, automatic reminders, late fees, deposits, partial payments, refunds, PDF copies, a member billing portal, and CRM/Activity Log integration are all built in.


The problem

Freelancers and small agencies run their websites in one place and their billing in another — a separate invoicing SaaS with its own subscription, its own client list, and a per-payment surcharge on top of Stripe's. For a business that sends a handful of invoices a month, that's a whole extra tool (and bill) for what amounts to a document, a payment link, and a ledger.

The fix

A self-contained feature module under app/Features/ClientInvoicing/ turns the site into the billing tool: a client list, an invoice/estimate builder with line items and tax, a public pay-online page served from the install's own domain, a payment ledger, and an hourly automation tick that generates recurring invoices, sends reminders, and applies late fees — all in the install's own database, with Stripe's fees the only cut taken.

Like every addon, it's structured as a feature module gated by feature:client_invoicing middleware. The service provider boots unconditionally; routes 404 and the sidebar entry hides when the feature is off. Toggling it on runs the module's migrations. Disabling preserves all data.

What the dashboard gets

Four pages under Dashboard → Invoicing (Invoices, Clients, and Recurring need Manager and up; Settings is Admin-only):

  • Invoices — the ledger: Outstanding / Drafts / Estimates / Paid / All tabs plus search by number or client. Each row shows number, client, issue/due dates (due date turns red when overdue), total, open balance, and a status badge — Draft, Sent, Partially paid, Overdue, Accepted (estimates), Paid, Void. Row actions: edit, open/copy the public link, Download PDF, Send (or Resend) by email, Mark sent (no email) for offline workflows, Record payment, Make recurring…, Mute/Resume reminders, Convert to invoice (estimates), Void, and Delete draft (only drafts are deletable — sent documents are records and can only be voided).
  • Invoice editor — pick a client (or quick-add one inline), set issue date, due date, a per-invoice tax percent, and an optional required deposit percent, then build line items: description, quantity (decimals allowed), and unit price, with live-updating subtotal/tax/total. A negative unit price works as a discount line. Notes (shown to the client) and terms print on the invoice. Save & send emails it in one step. Once any payment is recorded — or the invoice is paid or void — line items lock; the payment history section lists every payment with method, reference, date, and a per-payment Refund button for Stripe payments.
  • Recurring — every schedule with client, optional label, amount, frequency (weekly / monthly / quarterly / every 6 months / yearly / every 2 years / every 3 years), next run, auto-send mode, generated count, and an active switch; edit cadence, label, days-until-due, and end date, Generate now, or delete (already-generated invoices are kept). Schedules are created from any invoice's Make recurring… action, which snapshots its line items as the template. Schedules mirrored from WHMCS carry a WHMCS badge.
  • Clients — the billing address book: name, company, email, phone, billing address, internal notes, and a lifecycle status (Prospect → Active → Inactive), with invoice counts per client. When the CRM feature is enabled, an Import from CRM action pulls contacts in with one click (Select all supported) — contacts without an email or already imported are excluded, and imports stay linked to their CRM contact. Manually added clients are mirrored into the CRM (source invoicing) when the sync setting is on. Clients with invoices can't be deleted.
  • Settings (admin only) — invoice + estimate number prefixes, default due days/tax, currency, default notes/terms, the business identity block (falls back to Business settings), payment options (pay online, choose-your-amount, ACH), the Stripe webhook signing secret, reminder cadence, late-fee policy, CRM sync, PDF attachments, the WHMCS migration bridge (URL, API credentials, sync/import/takeover switches, Test connection, Sync now), and email options: sender name/address, Invoice emails go to (client / client and me / only me), and My email for owner copies and payment notifications.

The dashboard also gets two cards: Client Billing (outstanding balance + open/overdue counts) and Invoice Aging (balances bucketed Current/1–30/31–60/61–90/90+ plus average days-to-pay).

Estimates

New estimate creates the same document with its own number sequence (EST-0001, prefix configurable) and no payment surface. The public page shows an Accept estimate button instead of Pay; accepting stamps accepted_at (exactly once — an atomic claim), notifies the owner by email, logs to the CRM timeline and Activity Log, and shows an Accepted badge everywhere. Convert to invoice (from the row menu, the editor, or after acceptance) copies the client, lines, tax, and terms into a fresh draft invoice with a new INV- number and links the two; converting twice reuses the same invoice. The due-date field doubles as Valid until on estimates.

Recurring invoices

Each schedule holds template line items, tax, notes/terms, an optional deposit percent, a cadence, and an optional end date. The hourly automation tick (invoicing:process via LazyCron — no queue worker needed) generates the invoice when a schedule comes due, then advances the next-run date from the scheduled date, not "today", so cadence never drifts; a long outage generates one catch-up invoice and fast-forwards rather than back-billing every missed period. Auto-send emails each generated invoice immediately; off leaves drafts for review. Schedules deactivate themselves once they advance past their end date.

Cadences are weekly, monthly, quarterly, every 6 months, yearly, every 2 years, and every 3 years; every non-weekly cadence re-anchors to the billing day-of-month (Jan 31 → Feb 28 → Mar 31, never drifting to the 28th). Each schedule can carry a label (shown under the client on the Recurring page — mirrored hosting schedules use the product name and domain) and a per-schedule Days until due override: blank uses the global default, 0 issues the invoice with no due date.

Send recipients

Settings → Email → Invoice emails go to decides who receives invoice emails: The client (default), The client and me (the client gets the invoice, the owner gets a [Copy] … email with the items table, an Open in dashboard button, and the public link), or Only me — the owner gets the copy and the invoice stays a draft for review. The setting drives recurring generation and the editor's Save & send; the Send dialog on the invoice list offers the same three choices per send. Owner copies and payment notifications go to My email; when it is blank they fall back to the first admin user's address.

WHMCS migration

The WHMCS migration section of Settings turns Client Billing into a landing zone for an existing WHMCS installation so billing can move over gradually instead of in one cut-over. Enter the WHMCS URL, an API identifier + secret (created under Setup → Staff Management → API Credentials; the secret and the optional access key are write-only and encrypted at rest), and switch on Sync every hour. The hourly invoicing:whmcs-sync tick (LazyCron; Sync now runs it in-process, Test connection proves the credentials) then mirrors:

  • Clients — every Active WHMCS client (add Inactive/Closed with the toggle) with name, company, email, phone, and billing address. An Active WHMCS client imports as Active; Inactive and Closed both import as Inactive, keeping WHMCS's own word for the same idea. A local client with the same email is linked, not duplicated — unless it already mirrors a different WHMCS account, in which case the second one is imported as its own client (WHMCS permits two accounts to share an address, and re-pointing the link would make them fight over one row). Local notes and CRM links are never touched.
  • Invoices — every WHMCS invoice (optionally only those dated on or after a chosen date; anything older is out of scope silently, whoever it belongs to) as a mirrored invoice numbered WHMCS-{invoicenum} (prefix configurable), with WHMCS's line items, totals, tax, and status (Unpaid/Collections/Payment Pending → Sent, Paid → Paid, Refunded → Paid + refunded, Cancelled → Void, Draft → Draft) and WHMCS's transactions as payment rows. Mirrored invoices show a WHMCS badge on the list and a warning callout in the editor; clients pay them on the normal tokenized page or member portal. Unchanged invoices cost one listing row per run — the detail call happens only when the status, totals, balance, or dates moved.
  • Hosting services — every recurring product (Monthly, Quarterly, Semi-Annually, Annually, Biennially, Triennially) becomes a recurring schedule labelled product — domain, with the product's recurring amount plus every Active addon on the same billing cycle as extra lines. The schedule's Days until due is set to the configured lead days so its due dates match WHMCS's own Invoice Generation setting.

The three phases. (1) Mirror — WHMCS keeps billing; the schedules exist but stay paused and simply track WHMCS's next due date. (2) Take over — turn off Invoice Generation and overdue suspension in WHMCS (Setup → Automation Settings), then switch on Generate hosting invoices here: the next sync activates every schedule whose WHMCS product is Active, and this site generates the hosting invoices from then on. (3) Retire — switch the hourly sync off; everything imported stays. Payment push-back stops with it, so clear any still-open mirrored invoices first.

The double-billing guard. Ownership of a schedule's cadence is decided per sync:

  • A schedule this site has never billed keeps mirroring WHMCS's next due date while it is paused.
  • The first sync after the takeover derives the first run date from WHMCS once, skipping a period a mirrored invoice already covers — paid as well as open, since WHMCS only advances its next-due date when its own copy settles, and a client who paid the current period here (with the push not yet landed) would otherwise be billed for it twice. Only an invoice due on or after WHMCS's next due date counts, so a merely historical paid invoice can't skip a live cycle.
  • An active schedule is never re-anchored from WHMCS (its next-due date stops advancing once it stops billing, so rewriting from it would re-bill periods already invoiced here) and its line items and label are left alone — after takeover, this site owns what the schedule bills.
  • Once this site has billed a schedule, its next run date only ever moves forward: a pause and resume can't pull it back onto WHMCS's stale date.
  • A schedule that goes live already past its next run date bills on the very next tick. That is right for a genuinely overdue service and wrong when WHMCS holds an unpaid invoice for that period which the import window left behind, so the sync warns instead of guessing: the run summary ends with “N need a look”, and each one is named in the log. Set “Import invoices dated on or after” far enough back to cover any unpaid invoices before you take over — the coverage check can only see invoices that were actually mirrored, so a narrow window quietly narrows the guard with it.
  • A product that leaves Active, or turning the takeover switch off, pauses the schedule; a product deleted in WHMCS pauses it too (it stops appearing in the listing). A schedule you paused by hand is left paused once this site has billed it at least once; the sync only resumes a schedule it paused itself (a WHMCS product that left Active and came back). Before the first generated invoice there is nothing to distinguish your pause from the pre-takeover default, so switching the takeover on does activate it.

Payment push-back. While Push payments back to WHMCS and Sync every hour are both on, a payment recorded here against a mirrored invoice (Stripe, or a manual entry) is posted to WHMCS with AddInvoicePayment (transaction id wpc-{payment id}, gateway stripe / banktransfer / mailin) so WHMCS stops dunning for it; a Duplicate Transaction ID reply counts as pushed, a transport failure is retried by the next hourly sync, and the sync never re-imports its own pushes as a second payment. Push-back rides on the hourly sync, so switching the sync off stops it: settle any open mirrored invoices before retiring the sync, or leave it on until they are paid.

What the sync overwrites while it is on. Mirrored clients (name, company, email, phone, address, status), mirrored invoices (dates, items, totals, status, payments from WHMCS — edits made here are replaced by the next sync, so change them in WHMCS), and mirrored schedules (cadence and due days always; label, items, and the next run date only while the schedule is paused and this site has not billed it). A mirrored invoice that was paid here is never downgraded to Sent or Void by WHMCS's view of it, and reminders and late fees skip mirrored invoices while the sync is on (WHMCS runs its own).

Limitations. Addon line items need the GetClientsAddons permission on the API role; without it the schedules still mirror, billing the product amount alone, and the run says so. Refunds are not pushed back to WHMCS; one currency per invoice (the WHMCS invoice's currency, else the site default); addons on a different billing cycle than their parent product are skipped with a reason in the sync log (give them their own schedule, or add the line by hand once the schedule is active, at which point the sync stops overwriting its items); WHMCS credits applied to an invoice show up as paid amount rather than as a separate credit line; a WHMCS transaction whose invoice has since been DELETED there cannot be mirrored (there is nothing to attach it to), so an income report summing raw transactions can read slightly higher than the mirror's paid total; services with a non-recurring cycle (One Time, Free Account) are not mirrored; and an invoice deleted in WHMCS is not noticed (its mirror stays as it is — void it here if you delete one there, or reminders will resume for it once the sync is retired).

Reminders

Off by default; enable under Settings → Reminders. When on: a due-soon heads-up N days before the due date (0 skips it), then overdue nudges every N days after, capped at a configurable max per invoice. Every send is claimed in the email log first, so overlapping cron ticks can never double-send. Individual invoices can be muted/resumed from their row menu.

Late fees

Off by default. When enabled, an invoice overdue by the configured days gets the fee once — as a visible "Late fee" line item (percent-of-balance, capped at 20%, or a flat amount), recomputing totals through the same path as every edit. The applied timestamp is claimed atomically, so overlapping runs can't double-fee.

What the client sees

The emailed invoice contains the itemized table, totals, any notes, an optional PDF attachment, and a View & pay invoice button pointing at the tokenized public page (/invoice/{token} — a 40-character random token, no login needed):

  • A printable document: the business identity block, number, issue/due dates, billed-to block, line items, totals, payment history, notes, and terms — plus Download PDF and Print buttons.
  • A status banner when relevant: overdue (with the missed due date), void, accepted, or a green thank-you right after paying/accepting.
  • A Pay button showing the open balance whenever the invoice is payable. When a deposit is required or choose-your-amount is on, an amount chooser offers Pay the full balance, Pay the N% deposit, and (optionally) another amount — validated server-side against the deposit floor, minimum $1, and the open balance. Checkout is embedded Stripe; with ACH enabled, US bank debits appear alongside cards.

The first client open records first_viewed_at — dashboard managers previewing don't count, and drafts are invisible to the public entirely (managers can preview them while logged in, including the PDF).

Member billing portal

When member accounts exist (Memberships, Ecommerce, or Real Estate), a logged-in member visiting /members/invoices sees every non-draft invoice and estimate billed to their email address, with live status badges and links to each document. The member dashboard shows a Your invoices card whenever something has actually been billed to them. Matching is by email only — no linking step required.

Payments & refunds

  • Online (Stripe) — one line item for the chosen amount, stamped with the invoice ID in the session metadata. The webhook (invoice/stripe/webhook, verified against its own signing secret stripe.webhook_secret.invoicing; subscribe it to checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, and charge.refunded) and the return page race to record the completed session; a unique claim on the checkout-session ID guarantees exactly one payment row lands. Credentials reuse the site-wide Stripe keys from Settings → API Keys.
  • ACH settlement — a bank-debit checkout completes days before the money clears, so the invoice enters a processing hold (payment_processing_at): the public page shows a "payment is processing" notice instead of the pay button, and reminders/late fees pause. async_payment_succeeded records the payment and releases the hold; async_payment_failed releases it with an activity-log entry, so dunning resumes and the client can pay again.
  • Manual — record checks, cash, bank transfers, or "other" with an amount (defaults to the open balance), received date, and reference.
  • Partial payments — any payment below the balance keeps the invoice outstanding with a Partially paid badge; when cumulative payments reach the total, it flips to Paid automatically.
  • Refunds — each Stripe payment row in the editor has a Refund button (full remaining amount, through Stripe); refunds recorded monotonically per payment (a stale charge.refunded webhook can't lower them) and rolled up on the invoice. The invoice keeps its Paid status — the books show the refund rather than rewriting history.

Emails

All transactional mail goes through the shared Marketing transport (CampaignMailer), with an optional invoicing-specific from name/address: the invoice/estimate email (with PDF attachment when enabled), reminders, payment receipts (claimed once per payment), owner copies of generated/sent invoices (see Send recipients), an owner notification on online payment, and an owner notification when an estimate is accepted. If the transport isn't configured, sending fails gracefully — the copy-link action, PDF download, and public page work without email entirely.

Integrations

  • CRM — manually created clients mirror into the CRM as contacts (source invoicing, toggleable); imports link to their originating contact; sent/paid/accepted events log to the contact's interaction timeline (type invoice).
  • Activity Log — sent, marked-sent, paid, voided, refunded, estimate-accepted, late-fee, and recurring-generation events all log under the invoicing category.

Statuses

Stored: draft → sent → paid, plus void. Displayed: Overdue, Partially paid, and Accepted are derived live from the stored status — no sweeper job when a due date passes. Voiding is allowed any time before full payment; recorded payments are kept for the books.

Limitations

  • Estimates have accept-only responses — no decline button or client comments (they can simply reply to the email).
  • Refunds are full-remaining per payment — no partial refund amounts from the dashboard (use the Stripe dashboard for those; the webhook records them correctly).
  • The member portal matches by email only — invoices billed to a different address than the member's login won't appear.
  • One currency per invoice, set from settings at creation.