How to Create Shopify Products from an Emailed Catalog Spreadsheet
Build a Spojit workflow that receives a product catalog spreadsheet by email, reads every row, and creates a Shopify product for each one automatically.
What This Integration Does
Suppliers, buyers, and warehouse teams often share new product ranges as a CSV spreadsheet attached to an email: one row per item, with columns like title, price, SKU, and stock. Re-keying those rows into Shopify by hand is slow and error-prone. This tutorial wires up a workflow that turns that inbound email into finished Shopify listings. The moment a catalog lands, Spojit pulls the spreadsheet off the message, parses each row, and calls Shopify once per product, so a fifty-line catalog becomes fifty draft listings without anyone touching the admin.
The workflow runs on a Mailhook trigger, so it fires within seconds of an email arriving at a Spojit-generated address, with no mailbox or polling to manage. The Attachment node fetches the spreadsheet bytes, the csv connector parses them into rows, and a Loop creates one Shopify product per row using the shopify create-product tool. Each run is independent and leaves new products in Shopify; nothing is stored between runs. Because the trigger deduplicates per message, the same email re-delivered will not create duplicate products, though sending two different emails with overlapping rows will.
Prerequisites
- A Shopify connection in Spojit (Connections -> Add connection -> Shopify) with permission to create products in your store.
- A workflow using a Mailhook trigger. The Attachment node will not save without one.
- A known, consistent column layout for the catalog spreadsheet (for example
title,price,sku,quantity,vendor). Agree this with whoever sends the file. - The csv and encoding utility connectors, which are built in and need no setup.
Step 1: Add the Mailhook trigger and generate an address
Create a new workflow and set the Trigger node type to Mailhook. Optionally set an Address prefix (1 to 24 characters, default mh) such as catalog, then click Generate email address. Spojit produces a unique address like catalog-1a2b3c4d5e6f7g8h@mailhook.spojit.com. Copy it and have your supplier send catalogs there, or set up a forwarding rule that routes catalog emails to it. To keep noise out, add a From allowlist with the supplier's address and a Subject regex such as (?i)catalog|price\s*list. The trigger output is available downstream as {{ input }}, including {{ input.subject }}, {{ input.from }}, {{ input.replyTo }}, and the list of {{ input.attachments }}.
Step 2: Fetch the spreadsheet with an Attachment node
Add an Attachment node after the trigger. Because the catalog is a single file, set Mode to Single so the node returns one object. Narrow which file it grabs with a Filename pattern glob like *.csv (or catalog-*.csv), and optionally a Content type filter of text/csv. Turn on Fail if no attachment matches so a malformed email stops the run loudly instead of creating zero products silently. The node output gives you {{ attachment.filename }}, {{ attachment.contentType }}, {{ attachment.size }}, and {{ attachment.content }}, where content is the file encoded as base64. Note the 10 MB per-attachment and 25 MB per-run limits.
Step 3: Decode the file into CSV text
The Attachment node hands you base64, but the parser needs plain text, so add a Connector node in Direct mode on the encoding connector and pick the base64-decode tool. Map its input to {{ attachment.content }} and store the result, for example as csvText. This turns the raw bytes back into the readable comma-separated text of the spreadsheet, which the next step parses row by row.
Step 4: Parse the rows with the csv connector
Add a Connector node in Direct mode on the csv connector and select the parse tool. Map the csv field to the decoded text from the previous step (for example {{ csvText.result }}). Leave header on so the first row is treated as column names and each parsed row becomes an object keyed by column. Keep dynamicTyping on so numeric columns like price and quantity come back as numbers. The tool returns items (the array of row objects) and count (the number of rows). Save the output as catalog. A parsed row then looks like this:
{
"title": "Merino Crew Sweater",
"price": 89.95,
"sku": "MCS-001",
"quantity": 24,
"vendor": "Highland Knits"
}
Step 5: Loop over every row
Add a Loop node set to ForEach and point it at {{ catalog.items }}. Each iteration exposes the current row (for example as {{ item }}), so the body of the loop runs once per product in the spreadsheet. Keep the loop body small: a single Shopify call per row. If your catalogs are large, this is also where you control pace; see the Tips for batching guidance.
Step 6: Create each Shopify product
Inside the loop body, add a Connector node in Direct mode on the shopify connector and choose the create-product tool. Map its fields from the current row so each listing reflects one spreadsheet line:
{
"title": "{{ item.title }}",
"vendor": "{{ item.vendor }}",
"status": "draft",
"variants": [
{
"sku": "{{ item.sku }}",
"price": "{{ item.price }}",
"inventory_quantity": "{{ item.quantity }}"
}
]
}
Setting status to draft lets your team review the new listings before they go live. Once you trust the feed, switch it to active. If you want richer copy, you can later generate descriptions with Miraxa, the intelligent layer across your automation, before this step.
Step 7: Email a confirmation back to the sender
After the loop, add a Send Email node so the supplier knows the catalog was imported. Set Recipients to {{ input.replyTo }} (Mailhook runs are always asynchronous, so there is no reply to the original message), give it a Subject like Catalog imported: {{ input.subject }}, and reference the number of rows created with {{ catalog.count }} in the body. Remember that external recipients must be on your organization allowlist under Settings -> General -> Email recipients.
Tips
- Before the Shopify call, add a short Condition node inside the loop that skips any row missing a
{{ item.sku }}or{{ item.title }}, so half-filled spreadsheet lines do not create empty listings. - Use
status: "draft"while you are validating the column mapping, then flip toactiveonce the output looks right. - Large catalogs can hit Shopify rate limits. Keep the loop body to a single
create-productcall, and for very large files consider splitting the spreadsheet upstream so each run stays well under the 25 MB per-run attachment limit. - Ask Miraxa, the intelligent layer across your automation, to explain a failed run: open the chat on the execution and ask "Why did my last run fail?" to pinpoint the row that broke.
Common Pitfalls
- Forgetting to decode. The Attachment node returns base64, not text. Feeding
{{ attachment.content }}straight intoparseproduces garbage; always runbase64-decodefirst. - Column drift. If a supplier renames a header (for example
qtyinstead ofquantity), the matching{{ item.* }}reference becomes empty. Agree on a fixed column layout and validate it in a Condition before the Shopify call. - Price formatting. Shopify expects price as a string. With
dynamicTypingon, prices parse as numbers, which is usually fine when mapped into the template, but watch for values with currency symbols or thousands separators in the source file. - Re-sends create duplicates. The Mailhook trigger deduplicates the exact same message, but a second email carrying the same rows will create products again. Use a unique
skuper row and decide whether tocreate-productor look up andupdate-productfor known SKUs.
Testing
Validate on a tiny scope first. Make a two-row CSV with one good row and one deliberately broken row (missing SKU), email it to your Mailhook address, and watch the run in the execution history. Confirm the Attachment node found the file, the parse output shows count: 2, the Loop ran twice, and the Condition skipped the broken row. Check that exactly one draft product appeared in your Shopify admin, then delete it. Only when the small run is clean should you point real supplier catalogs at the address and switch the product status to active.