How to Bulk Import Supplier Products into WooCommerce from a Mailhook CSV

Receive a supplier price-list CSV by email, parse and AI-normalize every row, then create or update the matching WooCommerce products automatically.

What This Integration Does

Suppliers rarely send catalog updates in a format your store can use directly. One ships a spreadsheet with prices in a column called RRP, another calls it retail, a third pads SKUs with leading zeros and writes product names in ALL CAPS. This Spojit workflow lets a supplier simply email their price-list CSV to a dedicated address. Spojit pulls the file off the email, reads each row, uses an Agent-mode Connector node to normalize the messy fields into a clean, consistent shape, then upserts each item into WooCommerce. New SKUs become new products; existing SKUs get their price and details updated. No one has to open the spreadsheet or touch the WooCommerce admin.

The run is triggered by a Mailhook: Spojit generates a unique inbound address, and any email sent to it starts a run within seconds, with no mailbox or OAuth to connect. The CSV attachment is fetched on demand, parsed into rows, and each row flows through a Loop that checks WooCommerce for an existing product by SKU and then either creates or updates it. The workflow leaves your WooCommerce catalog in sync with the latest supplier file and emails a short summary back to the sender. Because the upsert keys on SKU, re-running the same file (or receiving a corrected one) is safe: it updates existing products in place rather than creating duplicates.

Prerequisites

  • A WooCommerce connection added in Spojit (Connections - Add connection - WooCommerce) with REST API credentials that can read and write products.
  • A supplier price-list CSV that includes, at minimum, a SKU column, a product name, and a price. Column names do not need to be standardized; the agent handles the variation.
  • The supplier's sending address, so you can optionally restrict the Mailhook to only accept mail from them.
  • Approval that AI normalization (Agent mode and a Knowledge query, if used) consumes credits per run. See Managing Credits for details.

Step 1: Start the workflow with a Mailhook Trigger

Create a new workflow and set the Trigger node type to Mailhook. Enter an Address prefix such as supplier-feed (1-24 characters), then click Generate email address. Spojit produces a unique address like supplier-feed-3f9a1c7b2d4e6f80@mailhook.spojit.com. Copy it and send it to your supplier (or point a forwarding rule at it). Under the trigger's filters, add the supplier's address to the From allowlist so unrelated mail is ignored, and optionally set a Subject regex like price.?list to only run on the right emails.

The trigger output is available downstream as {{ input }}, including {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and {{ input.attachments }}. Each attachment reference looks like { id, filename, contentType }; the actual bytes are fetched in the next step. For more on setup and rotation, see Setting Up a Mailhook Trigger and Filtering Mailhook Emails.

Step 2: Fetch the CSV with an Attachment node

Add an Attachment node. It only saves inside a Mailhook workflow, which this is. Set Mode to Single so you get one file as an object, set the Filename pattern to *.csv (or something tighter like price-*.csv), and optionally set Content type to text/csv. Turn on Fail if no attachment matches so a stray email with no spreadsheet does not silently produce an empty import.

The node outputs { filename, contentType, size, content }, where content is the base64-encoded file. Keep in mind the default limits of 10 MB per attachment and 25 MB per run. You will pass {{ attachment.content }} into the parser in the next step. See How to Set Up Email-Triggered Document Processing for a related attachment pattern.

Step 3: Decode and parse the CSV into rows

The Attachment content is base64, so first add a Connector node in Direct mode on the encoding connector and pick the base64-decode tool, mapping its input to {{ attachment.content }}. This gives you the raw CSV text.

Next add a Connector node in Direct mode on the csv connector and choose the parse tool, feeding it the decoded text. The result is an array of row objects keyed by the supplier's original headers, for example:

[
  { "Item No": "00123", "PRODUCT": "WIDGET - BLUE", "RRP": "$24.95" },
  { "Item No": "00124", "PRODUCT": "widget green", "RRP": "29" }
]

Reference the parsed rows downstream as {{ csv_rows.data }} (name the node's output variable csv_rows). If your supplier ever sends tab-separated or oddly quoted files, the parse tool handles common delimiters; see How to Validate and Clean CSV Data Before Import for cleanup techniques.

Step 4: AI-normalize every row with a Connector node in Agent mode

Supplier columns are inconsistent, so use a Connector node in Agent mode to map them into a fixed shape. Add a Connector node and switch it to Agent mode. In the prompt, pass the raw rows and ask for a normalized list, then attach a Response Schema so the output is reliable JSON rather than prose. A prompt like the following works well:

Normalize this supplier price list into clean product records.
Rows: {{ csv_rows.data }}

For each row produce:
- sku: the item identifier, trimmed, leading zeros removed
- name: the product name in Title Case, no extra spacing
- regular_price: the price as a plain decimal string, no currency symbol
Skip rows that have no SKU or no price.

Set the Response Schema to force the array shape, for example:

{
  "type": "object",
  "properties": {
    "products": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "sku": { "type": "string" },
          "name": { "type": "string" },
          "regular_price": { "type": "string" }
        },
        "required": ["sku", "name", "regular_price"]
      }
    }
  }
}

