Skip to content

Shopify Integration

A comprehensive accounting-first integration for Shopify stores, providing revenue recognition, payout reconciliation, fee tracking, and financial reporting. The /api/v1/shopify/* endpoints require the Commerce add-on on the business (the exceptions are finalize-pending-install and connection/diagnose, so a new account can finish an App Store install and see why a connection fails). Connect, disconnect, resync and move calls need admin permission.

The /api/v1/shopify/* endpoints below work with an API token but are not part of the public API Reference — they back DayZero's own Integrations page and may change. Booked activity lands in the public Journal Entries, Inventory and Reports endpoints, which is where integrations should read from.

What Gets Synced

Data Description
Orders Pulled into the Shopify subledger; summarised into sales-recognition journal entries
Payouts Settlement journal entries that match the bank deposit 1:1
Products & inventory Synced into the product catalog for inventory and COGS tracking
Customers Customer records and balances
Refunds Netted into the daily / payout summaries
Gift cards Liability tracking
Disputes Reserve management

Connect Shopify

From the Integrations page in DayZero, enter your Shopify shop domain and click Connect Shopify. You'll be redirected to Shopify to authorize access.

Or via API:

bash
curl "https://api.ondayzero.com/api/v1/shopify/install-url?shop=your-store" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

shop accepts your-store or your-store.myshopify.com. Say where the store's history starts with posting_start_date=YYYY-MM-DD (orders and payouts from that day are imported and posted) or import_all_history=true (no cutoff), not both; a date in the future or before 2006 is a 400. Without either, a new store starts today and a reconnected one keeps its date. GET /api/v1/shopify/history-start-options returns the choices the Connect form offers (start of this month, start of the fiscal year, books_start = the first journal entry, first_open_day = the day after the last closed period, all history) with a default_key. The response includes install_url, nonce and shop. Redirect the merchant to install_url from the same browser (the call sets a dz_oauth_shopify cookie that the callback requires) to begin the OAuth flow. After authorization, DayZero registers webhooks and queues the initial sync. A shop domain can be live on only one business at a time. (Merchants who installed from the Shopify App Store before creating a DayZero account call POST /api/v1/shopify/finalize-pending-install?shop=your-store, with the same optional posting_start_date / import_all_history, which returns the same {install_url, nonce, shop} bound to the current business; it needs the install cookie Shopify's redirect set in that browser. The parked install is held for one hour: after that the call returns 400 INTEGRATION_059 and the merchant opens DayZero from Shopify admin again.) A business can connect more than one Shopify store (multi-brand, multi-region): connecting a second shop adds it beside the first (see More than one store).

Option 2: Manual Access Token

If you have a Shopify custom app access token:

bash
curl -X POST "https://api.ondayzero.com/api/v1/shopify/setup" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"shop": "your-store.myshopify.com", "access_token": "shpat_..."}'

The token is tested against Shopify before anything is saved; a failing token returns 400 and leaves the business unchanged. /setup then registers webhooks and imports products (with cost per item) and inventory.

For a custom-app token, POST /api/v1/shopify/stores/connect-with-token is the stricter option. Send {"shop_domain": "your-store", "access_token": "shpat_...", "dry_run": true} first (add posting_start_date or import_all_history as on /install-url): the token must open that shop and carry read_orders, read_products and read_shopify_payments_payouts. The response lists granted_scopes and missing_scopes (read scopes only: a custom app needs none of the write scopes the OAuth app asks for), and the resulting posting_start_date / import_all_history; without dry_run it saves the token, creates the store and queues the first sync (HTTP 201, with store_id and sync_queued). It returns 409 if the shop is live on another business. For a business already connected to a different shop it adds the store beside that one (adds_store: true). If a connection will not complete, GET /api/v1/shopify/connection/diagnose?live=true reports the stored shop, scopes against the app's required scopes, webhooks, recent install attempts and a list of problems, each with a fix. Agents have the matching MCP tools check_shopify_connection and connect_shopify_store_with_token (check_only: true is the dry run).

New permissions. The app also asks for read_locations (stock per Shopify location) and read_gift_cards (lets DayZero read gift-card balances). Stores connected before these were added approve them once, when they next open DayZero from Shopify admin. Until then the store keeps syncing as before: stock is summed on the default location, gift cards are skipped, and connection/diagnose?live=true lists the permissions not yet granted (scopes_not_granted). Approving goes through OAuth, but a store that has synced before only gets its new token, its webhooks re-registered and the daily sync from where it left off; its history is not imported again. If Shopify still leaves a permission out, opening the app goes straight to the integration instead of asking again.

Automatic Accounting

DayZero never posts one journal entry per order — the general ledger only sees summaries. Orders, refunds, products and customers are pulled on a schedule into a subledger; webhooks carry payouts, disputes, product and cost changes, stock counts and app lifecycle. Two posting modes are available (GET/PATCH /api/v1/shopify/subledger/posting-config):

posting_mode Journal entries created
per_payout (default) One entry per Shopify payout. Debits: the net deposit (to the mapped bank ledger, or to the Shopify Merchant Account clearing ledger when bank matching is on), processing fees, refunds, discounts and gift-card redemptions. Credits: product sales, shipping, tips, duties and sales tax payable (one ledger per jurisdiction). COGS / inventory lines are added when inventory is tracked. The entry total equals the bank deposit, so bank matching is exact.
daily_summary One sales-recognition entry per shop-local day (the shop's own timezone, as in Shopify's reports) (Debit Shopify Merchant Account clearing / Credit revenue, tax payable, etc., plus COGS), and a separate settlement entry when each payout lands that moves cash from Clearing to Bank net of fees. Accrual-friendly.

Posting is idempotent per day / per payout, so re-syncing never double-books. Other posting-config fields: posting_start_date (ignore history before this date; clear_posting_start_date: true removes it), bank_match_enabled (auto-match payouts to bank transactions) and catalog_sync_hour (0-23, default 6) / catalog_sync_timezone (default UTC) for the daily catalog sync of products, inventory and orders. bank_match_enabled defaults to false. The sync poller runs hourly and each store syncs once a day at its configured local hour; a daily payout fee true-up runs at 05:00 UTC and a clearing-balance drift check at 05:30 UTC. Per store DayZero subscribes the payout, dispute, app-uninstall and catalog webhooks (products/create, products/update, products/delete, inventory_items/update, inventory_levels/update); the mandatory privacy topics (customers/data_request, customers/redact, shop/redact) are declared app-wide.

Products, cost per item and stock

Each daily sync (and the connect-time import) reads the whole catalog from Shopify (one GraphQL bulk query; REST paging when bulk is unavailable): products, variants, each variant's cost per item and its stock per location. Products and variants are matched on their Shopify ids, so products created after connect arrive too, and products Shopify no longer has are archived.

  • Cost. Shopify's cost per item becomes the variant's unit cost whenever Shopify has one. A cost set in DayZero (Inventory, Products, Import unit costs) is kept for variants with no cost in Shopify.
  • Stock vs. non-inventory. A product is stock (category Finished Goods) unless Shopify says it holds none: gift cards, items Shopify neither tracks nor ships, and untracked add-ons such as shipping or returns protection, warranties, donations and tips (category Non-inventory). A category you set yourself is kept. Non-inventory lines never count as missing a cost.
  • Stock levels. The first count of a variant is its opening stock; each later sync reconciles DayZero to Shopify's available count, and every difference is an inventory adjustment with its reason (Shopify inventory import / Shopify daily inventory reconcile). Stock is read from Shopify only; DayZero never writes stock counts back. Oversold (negative) counts are left alone.
  • Locations. Stock lands on the DayZero location linked to the Shopify location. That needs the read_locations permission; until a store has approved it, its stock is summed on the default location.
  • Opening stock. Once a counted variant has a cost, DayZero records one opening cost lot for it and books its value once: Debit Inventory (or the mapped inventory ledger), Credit Opening Balance Equity, dated the store's posting_start_date (the count date when all history is imported; today when that date is in a closed period). A cost added later books only the difference; re-running books nothing.
  • Order lines. After the catalog read, order lines that were not linked to a variant (the product arrived later) are linked by Shopify variant id, or by SKU when exactly one variant has it, and lines without a cost capture the variant's cost (or its newest cost lot's). A recorded cost is never overwritten.

Rebuild COGS. When orders are missing cost, the Shopify card and the subledger show N orders missing cost with Set costs (the unit-cost import) and Rebuild COGS. GET /api/v1/shopify/cogs/status returns orders_missing_cost, unlinked_lines, ariants_without_cost and how many posted payouts / days a rebuild trues up; POST /api/v1/shopify/cogs/rebuild (?dry_run=true only reports) queues the rebuild in the background: catalog read, opening stock, line linking and costing, then a true-up of every posted payout (per-payout stores) or day (daily-summary stores). Unchanged periods book nothing; a period in a closed accounting period keeps its difference as a pending adjustment that you post into an open period from the subledger. Agents use ebuild_shopify_cogs (MCP and chat; approval-gated, dry_run supported).

Map posting onto your own chart

If your chart already has the accounts (a Shopify Clearing Account, your own sales, fees or Shopify Capital loan ledgers), map them instead of letting DayZero create its default ledgers. GET/PUT /api/v1/shopify/stores/{store_id}/ledger-mapping takes a ledger id (or null to clear) for each of clearing, sales, discounts, shipping, fees, chargebacks, cogs, inventory, capital_repayment, sales_tax (all jurisdictions on one ledger) and refunds. Inactive ledgers, another business's ledgers and ledgers of the wrong type are refused (VALIDATION_725). The same panel is on the Shopify posting settings card.

To map before anything posts:

  1. Connect the store.
  2. PUT .../ledger-mapping with {"auto_post": false} (or switch Post automatically off) before the first sync finishes. Payouts keep syncing into the subledger; no journal entry is written.
  3. Map each bucket.
  4. Check GET /api/v1/shopify/stores/{store_id}/posting-summary?from=YYYY-MM-DD&to=YYYY-MM-DD.
  5. Switch auto_post back on. Unposted payouts post on the next sync, or queue them now with POST /api/v1/shopify/subledger/backfill.

Bank account. Map bank to the account Shopify deposits payouts into. It has no default: with no bank mapped and bank matching off, payouts are held unposted and the mapping response, posting summary, subledger summary and sync result carry shopify_bank_unmapped, because posting to a made-up bank ledger would double the deposit your bank feed already books. Map it (or turn on bank matching) and the held payouts post on the next sync.

Shopify Capital. Shopify takes Capital repayments out of each payout, so the deposit arrives net. The payout entry debits the capital_repayment ledger for the amount withheld (a reversal credits it). With no capital ledger mapped the amount goes to a suspense ledger named Shopify Capital – unmapped, and the mapping response, the posting summary and the sync result carry the warning shopify_capital_unmapped; map the loan ledger and reclass the suspense balance. Payout activity of a type DayZero does not recognise posts to Shopify Unclassified Payout Activity and is logged, rather than being folded into rounding.

More buckets. The mapping also takes store_credit (refunds issued as store credit, and store credit spent), return_fees (restocking / return fees kept), tips_payable, disputed_funds, shopify_credit (the Shopify Credit card), capital_fees and sales_accrual (month-end cutoff), plus sales_tax_ledgers: {"US-CA": "<ledger id>"} maps one state's (or province's) tax liability; a local tax under it (US-CA Los Angeles County Tax) follows the state, and null removes a mapping. The response lists tax_jurisdictions the store collected tax in. When the mapped bank also has a bank feed and bank matching is off, the response carries bank_feed_double_count and the warning shopify_bank_feed_double_count: each Shopify deposit the feed brings in (same amount, Shopify-looking, within five days of a posted payout) has its offset re-pointed to the bank itself, so the income is booked once; the subledger records it as a match (duplicate_excluded) and unmatching restores the deposit. Turning on bank matching is the cleaner setup.

Accrual rules

Periods posted from this release follow these rules (a payout or day posted earlier is always trued up under the rules it was posted with, so nothing already booked moves):

  • Sales tax by jurisdiction. Tax lines book to Sales Tax Payable - <country>-<province> from the order's ship-to (else bill-to) address: US-CA, CA-ON, GB. A county, city or district tax keeps its own line under its state (US-CA Los Angeles County Tax). Stored tax lines carry country_code / province_code (backfilled from the orders).
  • Refunds. Refunded duties come off Duties Collected, a restocking or return fee kept is credited to Restocking and Return Fee Income, and the part of a refund issued as store credit credits Shopify Store Credit Liability instead of the tender's clearing. Store credit spent on a later order debits that liability. Shipping refunds are read from refund_shipping_lines (or shipping_refund adjustments on older payloads). An exchange is booked by its cash and flagged in the calculation trace.
  • Disputes. Funds Shopify withdraws for a dispute debit Disputed Funds Receivable (the dispute's reserve_journal_entry_id names the payout entry). A won dispute's reversal credits it back. A lost one is settled by the daily upkeep: Debit Sales Refunds and the order's share of sales tax, Credit the receivable (resolution_journal_entry_id). The chargeback fee stays an expense of the payout that withheld it.
  • Shopify Capital and Credit. With capital_fee_rate_bps set (the fee over the total repayable), each remittance splits into principal (the Capital ledger) and Shopify Capital Fees; a fee Shopify sends on the transaction wins. lending_credit* activity posts to Shopify Credit, not the Capital loan.
  • Tips. With tips_staff_owed on, tips on POS orders credit Tips Payable.
  • Channels and locations. With split_by_channel / split_by_location on, product sales and discounts post one line per sales channel / POS location, tagged with an accounting class (Shopify <channel>) and location (Shopify location <id>, Shopify online), created on first use.
  • Gateways. Marketplace sales (Amazon, TikTok Shop, Walmart, eBay, Etsy, Target Plus, Faire, by gateway or source_name) clear through Marketplace Clearing - (or the mapped other-gateways clearing). gateway_fees ({"paypal": {"percent_bps": 349, "fixed_cents": 49}}; kinds paypal, bnpl, manual, pos_cash, other, marketplace or one marketplace:<name>) books the expected processor fee with the day (Debit fees, Credit that clearing), so the clearing drains to what the gateway deposits.
  • Month-end cutoff (month_end_cutoff, on for stores connected from now on, opt-in for existing ones, per-payout stores). After the month ends, the daily sync posts on its last day an accrual for the sales charged in the month whose payout was not paid by then (Debit Shopify Sales Accrual, Credit revenue, tax and shipping, with their COGS) and its reversal on the 1st; the payout then books the sale as usual and the two cancel. Once per month (entries carry [src:shopify:accrual:YYYY-MM]); nothing is posted when either day is in a closed period. Subscription renewals and pre-orders are left to their deferrals. GET /api/v1/shopify/subledger/accruals lists them.
  • Daily true-up. After each daily sync, posted payouts of the last 60 days whose orders, line items (cost) or refunds changed since they were posted or last checked are trued up; a closed period's difference waits as a pending adjustment.
  • Late activity (late_activity): hold (default) holds a new payout or day dated in a closed period until it is reopened; post_in_next_open_period posts it on the first day of the next open period, its memo naming the original date.
  • Currency. A shop selling in a currency other than the business's base currency is held with currency_mismatch (nothing is converted); the connect preview returns currency_mismatch with the reason.

These settings are fields of PATCH /api/v1/shopify/subledger/posting-config (month_end_cutoff, late_activity, tips_staff_owed, gateway_fees, capital_fee_rate_bps) and are on the ledger mapping panel under Advanced.

Gateway reconcile. GET /api/v1/shopify/subledger/gateway-reconcile?as_of= returns each tender clearing's balance (Shopify Payments, PayPal, Klarna / Afterpay, manual, POS cash, other gateways, each marketplace, store credit) and its fee setting, and ties Gift Card Liability to Shopify's outstanding gift-card balance (needs the gift card permission; without it scope_available is false). POST .../gateway-reconcile/{gateway}/adjustment with {"as_of": "YYYY-MM-DD"} previews one entry clearing that residual into the fee ledger; send "dry_run": false once the user approved it (admins). The subledger's Gateways tab does both.

Sales tax report. GET /api/v1/shopify/subledger/sales-tax?from=&to= (at most 400 days) returns, per jurisdiction and month, taxable sales, exempt sales (orders with no tax, by ship-to state), tax collected, tax refunded and net tax, in cents. An order taxed by a state and a county counts its sales in both. The subledger's Sales tax tab shows it with CSV export; agents use get_shopify_sales_tax.

Manual bank match. The payout drawer's match dialog also lists deposits someone already categorized; matching one re-classifies it to the clearing account after the person confirms (reclassify: true on POST .../payouts/{id}/bank-match), and unmatching restores its category.

Tie to the dashboard. The posting summary returns, per month: gross sales, discounts, shipping, tax, fees, chargebacks, Capital withheld, refunds, net deposit (all cents), the posted journal entry ids, payouts not yet posted and the clearing ledger balance at month end. Agents read it with the MCP tool get_shopify_posting_summary(business_id, start_date, end_date). To have a human approve an adjusting entry instead of posting it, queue it with create_pending_change.

Ask Regent

Once a store is connected, Regent (and any agent or MCP client on the commerce profile) reads the same synced data the pages above show. Ask in plain words; these are the tools behind the answers:

Question Tool
"How did Shopify do last month?", "sales vs the month before", "best sellers", "AOV", "refund rate", "margin" get_shopify_sales_report(start_date, end_date, compare, group_by, top_n) — totals, rates, prior-period change, a day/week/month series, top products, fulfillment mix
"Show recent orders", "which orders are missing COGS?", "did yesterday's orders post?" list_shopify_orders (chat: get_shopify_orders) and get_shopify_order
"When's my next payout?", "why isn't payout X posted?" list_shopify_payouts, get_shopify_payout
"What did Shopify fees cost?", "refunds by product", "which disputes need evidence?" get_shopify_fees, get_shopify_refunds, list_shopify_disputes
"Do the Shopify entries tie?", "is Shopify fully posted for September?" get_shopify_posting_summary, get_shopify_posting_gaps
"How much sales tax did we collect in California?", "sales tax to file for Q3" get_shopify_sales_tax(start_date, end_date)
"Is Shopify synced?", "why did Shopify stop syncing?" get_shopify_sync_status, check_shopify_connection

The sales report counts paid, partially refunded and refunded orders by the shop's local day, so it lines up with the Shopify dashboard; refunds and fees are by processed date. Gross margin is only computed over lines whose cost was captured, and data_quality.orders_missing_cogs says how many orders are outside that. Amounts are cents. The CFO Portal's Shopify view shows the same figures month by month.

Payout Reconciliation

Shopify payouts are matched against bank deposits for reconciliation. Each payout includes a breakdown of orders, refunds, fees, and adjustments — GET /api/v1/shopify/subledger/payouts lists them (filter by status, start_date, end_date, posted), and GET /api/v1/shopify/subledger/payouts/{payout_id} shows the detail.

Browse the Subledger

Everything DayZero pulled from Shopify is queryable, which is useful for tying a journal entry back to the orders behind it:

Endpoint Returns
GET /api/v1/shopify/subledger/summary Record counts (orders, payouts, refunds, posted vs. unposted summaries), the clearing-account balance and the store's posting settings
GET /api/v1/shopify/subledger/orders Orders; filter by journal_entry_id, financial_status, date range
GET /api/v1/shopify/subledger/orders/{order_id}/line-items Line items for one order
GET /api/v1/shopify/subledger/daily Per-day sales summaries
GET /api/v1/shopify/subledger/customers Synced customers (search supported)
GET /api/v1/shopify/subledger/backlink/{journal_entry_id} The Shopify records that produced a journal entry

These list endpoints use limit/offset paging rather than cursors.

Sync Status

bash
curl "https://api.ondayzero.com/api/v1/shopify/status" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Returns connected, healthy, shop_domain, orders_synced, products_synced, last_sync, and the current bootstrap progress (sync_state, sync_stage, sync_started_at, sync_completed_at, sync_error). While not connected it also returns the last store's disconnected_shop_domain, disconnected_at and disconnected_reason (app_uninstalled when the DayZero app was removed in Shopify, else disconnected). An app uninstall also sends an urgent notification (in-app and email), and the first sync after connecting sends one in-app summary of what needs attention (payouts held for a bank account, payouts not posted). GET /api/v1/shopify/stores lists the store rows (id, shop_domain, is_primary, shop_plan, shop_currency, disconnected_at, and sync_status: syncing, disconnected or not_synced). Every store connected beside the primary one reads syncing; not_synced is a leftover row nothing syncs.

Manual Import

To re-import products (PUT /import imports products and variants only; the verb predates /import-orders and is kept for existing callers) or pull all orders on demand (both queue a background job and return status: queued with a workflow_id; watch GET /api/v1/shopify/status for progress):

bash
# Import products
curl -X PUT "https://api.ondayzero.com/api/v1/shopify/import" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

# Import orders
curl -X POST "https://api.ondayzero.com/api/v1/shopify/import-orders" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

POST /api/v1/shopify/bootstrap queues the full end-to-end catch-up (products, inventory, orders, payouts, balance transactions and summary posting). It reads from the store's posting_start_date, so after moving that date earlier it imports the skipped stretch without deleting anything (the Integrations page runs it for you after asking). Orders are saved 100 at a time and orders already stored at the same Shopify version are skipped, so a run that stops part-way (or a repeat run) continues from where it stopped instead of starting over; a step retried after a worker restart picks up at the Shopify page it had reached. A run can take up to 8 hours on a large shop. A run that fails or times out sets the sync state to error with a plain message (no longer "syncing"), and GET /api/v1/shopify/sync/recovery says which step it stopped on; retry the import to continue. If payout or refund counts look wrong — typically because posting_start_date filtered out history on the original sync — POST /api/v1/shopify/resync wipes the subledger (journal entries and ledgers are untouched) and re-pulls the entire history. POST /api/v1/shopify/subledger/backfill with {"limit": 200} (default 50, max 500) queues posting for paid payouts and daily summaries that were pulled but never booked — GET /api/v1/shopify/subledger/summary shows how many are payouts_unposted / daily_summaries_unposted.

More than one store

A business can connect several Shopify stores, one per brand or region. Connect the second store the same way as the first (Connect form, GET /api/v1/shopify/install-url?shop=..., POST /api/v1/shopify/stores/connect-with-token, or opening DayZero from that store's Shopify admin). The first store stays the primary store: the business's own Shopify fields keep naming it and its token is kept. The second store gets its own store row, token, posting settings, ledger mapping (/api/v1/shopify/stores/{store_id}/ledger-mapping), history start date and daily sync.

  • Every order, payout, balance transaction, refund and dispute records the store it came from (shopify_store_id). Rows imported before a business could have two stores carry no store and belong to the primary store.
  • Each store's payouts and days post on their own. A second store's money in transit has ledgers of its own, named with the shop: Shopify Merchant Account (eu-brand), and likewise its PayPal / manual / POS / other gateway clearings, Shopify Accounts Receivable, Shopify Reserve, Disputed Funds Receivable and Shopify Sales Accrual. Revenue, fee, tax and COGS ledgers are shared. Map a store's clearing (or any other ledger) on its ledger mapping to use your own account instead.
  • With two or more stores every Shopify summary line is tagged with the store's class, Shopify store <shop>, so reports split by store. A line already tagged with a sales-channel class keeps it.
  • Webhooks for the second shop route to the business. Uninstalling the DayZero app from the second shop disconnects only that store; the other stores keep syncing.
  • GET /api/v1/shopify/subledger/summary, /subledger/orders, /subledger/payouts, /subledger/daily, /subledger/daily/{date} and POST /subledger/daily/{date}/repost take store (shop domain, handle or store id) to narrow to one store; without it they cover every store. An unknown store is a 404. The Shopify card lists each store with its own counts, and the subledger page has a store picker (?store=<shop domain>).
  • The MCP and assistant tools list_shopify_orders, list_shopify_payouts and get_shopify_sync_status take an optional store. Without it they cover every store; store-level answers (posting mode, bank mapping) come from the primary store.

Limits: the products catalog is one catalog per business (products from both stores land in it), the sync progress shown on the card is per business, and when the primary store is disconnected while another stays connected the other stops syncing until the primary is reconnected.

Rows imported before stores were told apart can be pinned to the primary store with python scripts/admin/backfill_shopify_store_ids.py --execute (dry run without --execute; batched). Run it before making a different store the primary.

Move a Store to Another Business

POST /api/v1/shopify/stores/{store_id}/move moves a live store between two businesses where the caller is owner or admin (directly or through the managing firm); the current business must be one end. Send {"target_business_id": "…", "dry_run": true} to preview the rows that would be purged. To apply, send confirm set to the shop domain; applying also needs a recent re-verification of the signed-in user. The token and store re-point, the old business keeps its journal entries but its Shopify subledger rows are purged, and the new business syncs from today. Returns 409 if the store is disconnected or is not that business's primary shop.

Disconnect

Preview the impact first with GET /api/v1/shopify/disconnect/preview, then choose how much to remove with purge_mode:

bash
curl -X DELETE "https://api.ondayzero.com/api/v1/shopify/disconnect?purge_mode=connection_only" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
purge_mode Effect
connection_only (default) Revokes the token, removes webhooks and clears credentials. Subledger and journal entries stay.
subledger Also deletes every synced Shopify record (orders, payouts, customers…). Journal entries stay intact.
full Subledger purge plus a reversing journal entry for every Shopify-sourced entry, and removal of any Shopify-managed ledger left empty. Requires confirm=WIPE; pass cutoff_date=YYYY-MM-DD to leave entries in closed periods alone.

If Shopify-side cleanup fails (the webhooks could not be deleted or the token could not be revoked), the disconnect still completes in DayZero and the response says so with webhooks_removed: false / token_revoked: false. The Integrations page then warns you; remove the DayZero app from the shop's Shopify admin (Settings, Apps) to be certain it can no longer reach the store.

Common errors

Status Cause Fix
403 The business does not have the Commerce add-on, or the token's user is not an admin for connect, disconnect, resync and move Enable the add-on, or use an admin token
400 Setup token rejected by Shopify, or a purge_mode=full disconnect without confirm=WIPE Check the token and its scopes with connection/diagnose?live=true; send the confirmation
409 stores/connect-with-token or move against a shop that is already live on another business, or a disconnected or non-primary store Disconnect it from the other business first
400 (INTEGRATION_059) finalize-pending-install after the parked App Store install expired (one hour) or was used Open DayZero from Shopify admin again, or connect with the store domain
held currency_mismatch The shop's currency is not the business's base currency Change one of them; Shopify amounts are not converted
400 (VALIDATION_725) ledger-mapping names an inactive ledger, another business's ledger or a ledger of the wrong type Pick an active ledger of the right type from this business