Skip to content
Gaurav TiwariDocs

Import a FluentCart or EDD Store

Version 0.2.0 Updated November 15, 2026

How to move a FluentCart or Easy Digital Downloads store into Vendwick: export, dry run, import, what is mapped, and how installed plugins keep their licenses. The page ends with the export file format and the admin routes.

The store importer brings your products, plans, customers, orders, license keys and site activations into Vendwick in one pass. License keys come over exactly as they are, so plugins and themes your customers already installed keep working once their updater points at the new store.

It works in three steps:

  1. Export the WordPress store to one file with vendwick-export.php (read-only).
  2. Upload the file in Vendwick and review a dry run: what would be created, skipped, and why. Nothing is written.
  3. Run the import. Follow the progress, then download any conflicts as CSV.

What the import never does:

  • It sends no email to anyone, queues none, and fires no order or license events (so no webhooks, receipts or invoices go out).
  • It does not number invoices; invoice numbers from the old store are kept.
  • It does not touch the WordPress store. The exporter only reads.
  • It does not move payment methods or automatic renewals (see Subscriptions).

Easy Digital Downloads is untested. The EDD exporter is built from EDD 3’s documented tables and Software Licensing and Recurring Payments, and it is covered by tests with sample rows, but it has not been run against a real EDD store yet. Read the dry run carefully and check a few imported licenses before you switch your updaters over.

Before you start

  • Turn on the Store Importer module (Modules page). It needs Licensing.
  • Private file storage must be configured (it is where release files live too).
  • Set Default currency in Store settings, or note the currency you want for records that have none.
  • Import into a store that is not yet selling, or at a quiet moment. The import runs alongside live traffic safely, but a fresh store makes the dry run easier to read.

Step 1: export from WordPress

Copy clients/wordpress/vendwick-export.php from the Vendwick package to the WordPress site. Pick one way to run it.

WP-CLI (best for large stores). Put the file in wp-content/mu-plugins/, then:

wp vendwick-export fluentcart --output=fluentcart.ndjson.gz
wp vendwick-export edd --output=edd.ndjson.gz

Or run it once without installing it:

wp eval-file vendwick-export.php fluentcart --output=fluentcart.ndjson.gz

A file name ending in .gz is gzip-compressed (much smaller). Without --output the export goes to standard output.

Admin download. With the file in mu-plugins (or activated as a plugin), go to Tools, Vendwick export as an administrator, pick the store and download. The download is streamed and not cached.

Either way, the exporter reads rows in batches of 500, so memory stays flat on large stores. Requirements: PHP 7.4+, WordPress 5.5+, FluentCart or EDD 3 tables in the database (the plugins themselves may be inactive).

The file holds license keys and customer details. Keep it private, delete it after the import, and remove the exporter from the server.

Step 2: upload and dry run

Open Store import (Store settings). Drop the file on the page or choose it. The page reads the file’s header before uploading and shows the source, the store URL, when it was exported and how many records it holds.

Files up to 7.5 MB go to the plugin’s upload route; larger ones go through the companion’s /_vendwick/admin/import-upload endpoint (up to 512 MB as sent, 4 GB once decompressed). Both stream the file into private storage.

Choose the options, then Start dry run:

OptionDefaultWhat it does
Imported productsHidden (archived)Archived products are not on sale but their licenses and updates work. Choose "On sale right away" to make them active. Products that were drafts in the old store are always archived.
Skip orders beforenoneOrders paid before this UTC day are left out of orders and reports. Their licenses are still imported, without an order.
Skip refunded ordersoffFully refunded orders are left out.
Default currencythe export’s store currencyUsed for records with no valid currency.

The dry run reads the whole file and reports, per record type: in the export, new, already imported, updated, skipped, conflicts. It lists the first 500 conflicts with their reasons (the total is always shown), lines it could not read (with their line numbers), and warnings such as subscriptions that will stop renewing or activations without a site URL. It writes nothing. You can run it again with other options.

Step 3: import

Start import runs the same passes and writes the records. The progress view shows each pass:

products, plans, customers, orders, order items, licenses, site activations, subscriptions, coupons, then seats, counters and reports.

