Stripe invoices and pay links
Every invoice in The Contractor Codex is a real Stripe invoice. The portal doesn't run its own payment system — it builds invoices in Stripe via the Connect API and shows the client a Pay-Now button that opens Stripe's hosted checkout. Money flows directly from your client's bank to yours.
Where invoices come from
Three sources:
1. Quote acceptance (automatic)
When a client accepts a quote, the portal automatically creates a Stripe invoice based on the quote's billing mode. The line items, totals, and customer all come from the quote.
2. Manual invoice (from a project)
From a client's workspace, click New Invoice on any project to bill against tracked time. The portal calculates billable hours, applies the project's rate, and creates a Stripe invoice.
3. Recurring invoicing (cron)
For monthly retainers or recurring product fees, you can configure a recurring invoice via a client's workspace → Recurring tab. A cron job runs hourly and fires new invoices on the schedule you set.
What the client sees
Every invoice produces:
- A Stripe-hosted invoice page at a URL like
https://invoice.stripe.com/.... This is what a Pay-Now link opens. - A PDF of the invoice (downloadable from the hosted page).
- An email sent to the client's address (if you didn't disable the email).
The hosted page is fully branded with your business name, logo, and accent color (set in Settings).
The client can pay by:
- Credit/debit card (Visa, Mastercard, Amex, Discover)
- ACH bank transfer (US only)
- Stripe Link (one-click for repeat payers)
Enable/disable each method in Settings → Payments.
Pay-Now links — where they appear
The portal sprinkles Pay-Now links everywhere a client might pay:
- The accepted-quote page after they sign
- The portal dashboard's Outstanding card
- The portal's Pay page (lists every open invoice)
- Every invoice notification email
- The receipt confirmation email
Every Pay-Now link goes to the same Stripe-hosted invoice page. Multiple links to the same invoice are fine — Stripe handles the dedup.
Invoice statuses
Stripe statuses, mirrored in the portal:
draft— created but not sent to the client. Editable in Stripe.open— sent. Client can pay; you can void.paid— payment received. Funds en route to your bank.void— cancelled. Can't be paid; doesn't count against your records.uncollectible— marked off as bad debt by you.
The portal shows the status as a badge on every invoice card.
Status drift / sync
The portal listens for Stripe webhooks at /api/webhooks/stripe and updates invoice statuses in real time. If a webhook drops (rare), the portal has a self-heal layer on the customer Pay page that pulls live status from Stripe on every visit — so the customer always sees the truth even if a webhook was missed.
Refunds
To refund a paid invoice, open the invoice in the portal and click Refund. You can refund the full amount or a partial amount. The refund goes through Stripe and the funds are pulled from your bank back to the client's card.
See Refunds and disputes for the full flow.
Marking an invoice uncollectible
If a client never pays and you've decided to write it off, click Mark uncollectible on the invoice. The status moves to uncollectible and it stops appearing in your Outstanding totals. Stripe records this for your accounting and tax purposes.
This is a manual action you take per invoice. There is no automatic "mark uncollectible after N days" setting.
Recurring invoices
Two flavors:
Per-client recurring (via Recurring tab)
From a client's workspace, set up a recurring invoice with:
- A billing cycle (monthly or quarterly)
- A day of the month
- A mode (
auto_send— invoice fires + emails the client; ordraft_only— invoice fires as a draft for you to review before sending)
The cron at /api/cron/recurring-invoices runs hourly and fires invoices when their nextScheduledAt is in the past.
Per-line-item recurring (from quotes)
Quote line items with kind = recurring_monthly or recurring_annual are shown on the quote PDF as ongoing commitments. After acceptance, you set up the recurring invoice via the Recurring tab using those line items as the template.
Stripe Connect application fee
Every invoice carries a service fee (1% on Standard, 0.2% on Pro) that Stripe collects automatically as an application fee and routes to the platform. You see the fee on Stripe's side; the client never sees it (the invoice total is unchanged).
Custom invoice memo + footer
Set a per-org invoice memo and footer in Settings → Payments. These appear on every Stripe-hosted invoice page and PDF — useful for things like "Thanks for your business" or "Payment due Net 30."
Setting up recurring invoices in detail
For ongoing engagements (retainers, monthly hosting fees, quarterly maintenance contracts), set up a recurring invoice config from a client's workspace:
- Open the client at /admin/clients.
- Click the Recurring tab in their workspace.
- Click New Recurring Invoice.
The form asks for:
- Billing cycle —
monthlyorquarterly. (For annual, usequarterlyset to fire 4x/year, or contact support — annual is on the roadmap.) - Day of month — 1–28. The cron fires on this day of each billing cycle. Days 29–31 aren't supported because months don't all have them.
- Mode —
auto_send(invoice fires + emails the client automatically) ordraft_only(invoice is created as a Stripe draft for you to review + finalize before sending). - Line items — same composer as a one-off invoice: name, quantity, unit price, optional description.
- Currency — default USD.
- Start date — when to fire the first invoice.
- End date (optional) — when to stop. Empty = recurring indefinitely.
When the cron fires
The recurring-invoice cron runs hourly. Every hour, it checks all enabled recurring configs and fires invoices where:
nextScheduledAt <= now()(the scheduled date has passed)isEnabled = trueendDate is null OR endDate > now()(the config hasn't ended)
After firing, nextScheduledAt is advanced to the next billing cycle (e.g., +1 month for monthly, +3 months for quarterly).
Pausing or stopping a recurring config
In the client workspace's Recurring tab:
- Pause — toggles
isEnabledtofalse. Future invoices stop firing; existing invoices stay open. Re-enable to resume on the next scheduled date. - Delete — removes the config entirely. Existing invoices stay (they're separate Stripe records); no future invoices will fire.
To change the amount on a recurring config (e.g., the client's retainer goes up), edit the line items in the config. The next invoice fires at the new amount.
Client-initiated cancellation
Clients with an active retainer can also request cancellation themselves from their portal at /dashboard/billing. The request lands on the customer record with a status flag and an effective date (end of the current cycle, with a 5-day minimum notice — if there isn't enough notice the date bumps to the end of the next cycle). The retainer keeps running until you respond or the effective date arrives. Full admin approve / decline / counter controls and the auto-apply cron are in a follow-up release; for now, you'll see the request in the activity log and can handle it manually from the Recurring tab. See Retainer cancellation in the glossary.
Auto-send vs draft-only
- auto_send — set it and forget it. The invoice fires, emails the client, you don't have to do anything. Use this for clients on autopilot.
- draft_only — useful when you want to review the invoice before it goes out (add notes, adjust line items, attach a custom memo). The cron creates the invoice in Stripe with status
draft, sends you a notification, and you finalize from the invoice card.
For most retainers, auto_send is the right default. For client-specific adjustments each cycle (e.g., a client whose hours vary monthly and you need to add them as line items), use draft_only.
Manual invoice creation
For one-off billing (e.g., hourly work outside a quote), create an invoice manually:
- Open a client at /admin/clients.
- Pick a project from their workspace.
- Click New Invoice on the project.
- The portal calculates billable hours since the last invoice (or the project start) and pre-fills the line items.
You can adjust:
- Line items — edit names, descriptions, amounts. The pre-fill is a suggestion.
- Billable minutes range — restrict to a specific date range if you only want to invoice part of the work.
- Snapshot type — mark as a deposit, balance, or regular invoice.
- Currency — default USD; override per-invoice.
Hit Create + Send and the invoice goes out. Or Save as Draft to review before sending.
What's a BillingSnapshot?
Every manual invoice creates a BillingSnapshot record — a frozen capture of the billable work + amounts at the moment of creation. The snapshot is what the Stripe invoice references; the invoice itself doesn't store the work details (Stripe just sees a line item like "Consulting hours" with a total).
You can re-open a snapshot in the admin UI to see what hours/dates/rates went into it. Useful for audit purposes — even if the project changes later, the snapshot is unchanged.
Invoice status badges across the portal
Every place an invoice shows up in the portal uses the same status badge component (<InvoiceStatusBadge>) so the same Stripe status always renders the same label and color:
paid→ green "Paid"open→ amber "Invoice Sent"draft→ grey "Draft"void/voided→ red "Voided"uncollectible→ red "Uncollectible"payment_failed→ red "Payment Failed"
If you see different labels in different places, that's a bug — report it.
Force-refresh from Stripe
If you suspect a webhook dropped (the portal's status doesn't match Stripe's), the customer-facing Pay page has a built-in self-heal layer: on every visit it pulls live status from Stripe and reconciles. So if a client just paid and you're not seeing it reflected immediately, have them refresh /dashboard/billing and the status will sync.
For admins, there's no manual "sync now" button — the next webhook event (or a customer page load) will sync. If you're stuck more than 10 minutes after a payment with no status change, contact support.
Invoice numbering
Stripe assigns invoice numbers automatically (e.g., INV-12345). You can customize the format in your Stripe dashboard → Invoices → Settings. The portal uses whatever Stripe generates.
For audit/accounting purposes, invoice numbers are unique within your Stripe account, never reused, and stay constant even if the invoice is voided or refunded.
Custom payment terms
Per-invoice payment terms (Net 30, Net 7, etc.) come from Stripe — set the default payment terms in Settings → Payments via invoice_due_days (default 30). The portal applies this to every invoice created.
For per-invoice overrides, edit the invoice in Stripe directly (the View in Stripe link opens it) and adjust the due date there.
Email notifications
Every invoice triggers a few notification points:
- Invoice sent — client gets an email with the Pay-Now link.
- Invoice viewed — client opens the link; portal notifies you in-app.
- Invoice paid — client pays; portal notifies you + sends client a receipt.
- Invoice failed — payment attempt fails; portal notifies you + the client.
- Invoice voided — admin voids; client is notified.
Each notification has its own admin-side toggle in Settings → Notifications — turn off whichever you don't want fired.
Multi-currency invoicing
For international clients, set the invoice's currency per line item (must match across all items on one invoice). Stripe handles the FX:
- The client is charged in the invoice currency.
- Stripe converts to your account's default currency at their rate (~1% spread) when settling.
- The application fee (1% Standard / 0.2% Pro) is taken in the invoice currency before conversion.
For clients you bill regularly in non-default currencies, set up a recurring config with the right currency baked in.
