Shopify Order Webhook to QuickBooks: Invoice, Sales Receipt, Credit Memo
Updated April 2026.
Shopify sends order webhooks; QuickBooks expects Invoice or SalesReceipt or CreditMemo objects in their own schema. Mapping the two is mostly grunt work plus a few real decisions (paid vs. unpaid orders, refunds, multi-currency exchange rates). This page is the working reference: four worked examples (orders/create -> QB Invoice, orders/paid -> QB SalesReceipt, orders/refunded -> QB CreditMemo, multi-currency order with exchange rate), error reference, and a structural comparison with A2X, Synder, and Bold.
New here? The full /v1/map endpoint reference (contract, sandbox scope, errors, limits, all use cases) lives at Transformation API overview →. This page is the Shopify-to-QuickBooks deep dive.
1. 30-second quickstart
import httpx, hmac, hashlib, base64, os from fastapi import FastAPI, Request, HTTPException app = FastAPI() SHOPIFY_SECRET = os.environ["SHOPIFY_WEBHOOK_SECRET"] TARGET = ( "QuickBooks-style Invoice with DocNumber, TxnDate, CurrencyRef, " "CustomerRef, Line (array of SalesItemLineDetail per line_item), " "TxnTaxDetail.TotalTax, TotalAmt, BillEmail, BillAddr." ) @app.post("/shopify-webhook") async def shopify_webhook(req: Request): # 1. Verify HMAC (see section 4). body = await req.body() sig = req.headers.get("x-shopify-hmac-sha256", "") expected = base64.b64encode(hmac.new(SHOPIFY_SECRET.encode(), body, hashlib.sha256).digest()).decode() if not hmac.compare_digest(sig, expected): raise HTTPException(401, "Bad HMAC") order = await req.json() # 2. Map Shopify order -> QuickBooks Invoice via /v1/map. async with httpx.AsyncClient() as c: r = await c.post( "https://streamfix.dev/v1/map", headers={"Authorization": "Bearer sk_..."}, json={"payload": order, "target": TARGET}, timeout=30, ) invoice = r.json()["output"] await qbo.invoices.create(invoice) return {"ok": True}
First call with a given order shape and target text compiles the mapper; later calls hit the cache and run in milliseconds. Different order types (paid vs. unpaid vs. refunded) get separate cached mappers - one per (shape, target) pair.
2. Endpoint reference
POST /v1/map. Auth: Authorization: Bearer sk_YOUR_KEY.
payload- the parsed Shopify order JSON.target- description of the QuickBooks-shaped output.
output- the QuickBooks-shaped record.cached-trueif the mapper was reused.fingerprint,elapsed_ms,code_length- debugging metadata.
3. Worked examples
All four outputs below are verified responses from /v1/map. Shopify webhook shapes evolve - check the official webhook reference for current fields. QuickBooks Online API field names are documented in the Intuit Accounting API reference.
The examples below use Shopify's customer ID (e.g. 999) inside CustomerRef.value and Shopify's SKU (e.g. SHIRT-RED-L) inside ItemRef.value. QuickBooks's Online API expects QB's own internal Customer Id and Item Id in those fields, not Shopify's. POSTing the example output as-is will return "Object not found." The mapping step shown here produces a QuickBooks-shaped record - your code is responsible for looking up the matching QB IDs (by email, by SKU, or via a side table) and substituting them before calling QB's API. Some teams do this in a second /v1/map call where the payload includes both the Shopify order and the looked-up IDs; others handle it in plain Python around the call.
3.1 orders/create -> QuickBooks Invoice
A new order with line items, tax, and billing address. Maps to the QB Invoice object with SalesItemLineDetail lines.
{
"id": 5678901234, "name": "#1042", "email": "[email protected]",
"created_at": "2024-04-15T10:30:00Z", "currency": "USD",
"subtotal_price": "159.97", "total_tax": "12.80", "total_price": "172.77",
"customer": {"id": 999, "first_name": "Alice", "last_name": "Buyer"},
"line_items": [
{"sku": "SHIRT-RED-L", "quantity": 2, "price": "29.99", "title": "Red Shirt L"},
{"sku": "MUG-BLUE", "quantity": 1, "price": "99.99", "title": "Blue Mug"}
],
"billing_address": {"address1": "1 Main St", "city": "Boston",
"province_code": "MA", "country_code": "US", "zip": "02101"}
}{
"DocNumber": "#1042",
"TxnDate": "2024-04-15",
"CurrencyRef": {"value": "USD"},
"CustomerRef": {"name": "Alice Buyer", "value": "999"},
"Line": [
{"DetailType": "SalesItemLineDetail", "Amount": 59.98,
"SalesItemLineDetail": {"ItemRef": {"name": "Red Shirt L", "value": "SHIRT-RED-L"},
"Qty": 2, "UnitPrice": 29.99}},
{"DetailType": "SalesItemLineDetail", "Amount": 99.99,
"SalesItemLineDetail": {"ItemRef": {"name": "Blue Mug", "value": "MUG-BLUE"},
"Qty": 1, "UnitPrice": 99.99}}
],
"TxnTaxDetail": {"TotalTax": 12.8},
"TotalAmt": 172.77,
"BillEmail": {"Address": "[email protected]"},
"BillAddr": {"Line1": "1 Main St", "City": "Boston",
"CountrySubDivisionCode": "MA", "Country": "US", "PostalCode": "02101"}
}3.2 orders/paid -> QuickBooks SalesReceipt
When the order is already paid (most direct-to-consumer Shopify flows), use a SalesReceipt instead of an Invoice - this represents money already received. Include PaymentMethodRef from the Shopify gateway name.
"QuickBooks-style SalesReceipt: object with DocNumber, TxnDate (YYYY-MM-DD), CustomerRef ({name: first_name+' '+last_name, value: str(customer.id)}), Line (array per line_item with SalesItemLineDetail nested: ItemRef {name: title, value: sku}, Qty: quantity, UnitPrice: float(price), Amount: float(price)*quantity, DetailType: 'SalesItemLineDetail'), TotalAmt (float total_price), PaymentMethodRef ({name: payment_gateway_names[0]}), CustomerMemo ({value: 'Shopify order '+name})."{
"DocNumber": "#1043",
"TxnDate": "2024-04-16",
"CustomerRef": {"name": "Alice Buyer", "value": "999"},
"Line": [{
"SalesItemLineDetail": {"ItemRef": {"name": "Widget A", "value": "WIDGET-A"},
"Qty": 2, "UnitPrice": 29.99},
"Amount": 59.98, "DetailType": "SalesItemLineDetail"
}],
"TotalAmt": 59.98,
"PaymentMethodRef": {"name": "shopify_payments"},
"CustomerMemo": {"value": "Shopify order #1043"}
}3.3 orders/refunded -> QuickBooks CreditMemo
Refunds map to a CreditMemo. The trick is that Shopify's refunds array nests the original line items inside refund_line_items[].line_item. The target string handles the nested extraction and totals the refund transactions.
"QuickBooks CreditMemo: object with DocNumber ('REFUND-'+name), TxnDate (refunds[0].created_at as YYYY-MM-DD), CustomerRef ({name: first_name+' '+last_name, value: str(customer.id)}), Line (array per refund_line_items: {DetailType: 'SalesItemLineDetail', Amount: float(subtotal), SalesItemLineDetail: {ItemRef: {name: line_item.title, value: line_item.sku}, Qty: quantity, UnitPrice: float(line_item.price)}}), TotalAmt (float sum of refunds[0].transactions amounts), PrivateNote (refunds[0].note)."{
"DocNumber": "REFUND-#1044",
"TxnDate": "2024-04-20",
"CustomerRef": {"name": "Alice Buyer", "value": "999"},
"Line": [{
"DetailType": "SalesItemLineDetail", "Amount": 29.99,
"SalesItemLineDetail": {"ItemRef": {"name": "Red Shirt L", "value": "SHIRT-RED-L"},
"Qty": 1, "UnitPrice": 29.99}
}],
"TotalAmt": 29.99,
"PrivateNote": "Customer changed mind"
}3.4 Multi-currency order with computed exchange rate
When a customer pays in EUR and your QuickBooks home currency is USD, Shopify gives you both via total_price_set.shop_money (USD, your home) and total_price_set.presentment_money (EUR, what they paid in). QB requires ExchangeRate as a single number; the target string computes it from the two amounts.
"QuickBooks Invoice with multi-currency: object with DocNumber, TxnDate, CurrencyRef ({value: presentment_currency}), ExchangeRate (float, total_price_set.shop_money.amount divided by total_price_set.presentment_money.amount, rounded 4dp), CustomerRef, Line (per line_item), TotalAmt (float total_price)."{
"DocNumber": "#1045",
"TxnDate": "2024-04-17",
"CurrencyRef": {"value": "EUR"},
"ExchangeRate": 1.085,
"CustomerRef": {"name": "Klaus Müller", "value": "1000"},
"Line": [{
"DetailType": "SalesItemLineDetail", "Amount": 100.0,
"SalesItemLineDetail": {"ItemRef": {"name": "Item X", "value": "ITEM-X"},
"Qty": 1, "UnitPrice": 100.0}
}],
"TotalAmt": 100.0
}Note: getting the rate from the order itself (rather than fetching from a daily FX API) means the recorded rate matches what Shopify actually settled the transaction at - which is what your accountant wants on the books.
4. HMAC signature verification (do this first)
/v1/map only handles the mapping step. Verify the webhook is from Shopify before you trust it - official docs. Without verification, anyone can POST a fake order and trigger QuickBooks writes.
import hmac, hashlib, base64 body = await request.body() # raw bytes header = request.headers.get("x-shopify-hmac-sha256", "") expected = base64.b64encode( hmac.new(SHOPIFY_WEBHOOK_SECRET.encode(), body, hashlib.sha256).digest() ).decode() if not hmac.compare_digest(header, expected): raise Unauthorized()
After verification, parse the body as JSON and pass it as payload to /v1/map.
5. Errors and status codes
| Status | Meaning | What to do |
|---|---|---|
200 | Success. | Send output to your QuickBooks API client. |
400 | Body missing payload or target. | Check the request. |
401 | Missing or invalid Bearer token. | Include Authorization: Bearer sk_.... |
402 | API key has zero credits. | Top up. |
422 | The mapper raised an exception (e.g. an order with a field shape the target didn't anticipate, like a draft order with no line_items). | Branch on order state in your handler; use a different target string per state. |
502 | Internal generation failure (rare). | Retry. Shopify will redeliver if you respond non-2xx. |
Practical pattern: respond 200 to Shopify quickly, queue the mapping work asynchronously. If /v1/map fails, your queue retries; Shopify is unaware.
6. Limits and behavior
- One mapper per (order shape, target text). orders/create, orders/paid, orders/refunded each have different shapes; they get separate cached mappers.
- Idempotency is your handler's job, not
/v1/map's. Shopify retries; check thex-shopify-webhook-idheader. - Sandbox: AST-validated Python. Decimal arithmetic via the
decimalmodule is available for precise currency math. - Determinism: same Shopify order in always produces the same QB record out, even if Shopify adds new optional fields.
7. Alternatives and how this differs
Shopify -> QuickBooks is a well-served space. Here's how /v1/map differs structurally. For the full sync workflow (auth, scheduling, write-back, accounting summarization), the tools below will fit better; for engineers building a custom integration, the API is simpler.
| Tool | Shape | What it does | Best for |
|---|---|---|---|
| A2X | Hosted SaaS for ecommerce accounting | Daily-summary entries (one journal per Shopify settlement), built specifically for high-volume merchants | Stores doing a lot of sales who want one tidy entry per payout, not one entry per order. |
| Synder | Hosted SaaS | Per-order sync to QB; supports Shopify, Stripe, Square, etc. | Multi-channel merchants who want one sync tool for all their payment sources. |
| Bold / native Shopify QB connectors | Shopify app store apps | Pre-built two-way sync; varies by app | Operators who want a one-click install with no code. |
| Zapier Shopify -> QB | No-code platform with pre-built triggers | Field mapping in their UI | Operators with Zapier already wired up across other tools. |
StreamFix /v1/map | HTTP API; called from your webhook handler | Just the order -> QB-shape mapping step | Engineers building a custom Shopify app or middleware where the mapping is one piece of a larger flow you're already writing. |
Structural difference: A2X / Synder / native connectors are complete sync products - they handle the webhook, the mapping, the QB API auth, the write, deduplication, and (for A2X) the daily-summary aggregation. /v1/map is just the mapping step. If you don't have a Shopify app and don't want to build one, those are easier. If you have a Shopify app and want to write to QB on your own terms, the API is simpler.
8. When NOT to use this
- You don't have an app yet, just a need to sync Shopify to QB. A2X, Synder, or a Shopify-app-store connector will be much faster to set up.
- You want daily-summary entries, not per-order entries. A2X is purpose-built for this; replicating their daily reconciliation logic in target text is doable but A2X just does it.
- You don't have the QuickBooks API auth wired up. The OAuth 2.0 dance with Intuit is non-trivial and isn't what
/v1/mapcovers. - Your accountant insists on a specific GL coding scheme that requires looking up your QB chart of accounts. The mapping step is fine; the lookup is something you do separately.
9. Get an API key
Free trial credits on signup.
Sign up