Home/Shopify Order to QuickBooks

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

Python (FastAPI webhook handler)
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.

Request body
  • payload  -  the parsed Shopify order JSON.
  • target  -  description of the QuickBooks-shaped output.
Response body
  • output  -  the QuickBooks-shaped record.
  • cached  -  true if 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.

Important: customer and item ID lookup

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.

payload (Shopify order)
{
  "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"}
}
output (QuickBooks Invoice)
{
  "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.

target
"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})."
output
{
  "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.

target
"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)."
output
{
  "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.

target
"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)."
output
{
  "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.

Python (raw HMAC, no SDK needed)
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

StatusMeaningWhat to do
200Success.Send output to your QuickBooks API client.
400Body missing payload or target.Check the request.
401Missing or invalid Bearer token.Include Authorization: Bearer sk_....
402API key has zero credits.Top up.
422The 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.
502Internal 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

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.

ToolShapeWhat it doesBest for
A2XHosted SaaS for ecommerce accountingDaily-summary entries (one journal per Shopify settlement), built specifically for high-volume merchantsStores doing a lot of sales who want one tidy entry per payout, not one entry per order.
SynderHosted SaaSPer-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 connectorsShopify app store appsPre-built two-way sync; varies by appOperators who want a one-click install with no code.
Zapier Shopify -> QBNo-code platform with pre-built triggersField mapping in their UIOperators with Zapier already wired up across other tools.
StreamFix /v1/mapHTTP API; called from your webhook handlerJust the order -> QB-shape mapping stepEngineers 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

9. Get an API key

Free trial credits on signup.

Sign up

Related transformations