DEV Community

Bertrand Azzopardi
Bertrand Azzopardi

Posted on

From invoice to paid to QuickBooks Online: modeling one billing flow across cards, ACH and stablecoins

If you've built billing for a small business, or you're the technical founder running one, you know the pattern. Invoices live in one tool. Card payments arrive through a processor. Bank payments arrive somewhere else. Someone then matches all of it against the books by hand.

None of these pieces is hard alone. The work is in the seams: which record is the source of truth, what "paid" means for each payment method, and what reaches your accounting system.

Disclosure: I'm the founder of SaturnShift, and this post uses it as the concrete example. SaturnShift is a payments and invoicing platform for US small businesses. Merchants send invoices and payment links that customers pay by card or ACH (processed by Stripe) or with USDC/USDT stablecoins (subject to plan), and the results sync to QuickBooks Online on the plans that include it. The goal is to make the data flow and its edges explicit, so you can model it correctly on your side. It isn't a feature tour.

Scope note: this article covers product behavior, not an API reference. It doesn't document endpoints, webhook names or sync timing.

The problem: one receivable, many rails

A single receivable (say, a $1,200 invoice for consulting work) can be settled in several ways:

  • Card, processed by Stripe
  • ACH bank payment, processed by Stripe
  • Stablecoin: USDC on Ethereum, Base, Arbitrum and Polygon, and USDT on Ethereum and Base

Card and ACH are available on Starter and Full Suite. Crypto Suite is stablecoin only.

If each rail has its own tool, you end up with three payment records, three fee structures and a manual match against one invoice. The approach here is to keep one request object (the invoice or payment link) and let the customer pick the rail at checkout. Everything downstream then hangs off that one object.

The objects

1. Invoice

An invoice is the primary receivable. What you can rely on:

  • Create, issue, void and send. These are the core lifecycle operations.
  • Templates, so you can reuse layouts and branding.
  • Tax is a rate you enter, per line or at invoice level. There's no automatic tax calculation on invoices; automatic sales-tax calculation exists only at online-store checkout.
  • Discount is a single invoice-level percentage on the subtotal. Per-line discounts aren't available.
  • CSV import creates invoices in bulk.
  • Bulk send: select multiple invoices and send them in one action.

Implementation note: if you generate invoices from another system, such as a job tracker, a spreadsheet or a scheduling tool, the CSV import plus bulk send pair is the supported batch path. Model tax as a stored rate rather than a computed jurisdiction lookup, because that matches how the invoice actually behaves.

2. Payment link

A payment link is a lighter request with no invoice document:

  • a fixed amount, or a customer-entered amount (useful for deposits, tips or "pay what you owe");
  • shared by email, by SMS (Text to Pay) or as a QR code.

Text to Pay sends invoices and payment links by text message. Don't build logic that treats a successful send as proof the customer received the message, and keep email as a fallback channel.

3. Checkout

Both objects resolve to a checkout:

  • a hosted checkout page, or
  • a checkout button for your site that opens SaturnShift checkout as a popup or redirect.

A fully embedded in-page (iframe) checkout isn't available. If your site design assumes the card form renders inside your own page, plan for a popup or redirect instead.

The flow, step by step

 invoice / payment link
        │   (issued and sent: email, SMS, QR)
        ▼
 customer opens checkout -> chooses rail --┐
        │                                  │
        │   card / ACH  (processed by Stripe)
        │   USDC (Ethereum, Base, Arbitrum, Polygon)
        │   USDT (Ethereum, Base)
        ▼
 payment in progress
        ▼
 invoice paid --> reminders stop for this invoice
        │
        ▼
 QuickBooks Online (Crypto Suite and Full Suite):
 invoices, payments, refunds and fees reflected in the books
Enter fullscreen mode Exit fullscreen mode

A few states matter in practice.

Issued and sent. Once an invoice is out, automatic email reminders can run. Reminders go before the due date and after the invoice is overdue, and they are sent by email.

Payment in progress. The reminder logic explicitly skips invoices that are already paid or have a payment in progress. That tells you something useful: "a payment has started" and "the invoice is paid" are separate conditions. If you mirror invoice status in your own system, keep them separate too, and don't collapse "customer clicked pay" into "paid".

Paid. Reminders stop, and the payment appears in reporting with the rest of your activity.

Void. Invoices can be voided. Treat void as terminal in your own model rather than deleting the record.

Refunds are supported across card, ACH and stablecoin payments, within the refund window. Model a refund as its own event against the original payment, not as an edit to it.

Where the money lands (by rail)