You can close the page: the cron tick (every 5 minutes) continues the import, and the page picks it up again when you come back (the run id is in the URL). Cancel stops a run and deletes the uploaded file; records already written stay.

When it finishes, the uploaded file is deleted, the summary lists what was imported, and Download CSV gives the conflicts (type, source id, reason, line, message).

Then:

  1. Upload releases. Release files are not in the export. Open each imported plugin and theme and upload its current ZIP on the Releases tab, or installed copies will see no update.
  2. Point the old updater at this store (next section).
  3. Review the products, then set them active when you are ready to sell.
  4. Cancel the subscriptions at the old payment provider when customers have moved (see Subscriptions).

Installed plugins and themes: license keys and item ids

License keys are imported byte for byte. The license API is EDD Software Licensing compatible (check_license, activate_license, deactivate_license, get_version), on any front-end URL with ?edd_action= and at /_vendwick/license.

Vendwick products get their own ids. Old clients still send the old item_id (the FluentCart product id or the EDD download id), so every imported product keeps its old ids in legacyItemIds, and the license API matches a request’s item_id against them:

  • A request with the product’s Vendwick id works, as for any product.
  • A request with the old id finds the imported product when that product owns the license, and the answer’s item_id is the id the client sent.
  • When a Vendwick id and an old id are the same number, the product that owns the license wins.
  • Clients that send item_name (common with EDD) match the product’s slug, and also its old slug and old name, kept in legacySlugs.

Each old id and slug is claimed (legacy-item-id:<id>, legacy-item-slug:<slug>), so two products can never answer for the same one. When a second import brings another product with an old id that is already taken (for example a FluentCart product and an EDD download that were both id 12), the product is imported but the id is not; the dry run reports it as legacy_id_clash, and that product’s installed copies can send item_name instead.

Point the updater at the new store. Change only the store URL in the plugin or theme you ship (EDD’s EDD_SL_Plugin_Updater takes it as its first argument; FluentCart’s updater has a similar setting) and release that version from the old store one last time, so installed copies pick it up. Keep the same item_id and license key settings. If your old store’s home page will be served as a static page in front of the Vendwick Worker, use https://your-store/_vendwick/license as the API URL, which always reaches the license API.

FluentCart’s own licensing SDK talks to a FluentCart-specific API that Vendwick does not emulate. Switch those products to an EDD-compatible updater, or to the Vendwick updater (clients/wordpress/class-vendwick-updater.php, which takes store_url, item_id and the key).