Name the output variable normalized so the clean list is available as {{ normalized.products }}. For guidance on when Agent mode pays off versus Direct mode, read How to Choose Between Agent Mode and Direct Mode and How to Use Structured Output for Reliable AI Data Extraction.

Step 5: Loop over the products and look up the existing SKU

Add a Loop node set to ForEach over {{ normalized.products }}. Each iteration exposes the current item, which you can reference as {{ item }}.

Inside the loop body, add a Connector node in Direct mode on the woocommerce connector and select the list-products tool. Map its sku filter to {{ item.sku }} so it returns only the product (if any) that already carries that SKU. Name this node's output existing. A non-empty result means the product is already in your catalog and you should update it; an empty result means you create it. For more on iteration, see Using Loop Nodes.

Step 6: Branch on whether the product exists, then create or update

Still inside the loop, add a Condition node that checks whether {{ existing.data }} contains a match (for example, that its length is greater than 0).

On the true branch (product exists), add a Connector node in Direct mode on the woocommerce connector with the update-product tool. Map id to the matched product's id, such as {{ existing.data.0.id }}, and set regular_price to {{ item.regular_price }} and name to {{ item.name }}. You can also set description or short_description if your normalization produced them.

On the false branch (new product), add a Connector node in Direct mode on the woocommerce connector with the create-product tool. Map the fields:

name:           {{ item.name }}
sku:            {{ item.sku }}
regular_price:  {{ item.regular_price }}
status:         publish

Because every decision keys on SKU, the same file can run twice with no duplicate products created. See the WooCommerce connector reference for the full field list on these tools.

Step 7: Email a summary back to the supplier

After the loop, add a Send Email node so the sender gets confirmation. Set Recipients to {{ input.replyTo }} (the address the supplier sent from), give it a Subject such as Price list imported, and write a short templated Body that references how many rows were processed, for example We imported your latest price list ({{ normalized.products.length }} items). Set If sending fails to Continue anyway so a delivery hiccup does not roll back a successful import. Note that external recipients must be on your org allowlist; see Configuring the Email Allowlist and Using Send Email Nodes.

Tips

  • Keep the Agent-mode normalization tightly scoped to a small JSON shape. The fewer fields you ask the agent to produce, the cheaper and more reliable each run is.
  • WooCommerce treats prices as strings. Always pass regular_price as a plain decimal string like 24.95, never with a currency symbol, or the create will be rejected.
  • If a supplier sends very large catalogs, watch the 25 MB per-run attachment limit. For multi-thousand-row files, ask the supplier to split the export or compress images out of the CSV.
  • Use categories on create-product only if you have stable category IDs; otherwise leave products uncategorized on import and tidy them later.

Common Pitfalls

  • Skipping the base64 decode. The Attachment node returns content as base64, so feeding it straight into the csv connector's parse tool produces garbage. Always decode first.
  • Relying on the list-products sku filter without an exact SKU. If your normalization leaves stray spaces or zero-padding, the lookup misses and you create a duplicate instead of updating. Trim and standardize SKUs in Step 4.
  • Forgetting the From allowlist on the Mailhook. Without it, any email to the generated address starts a run; a misdirected newsletter could trigger an empty or wrong import.
  • Assuming column names are stable. Suppliers rename headers without warning. The Agent-mode step exists precisely to absorb this drift, so resist hardcoding header names in a Transform instead.

Testing

Before pointing the real supplier at the address, build a tiny CSV with two or three rows: one SKU that already exists in WooCommerce and one brand-new SKU. Email it to the generated Mailhook address from an allowlisted account. Open the run in your execution history and confirm the Attachment node fetched the file, the parse produced the expected rows, the Agent-mode output matches your schema, and the loop took the update branch for the known SKU and the create branch for the new one. Check the products in WooCommerce, then re-send the same file once to prove no duplicates appear. Only after that should you hand the address to your supplier.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.