This is the part most homegrown reconciliation gets wrong, because the rails really do behave differently.

  • Card and ACH (Starter and Full Suite) are processed by Stripe. Stripe's own processing fees apply separately from the SaturnShift platform fee, so the platform fee is not the all-in cost of a card payment. Record both fee lines if you're computing net revenue.
  • Stablecoins settle on-chain, in kind, to your own payout wallet on the network the customer paid on. USDC paid on Polygon arrives as USDC on Polygon. It isn't converted to another asset or moved to a different network. Your stablecoin settles directly to your own wallet; SaturnShift does not hold it.

What that means for your data model:

  • Store asset and network on every stablecoin payment. "USDC" alone isn't enough, because payout wallets are registered by network (the same address on every network, or a different one for each).
  • SaturnShift does not convert stablecoins to fiat and offers no cash-out. If the business needs dollars in a bank account, that conversion happens outside the platform, through the merchant's own exchange, and your books should treat it as a separate transaction.
  • The supported stablecoins are USDC and USDT only; model the asset as one of those two values.

Reconciliation logic

There are two layers.

Inside the platform: reporting, reconciliation, settlement views and monthly statements cover payments across rails. Because each payment is made against a specific invoice or payment link, you aren't left matching amounts and dates to requests by hand.

In your accounting system: with QuickBooks Online connected (Crypto Suite and Full Suite), invoices, payments, refunds and fees are reflected in your books, including payments made in stablecoins. That's the useful part for a mixed-rail business: a stablecoin-paid invoice shows up in QuickBooks Online like any other payment, instead of as an unexplained transfer.

Conditions to design around:

  • QuickBooks Online is available on Crypto Suite ($49/month) and Full Suite ($69/month). It is not on Starter.
  • It's QuickBooks Online. Don't assume QuickBooks Desktop or other accounting packages.
  • Subscription payments don't sync to QuickBooks Online. Recurring billing charges a card (Full Suite). If you also run recurring billing, record those payments in your books another way. One-time invoices still sync.
  • This post deliberately doesn't describe the connection method, account mapping or sync timing. Build your month-end process so it doesn't depend on a timing assumption.

Plans and rails: the constraint matrix

Which rails a merchant can use depends on the plan, so your onboarding logic should check the plan before showing payment options:

Plan Price Rails QuickBooks Online Subscriptions
Starter $0/month Card, ACH, stablecoin No No
Crypto Suite $49/month Stablecoin only Yes No
Full Suite $69/month Card, ACH, stablecoin Yes Yes (charges a card)
  • Starter is capped at 10 invoices per calendar month. If you're building an import job, it needs to handle hitting that cap.
  • Paid plans have a 7-day trial requiring a card on file. Starter has no trial.
  • Each plan's SaturnShift platform fees are listed on the pricing page.

Practical implementation notes

  1. Plan for approval before go-live. A merchant can sign up and set up their invoices and store right away, but every account, on every plan, completes a short Stripe onboarding and can't take a live payment until SaturnShift approves the account. Approval is a separate manual review, so build launch plans around it rather than against a fixed clock.
  2. Keep one request ID across systems. Use the invoice (or link) as the join key between your CRM, the payment and the QuickBooks Online record. Don't key on amount and date.
  3. Track rail, asset and network per payment. Fees, refunds and settlement destinations differ by rail.
  4. Separate "in progress" from "paid" in any status you mirror.
  5. Treat Stripe fees as their own line for card and ACH.
  6. Don't promise customers settlement or payout times that the platform doesn't promise you.
  7. Check the customer self-service portal before building your own customer-facing billing page. It may already cover what you need.

Limitations, in one place

  • No automatic tax on invoices (it's a manual rate); no per-line discounts.
  • No iframe-embedded checkout; use the hosted page or the checkout button (popup or redirect).
  • No payment plans, installments or BNPL.
  • No cash-out; stablecoins settle in kind and SaturnShift does not convert them to fiat.
  • Stablecoins are USDC and USDT only (no other crypto assets).
  • QuickBooks Online only, and not on Starter. Subscription payments don't sync.
  • Automatic reminders are sent by email.
  • Live payments need Stripe onboarding and manual account approval.

Wrapping up

The design choice that makes this manageable is one request object per receivable, with the rail chosen at checkout and every downstream record (status, refunds, accounting) hanging off it. Whatever stack you use, modeling the states above explicitly will save you a month-end of manual matching.

If you want to see how this looks in SaturnShift, the QuickBooks Online integration page covers the product side.

Top comments (1)

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

the definition of "paid" differing by rail is the part most reconciliation code gets wrong. a card authorization that reaches the Stripe balance and a stablecoin transfer landing in a payout wallet are not the same event, and ledgers that treat them identically drift apart within one month-end close. storing asset and network per payment rather than a bare USDC flag is the detail teams usually learn after the first mismatched payout.