At a Glance — Stripe to QuickBooks Online with n8n

  • Right trigger: Use payment_intent.succeeded (one-time) or invoice.paid (subscription) — never payment_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.paid trigger → 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:

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

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:

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:

  1. Extract the timestamp (t) and signature (v1) from the header
  2. Construct the signed payload string: timestamp + "." + rawBody
  3. Compute HMAC-SHA256 of that string using your whsec_ secret
  4. Compare your computed signature against the v1 value from the header
  5. 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 = 'customer@example.com'

Two branches from here:

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:

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:

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.