Migrating WooCommerce Data to a Custom Application with the REST API
Moving a store from WooCommerce to a custom application (or another platform) is mostly a data problem: products, variations, customers and order history have to be exported completely and mapped to a new model. The WooCommerce REST API is usually the safest way to get that data out, because it returns the same structure regardless of how the store stores orders internally. This guide outlines a practical export plan.
Table of Contents
- Plan the migration
- Set up read-only access
- Export order: what to pull first
- An export script
- Data that needs special handling
- Incremental sync and cut-over
Plan the Migration
- Inventory the data: products and variations, categories/tags/attributes, customers, orders (with line items, refunds and notes), coupons, and extension data such as subscriptions or bookings.
- Inventory the behaviour: tax rules, shipping zones, payment gateways, emails, and any plugins that change checkout. These are rebuilt, not exported.
- Decide what history you need: many teams migrate open orders and the last few years of history, and archive the rest as a raw JSON export.
- Keep the old IDs: store WooCommerce IDs alongside your new IDs so you can reconcile, redirect URLs and re-run the import.
Set Up Read-Only Access
Create a dedicated API key with Read permission for the export (how to create keys). The key's user needs permission to view orders and customers, so use a Shop Manager or Administrator account. Run exports against a recent copy of the store if the live server is under load.
Export Order: What to Pull First
- Reference data:
/products/categories,/products/tags,/products/attributes(+ terms),/taxes,/shipping/zones. /products, then/products/<id>/variationsfor every variable product./customers?role=all(customers who checked out as guests only exist inside orders)./orders, plus/orders/<id>/refundsand/orders/<id>/noteswhere you need them./coupons.
See the wc/v3 endpoints reference for parameters.
An Export Script
This Node.js script pages through a collection and writes newline-delimited JSON, which is easy to re-import and diff:
// export.mjs - node export.mjs products orders customers
import { createWriteStream } from 'node:fs';
const BASE = process.env.WC_URL; // e.g. https://shop.example.com/wp-json/wc/v3
const AUTH = 'Basic ' + Buffer.from(`${process.env.WC_KEY}:${process.env.WC_SECRET}`).toString('base64');
async function get(path) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`${BASE}/${path}`, { headers: { Authorization: AUTH } });
if (res.ok) return res;
if (attempt >= 5 || res.status < 500) throw new Error(`${res.status} on ${path}`);
await new Promise((r) => setTimeout(r, 1000 * attempt)); // back off on 5xx
}
}
async function exportCollection(name, query = '') {
const out = createWriteStream(`${name}.ndjson`);
let page = 1, totalPages = 1;
do {
const res = await get(`${name}?per_page=100&page=${page}&orderby=id&order=asc${query}`);
totalPages = Number(res.headers.get('X-WP-TotalPages') || 1);
for (const item of await res.json()) out.write(JSON.stringify(item) + '
');
console.log(`${name}: page ${page}/${totalPages}`);
page++;
} while (page <= totalPages);
out.end();
}
for (const name of process.argv.slice(2)) {
await exportCollection(name, name === 'customers' ? '&role=all' : '');
}
Sorting by id keeps pages stable while new orders arrive during the export. For variations, read products.ndjson, filter type === "variable" and call products/<id>/variations for each.
Data That Needs Special Handling
- Passwords: the API never returns password hashes. Either ask customers to reset their password on first login, or export hashes directly from the
wp_userstable and implement WordPress-compatible verification (phpass for older hashes; WordPress 6.8+ uses bcrypt) in the new system. - Payment tokens and subscriptions: saved cards live at the payment provider (Stripe, etc.). Migrating them is done with the provider, not through WooCommerce.
- Prices and totals are strings in the API; convert to integer minor units (cents) on import to avoid floating-point errors.
- Order meta: extension data appears in
meta_data; keys starting with_(protected) may be missing and need a custom endpoint or direct database export. - HPOS: since WooCommerce 8.2, new stores keep orders in dedicated
wc_orderstables instead ofwp_posts. The REST API hides this difference; raw SQL exports must know which storage is active. - Images: product image
srcURLs point at the old site – download them before shutting it down. - URLs: export product and category permalinks so you can set up 301 redirects.
Incremental Sync and Cut-Over
- Run a full export and import into the new system; reconcile counts (
X-WP-Total) and order totals. - Keep the systems in step with incremental pulls using
modified_after(supported on recent WooCommerce versions) or with webhooks fororder.*,customer.*andproduct.*. - At cut-over, put the store in maintenance mode, run a final incremental sync, switch DNS, and keep the old site read-only for reference.
- Revoke the export API key once the migration is signed off.
Related: WooCommerce REST API guide, adding a custom endpoint (useful for exporting protected meta), and headless WordPress if you only want to replace the front end.