At a Glance — Stripe to QuickBooks Online with n8n
- Right trigger: Use
payment_intent.succeeded(one-time) orinvoice.paid(subscription) — neverpayment_intent.created, which fires before payment is confirmed - Signature verification: Required — verify the Stripe-Signature header using HMAC-SHA256 and your
whsec_secret before any processing; skip this and anyone can POST fake payments to your accounting system - Prevent duplicates: Use the Stripe Payment Intent ID as the QuickBooks DocNumber; query QBO before creating to skip records that already exist
- Customer matching: Query QuickBooks by email → create customer if not found → store QBO Customer ID in Stripe metadata to skip the lookup on future payments
- Subscriptions: Use
invoice.paidtrigger → create a Customer Payment against an open Invoice, not a Sales Receipt - Build time: 1–2 weeks for a production-grade implementation including error alerting and refund handling
What this workflow does
The goal is straightforward: when a payment succeeds in Stripe, create the corresponding record in QuickBooks Online automatically. In practice there are four things that need to go right for this to work reliably in production:
- Trigger on the right event —
payment_intent.succeededorinvoice.paid, not earlier events that fire before payment is confirmed - Verify the webhook signature — so only genuine Stripe events can trigger your accounting workflow
- Prevent duplicates — Stripe replays events when your endpoint doesn't acknowledge in time; your QBO records must be idempotent
- Match the Stripe customer to a QBO customer — create the customer if they don't exist, link them if they do
This article covers each of these in order. If you're building a subscription billing sync (recurring invoices), the same pattern applies — swap payment_intent.succeeded for invoice.paid and create a QBO Invoice or Customer Payment instead of a Sales Receipt.
Prerequisites
- n8n instance (self-hosted or n8n Cloud) with a publicly accessible webhook URL
- Stripe account with a webhook endpoint configured (you'll create this in Step 1)
- QuickBooks Online account with OAuth2 credentials configured in n8n (use the built-in QuickBooks Online node credentials)
- Basic familiarity with n8n's workflow editor
Step 1 — Configure the Stripe webhook
In your n8n workflow, add a Webhook trigger node. Set it to POST, copy the webhook URL, and note your webhook secret — you'll need it for signature verification in Step 2.
In Stripe Dashboard → Developers → Webhooks, add a new endpoint with your n8n URL. Select the events to listen to:
payment_intent.succeeded— for one-time paymentsinvoice.paid— for subscription renewalscharge.refunded— if you want to sync refunds as QBO credit memos
Do not subscribe to payment_intent.created or payment_intent.processing. These fire before payment is confirmed and will create premature records in QuickBooks that you'll need to clean up.
After creating the endpoint, Stripe shows you the Signing Secret (starts with whsec_). Copy it — you'll use it in the next step.
Step 2 — Verify the Stripe webhook signature
Without signature verification, anyone who discovers your webhook URL can POST fake payment data to it and create fraudulent records in your accounting system. This is not theoretical — webhook URL discovery happens.
Stripe signs every webhook with an HMAC-SHA256 signature using your endpoint secret. The signature arrives in the Stripe-Signature header in this format:
t=1699123456,v1=abc123...,v0=def456...
To verify it in n8n, add a Code node (JavaScript) immediately after the Webhook trigger. The verification logic:
- Extract the timestamp (
t) and signature (v1) from the header - Construct the signed payload string:
timestamp + "." + rawBody - Compute HMAC-SHA256 of that string using your
whsec_secret - Compare your computed signature against the
v1value from the header - Check that the timestamp is within 300 seconds of now (prevents replay attacks)
If verification fails, return a 400 status and stop the workflow. If it passes, acknowledge with a 200 immediately — Stripe requires acknowledgement within 30 seconds or it considers the delivery failed and will retry.
Important: n8n's Webhook node needs to receive the raw body bytes for signature verification to work. In n8n, set the Binary Property or ensure the raw body is accessible before any node parses the JSON. If you parse the JSON first and then try to verify, the signature check will fail because whitespace and character encoding differences change the hash.
Step 3 — Check for an existing QBO record (idempotency)
Stripe guarantees at least once delivery, not exactly once. If your n8n endpoint is slow to respond or returns a non-2xx status, Stripe will retry the event — sometimes minutes later, sometimes hours. Without idempotency, each retry creates a duplicate Sales Receipt or Invoice in QuickBooks.
The fix: use the Stripe event ID (or the Payment Intent ID) as the DocNumber field on every QBO transaction you create. Before creating a new record, query QBO for an existing one with that DocNumber:
SELECT * FROM SalesReceipt WHERE DocNumber = 'pi_3Pxyz...'
In n8n, use the QuickBooks Online node with the "Get Many" operation and a custom QQL query. If the query returns a record, skip creation and exit the workflow cleanly. If it returns nothing, proceed to Step 4.
Use the Payment Intent ID (pi_xxx) rather than the event ID (evt_xxx) as your DocNumber. Multiple events can reference the same payment intent, but the Payment Intent ID is stable and directly tied to the specific payment transaction you're recording.
Step 4 — Match or create the QuickBooks customer
QuickBooks transactions require a customer reference. Stripe has the customer's email — use that to find or create the matching QBO customer.
Query QBO for the customer by email:
SELECT * FROM Customer WHERE PrimaryEmailAddr = '[email protected]'
Two branches from here:
- Customer found — use their QBO Customer ID in the transaction
- Customer not found — create a new QBO Customer with the Stripe customer's name and email, then use the new customer's ID
To avoid running this lookup on every payment, store the QBO Customer ID in Stripe customer metadata after the first successful sync. On subsequent payments for the same Stripe customer, check metadata first before querying QBO.
Step 5 — Create the QBO Sales Receipt
With the idempotency check passed and the customer ID in hand, create the QuickBooks Sales Receipt using the QuickBooks Online node:
- DocNumber: Stripe Payment Intent ID (idempotency key)
- CustomerRef: QBO Customer ID from Step 4
- TxnDate: convert Stripe's Unix timestamp to YYYY-MM-DD
- Line items: map Stripe line items to QBO line items with the relevant Item reference and amount
- PaymentMethodRef: set to "Stripe" or the relevant payment method if you've created one in QBO
For subscription payments triggered by invoice.paid, create a Customer Payment against an open Invoice instead of a Sales Receipt — this keeps your accounts receivable accurate rather than creating revenue without a matching invoice.
Step 6 — Error handling and alerting
Payment workflows are business-critical. A silent failure means a payment occurred in Stripe with no corresponding record in QuickBooks — discovered days or weeks later during reconciliation. Build alerting in from the start:
- Add an Error Trigger workflow that catches failed executions and sends a Slack message or email with the Stripe event ID, event type, and error details
- Include the Payment Intent ID in every alert so you can manually create the QBO record if needed
- Set up a retry policy on the QBO write node for transient API errors (rate limits, 5xx responses from Intuit)
For a more robust implementation, consider a dead-letter pattern: failed records are written to a Google Sheet or database with their Stripe event ID and error reason, and a separate n8n workflow runs on a schedule to retry them.
Variations and edge cases
Multi-currency payments
If your Stripe account processes multiple currencies, check whether your QBO account is set up for multicurrency. If not, you'll need to convert Stripe amounts to your base currency using the exchange rate from the Stripe payment — available in the balance_transaction object, not the payment intent itself. Call the Stripe API to retrieve the balance transaction and use its exchange_rate field.
Partial refunds
A charge.refunded event may be a partial refund. Check amount_refunded against amount on the charge object to determine whether to create a full or partial QBO Refund Receipt. Use the original Payment Intent ID with a -refund suffix as the DocNumber to maintain the idempotency pattern.
Stripe Checkout and Payment Links
Payments through Stripe Checkout or Payment Links still fire payment_intent.succeeded, so the same workflow handles them. The customer email is available in the customer_details object on the checkout session — retrieve the session object from the Stripe API if it's not included directly in the webhook payload.
If you need this built and maintained rather than implemented yourself, Entech Solutions builds production-grade n8n Stripe integrations and QuickBooks Online automations — scoped, delivered, and documented. Most payment sync projects go live in 1–2 weeks.
Frequently asked questions
Can n8n sync Stripe payments to QuickBooks Online? Yes. n8n has official nodes for both Stripe and QuickBooks Online. You configure a Stripe webhook to trigger n8n on payment events, then write the payment data to QuickBooks as a sales receipt or customer payment. The key implementation detail is idempotency: using the Stripe event ID as the QuickBooks DocNumber to prevent duplicates when Stripe replays events.
How do you prevent duplicate QuickBooks records from Stripe webhook replays? Stripe replays webhook events if your endpoint doesn't acknowledge receipt quickly enough. To prevent duplicates in QuickBooks, use the Stripe event ID (or the Stripe Payment Intent ID) as the DocNumber field on every QuickBooks transaction. Before creating a record, query QuickBooks for an existing transaction with that DocNumber. If one exists, skip or update it — never create a second record.
Do I need to verify Stripe webhook signatures in n8n? Yes. Without signature verification, anyone can POST data to your n8n webhook URL and trigger your accounting workflow. Stripe sends a Stripe-Signature header with every webhook. You verify it using your Stripe webhook endpoint secret and the raw request body. In n8n, use a Function node immediately after the Webhook trigger to verify the signature before any processing begins.
Which Stripe events should trigger the QuickBooks sync? For payment sync: payment_intent.succeeded and invoice.paid are the primary triggers. For subscription accounting: invoice.paid covers recurring charges. For refunds: charge.refunded should create a QuickBooks credit memo or refund receipt. Avoid triggering on payment_intent.created — that fires before payment is confirmed and can create premature records.
How do I match Stripe customers to QuickBooks customers in n8n? The most reliable approach is to query QuickBooks for a customer by email (using a QQL query: SELECT * FROM Customer WHERE PrimaryEmailAddr = '[email protected]'). If found, use that customer's QuickBooks ID for the transaction. If not found, create the customer first, then create the transaction. Store the QuickBooks Customer ID in Stripe customer metadata to skip the lookup on subsequent transactions.