Built to sit next to Stripe, including on Connect.
Stripe's built-in slot for third-party tax apps doesn't support Connect, and Connect is where most platforms live. So Dilo plugs in alongside Stripe in three ways. Pick the one that matches how you bill today.
Direct API
For platforms on PaymentIntents or Elements, or with their own billing engine. Quote before you create the PaymentIntent. Commit on payment_intent.succeeded, with the PaymentIntent ID as your transaction ID.
Stripe Billing connector
For platforms on Stripe Billing + Connect. We run a service that listens to your Connect webhooks, writes tax onto each draft invoice, and commits it when the invoice finalizes. No change to your billing code.
History import
A read-only backfill of 12–24 months of invoices, charges, refunds and credit notes. You see each merchant's nexus exposure on day one. Imported rows are flagged as not calculated by us.
Two calls around the payment you already make
Requests use Stripe's address shape, integer cents and lowercase currency. Every object can carry a source_refs.stripe block, so you don't need your own ID-mapping table.
- Commit is idempotent on
pi_…orin_…: retry a webhook handler as often as you like - Look up any transaction by your Stripe ID:
/v1/transactions/ext:pi_3Q… - Per-line, per-jurisdiction amounts, rounded per line the way Stripe rounds
- SDK helpers for Go, Node, Python and Java that take a Stripe client (planned)
// 1. Quote before creating the PaymentIntent
const quote = await dilo.quotes.create({
currency: "usd",
tax_date: today,
customer: `ext:${stripeCustomer.id}`,
addresses: { ship_to: stripeCustomer.address },
lines: cart.lines,
}, { merchant: merchantId });
const pi = await stripe.paymentIntents.create({
amount: quote.amount_total,
currency: "usd",
metadata: { dilo_release: quote.content_release_id },
}, { stripeAccount: acct });
// 2. On payment_intent.succeeded: commit
await dilo.transactions.create({
...quoteBody,
external_id: pi.id,
content_release_id: pi.metadata.dilo_release,
collected: { amount_tax: quote.amount_tax },
source_refs: { stripe: { account: acct, payment_intent: pi.id } },
}, { merchant: merchantId });
What the connector does with each Stripe event
You connect through Stripe Connect OAuth or a restricted key. We listen to platform-level Connect webhooks, so event.account tells us which merchant each event belongs to.
| Stripe event | What Dilo does |
|---|---|
invoice.upcoming | Resolves addresses and loads exemptions ahead of time, so the burst of renewals on the 1st is fast. |
invoice.created (draft) | Quotes, then writes per-line tax onto the draft's line items. |
invoice.updated (still draft) | Recalculates, which picks up usage items added late. |
invoice.finalized | Commits, idempotent on the in_… ID. |
invoice.voided | Voids the transaction. If the period is already filed, it alerts you that a credit note is needed instead. |
credit_note.created | Refunds by line, quantity or amount, using the stored original. |
charge.refunded (no invoice) | Refunds against the PaymentIntent's transaction. |
customer.updated | Syncs the address and the tax_exempt hint. A real exemption still needs a certificate on file with us. |
Stripe behaviour we design for
The one-hour draft window
Stripe finalizes a subscription invoice about an hour after it's created. If tax isn't on it by then, the invoice goes out with none. We alert you, and you choose whether we bill the tax on a follow-up invoice or you absorb it. To rule it out, use finalize-for-me mode: you set auto_advance=false, and we finalize once the tax is written.
Prorations and credits
Plan changes create negative lines inside one invoice. The calculation accepts negative amounts, and we write exact per-line amounts rather than percentages. Our ledger and the Stripe invoice match to the cent.
Tax codes from your products
Each line gets its tax code from product.metadata.tax_code, then from your mapping keyed by prod_… or price_…, then from the merchant default. If none of those exist, the call fails with missing_tax_code. We never guess.
Drift between Stripe and the ledger
A reconciliation report compares our ledger with Stripe invoices and credit notes for each merchant and period. Missed webhooks and manual Dashboard edits show up before anyone files.
Later: a native Stripe Tax app for merchants who aren't on Connect (Checkout, Payment Links, Quotes). It depends on a Stripe partnership and isn't part of v1.
On Stripe Connect and unhappy with your tax setup?
Tell us how you bill today. We'll work out with you which path fits, and run Dilo in shadow mode next to your current provider.