Site activations and seats

  • Each activation is stored with its site URL as the old store kept it, with https:// added when it had no scheme (FluentCart and EDD keep sites without one), and its site hash is recomputed the way the license API computes it.
  • Rows for one site in several forms (with and without www., http and https) are merged into one activation.
  • Inactive activations come over as inactive. Activations without a URL are skipped.
  • Staging and local sites do not use a seat when "Staging and local sites don’t use a seat" is on (Store settings), the same as for new activations.
  • Each license’s seat counter is recounted at the end, so it equals its imported active activations. A license that had more active sites than its limit keeps them; new activations wait for a free seat.
  • A site that reports itself in a different form than the one stored (for example http:// while the import stored https://) is a new site to the license API and activates again, using a seat if one is free.
  • FluentCart’s per-activation hash is kept in the import link (meta.activationHash) for reference. Keyless activation by that hash is not supported.

What is mapped

Old storeVendwick
FluentCart product (fluent-products post with license_settings), EDD downloadProduct: plugin, theme (by name, slug or a category containing "theme") or digital (no licensing); version, requires WP and PHP, tested up to. Physical products are skipped.
FluentCart variation, EDD price (variable or single)Plan: price, currency, one-time (lifetime, with the validity in days) or subscription (day, week, month, 3 or 6 months, year), site limit (0 or blank = unlimited), trial days.
CustomerCustomer, merged by email (lower-cased). A customer that already exists keeps its fields; only blank ones are filled.
Paid, partially refunded and refunded ordersOrder FC-<id> or EDD-<id> (a suffix is added when the number is taken), amounts in minor units, discount, tax, refunded amount and date, payment method, transaction id, invoice number (kept and claimed, so new invoice numbers skip it). Pending, failed and canceled orders are skipped.
Order itemsOrder items with quantity, unit price, total, and the per-item discount and tax where the source stores them.
LicensesLicenses with the same key, product, plan, order, item, customer, status (FluentCart active and inactive are active, expired is expired, anything else disabled), site limit and expiry.
ActivationsSee above.
Subscriptions (FluentCart, EDD Recurring)Local subscriptions for the license, with their status and period, that do not renew automatically.
Coupons (FluentCart coupons, EDD discounts)Coupons with the same code, percent or fixed amount, dates, use limits, uses so far and minimum.

Subscriptions

Payment providers’ subscriptions cannot move between stores, so imported subscriptions are local records with automatic renewal off, no provider link, and a note on their import link. The old provider keeps billing until you cancel there. When a term ends, the subscription and its license expire as usual; customers renew from their account page, or you send them a renewal link. The dry run warns about every active subscription this affects.

Not imported

  • Release files (upload them afterwards), product images and galleries, product page content beyond a short description.
  • Refunds as separate records (a refunded order keeps its refunded amount and date), disputes, payment events and checkouts. No credit note is issued for a refunded imported order: the old store’s credit notes remain the record, and issuing new numbers for past refunds would create documents that never went to the customer.
  • Pending, failed, canceled and abandoned orders; physical products and their order items.
  • Tax rates, shipping, store settings, email templates, users and passwords.
  • FluentCart’s activation hashes as a way to activate (kept for reference only).

Re-running and updating

Every imported record is linked to its old store and old id (importLinks, one per record, claimed through import-link:<source>@<site>:<type>:<id>, where <site> is the export header’s site_url without the scheme, www. and trailing slash). A second run, with the same file or a newer export of the same store, writes nothing new: records already imported are counted as "already imported". The only change it makes is to licenses: when the old store changed a license since the last import (its updated_at is newer), its status, site limit and expiry are copied. New records in the newer export are imported.

A change made in this store wins over the export. When a license was changed here after it was imported (you disabled it, changed its sites or its expiry, or a refund or renewal did), a newer export leaves it alone and reports a changed_locally conflict instead, so a re-import can never re-enable a license you turned off. Licenses are the only imported records a re-run updates in place; everything else is written once.

This lets you import early, keep selling on the old store, and run the import again just before you switch.

Records are matched by source, old store and old id, so you can import two FluentCart (or EDD) stores into one Vendwick store even when their ids overlap: each store’s records get their own links. What can still clash between them is what must be unique in one store (a license key, a coupon code, a product slug); those show up as conflicts in the dry run.

Links written by an earlier 0.2.0 build had no store in them. They belong to the first store you imported from that source, and move to the new form the first time that store’s export runs again.

The uploaded file is deleted 7 days after upload, finished or not, by the store’s daily job. This also happens while the Importer module is turned off.

A run row stays small however large the store is: it keeps the first 100 conflicts itself and the rest (up to 500 in all) in importConflicts rows, commerce/import/conflicts returns all of them, and the dry run’s lists of ids it needs to remember are capped (beyond them, memo.truncated is set and the dry run’s counts are estimates; the import itself is exact).

Interrupted runs resume at the line where they stopped. A run that keeps failing at the same record stops after three attempts with the error; fix the cause and start it again from the run’s page (records already imported are skipped).

Undoing an import

There is no automatic undo. To find what a run wrote:

  • importLinks rows with runId set to the run’s id list every record it created (objectType, the old externalId and the Vendwick localId).
  • Imported products have details.importedFrom and details.sourceId; imported order items have fulfillmentData.importedFrom; imported orders are numbered FC-... or EDD-... and have no checkout.

Delete records from the admin as usual (products, coupons, licenses). Deleting a product does not remove its import link, so running the import again would bring it back under the same id.

The bundle format (version 1)

One UTF-8 NDJSON file: one JSON object per line, optionally gzip-compressed (detected by its magic bytes). Unknown fields are ignored. All amounts are integers in the currency’s minor unit (cents) with an ISO 4217 code; dates are ISO 8601 in UTC (2025-01-10T10:01:00Z; YYYY-MM-DD HH:MM:SS is read as UTC); ids are the old store’s ids, as integers or short strings ([A-Za-z0-9._:-], up to 64 characters). Lines may be in any order: the importer reads the file once per record type.

Line 1 is the header:

{"vendwick_export":1,"source":"fluentcart","source_version":"1.2.6","site_url":"https://old-store.example/","generated_at":"2026-11-15T09:00:00Z","currency":"USD","counts":{"product":6,"plan":7,"customer":3,"order":7,"order_item":9,"license":6,"activation":7,"subscription":2,"coupon":2},"exporter":"vendwick-export.php 1.0.0"}

source is fluentcart or edd. counts are used for progress bars only.

Every other line is a record whose first key is type (the importer reads the type from the start of the line to skip other types quickly):

typeFields (required in bold)
productid, name, slug, status (active, draft, archived), kind (plugin, theme, digital, service, physical), licensed, description, version, activation_limit (0 = unlimited), homepage, requires, requires_php, tested, created_at, updated_at
planid, product_id, name, slug, status (active, archived), price_minor, currency, billing (one_time, subscription), period (day, week, month, year, lifetime), interval, activation_limit, validity_days (one-time plans; 0 = lifetime), trial_days, created_at
customerid, email, first_name, last_name, company, phone, address1, address2, city, state, postcode, country (ISO 3166 alpha-2), tax_id, created_at
orderid, status (paid, partially_refunded, refunded, pending, failed, cancelled), number, customer_id, email, name, kind (purchase, renewal), currency, subtotal_minor, discount_minor, tax_minor, total_minor, refunded_minor, coupon_code, tax_inclusive, tax_country, tax_id, payment_method, transaction_id, invoice_number, created_at, paid_at, refunded_at
order_itemid, order_id, product_id, plan_id, name, quantity, unit_minor, total_minor, discount_minor, tax_minor (null when the source has no per-item amounts), created_at
licenseid, key (1 to 191 characters, kept exactly), product_id, status (active, expired, disabled), plan_id, order_id, order_item_id, customer_id, email, activation_limit (0 = unlimited), expires_at (null = lifetime), subscription_id, created_at, updated_at
activationid, license_id, site_url, status (active, inactive), activated_at, last_seen_at, activation_hash
subscriptionid, product_id, status (active, trialing, past_due, cancelled, expired), license_id, plan_id, order_item_id, customer_id, email, period (day, week, month, year), interval, gateway, recurring_minor, currency, current_period_start, current_period_end, cancelled_at, created_at
couponid, code, discount_type (percent, fixed), amount (percent 0 to 100, or minor units), currency (fixed coupons), description, status, starts_at, ends_at, max_uses, max_uses_per_customer, uses, minimum_minor, product_ids, created_at

A line that is not valid JSON, has an unknown type, or breaks a rule above is reported with its line number and skipped; the run goes on.

Conflict reasons

ReasonMeaning
duplicate_license_keyThe key already belongs to another license in this store. Skipped; the existing license is not changed.
legacy_id_clashAnother product already answers the old item_id. The product is imported without that alias.
email_name_mismatchThe email belongs to an existing customer with another name. Merged; the existing name is kept.
unknown_product, unknown_plan, unknown_licenseThe record points at something not in the export. An order item with an unknown plan is imported without a plan; the others are skipped.
currency_mismatchNo valid currency and no default. Set the default currency option.
coupon_code_takenA coupon with that code already exists. Skipped.
subscription_existsThe license already has a subscription. Skipped.
changed_locallyA newer export changes a license that was also changed in this store after the import. Skipped; this store’s values are kept.

Routes

All admin-only (plugins:manage), off with the module:

RoutePurpose
commerce/import/uploadRaw body up to 8 MiB (?filename=), plain or gzip. Returns the run.
commerce/import/start{ runId, mode: "dry_run" | "import", options: { productStatus, skipOrdersBefore, skipRefunded, defaultCurrency } }
commerce/import/step{ runId }: processes the next slice (about 8 seconds), returns { done, busy, progress, run }. Call it until done.
commerce/import/status, commerce/import/list, commerce/import/cancelOne run, the runs newest first, cancel and delete the file.

The companion endpoint POST /_vendwick/admin/import-upload?filename= takes larger files (administrator session plus X-EmDash-Request: 1) and registers the run through internal/import/register.

Share

Was this helpful?

Still stuck? Open a support ticket