How to Load Emailed Partner CSV Feeds into MySQL with a Mailhook
Partner CSV feed emails land on a Spojit mailhook, the rows are validated, then the clean rows are bulk-inserted into a MySQL table, with no manual download or import step.
What This Integration Does
Many suppliers, distributors, and channel partners still deliver data the same way: a scheduled email with a CSV attached (a price list, a stock feed, a daily sales extract). Someone on your team downloads the attachment, opens it, eyeballs it, and pastes or imports it into a database. This tutorial replaces that chore. You give each partner a dedicated Spojit mailhook address, and every email they send to it starts a run that parses the attached CSV, drops malformed rows, and writes the good rows into a MySQL table you control.
The run model is push-based and asynchronous. The moment a partner emails the mailhook address, Spojit starts a run within seconds, with no mailbox or inbox polling involved. The Attachment node pulls the CSV bytes, the CSV connector turns them into rows, a Loop validates each row, and the MySQL connector inserts the survivors. Each message runs once (Spojit deduplicates per message), so re-sending the same email does not double-insert as long as your run completes. The workflow leaves new rows in your MySQL table and sends no reply to the sender unless you add a Send Email step yourself.
Prerequisites
- A MySQL connection added under Connections -> Add connection -> MySQL, with credentials that can
INSERTinto your target database. - A destination table that already exists in MySQL (this workflow inserts rows, it does not create the table). Know its column names and types.
- The exact column layout of the partner CSV: header names, order, and which fields are required.
- The partner's sending address (or addresses), so you can set a From allowlist on the mailhook.
- Permission in the workspace to create workflows and add mailhook triggers. The CSV connector is a built-in utility and needs no connection.
Step 1: Create the workflow and add a Mailhook trigger
Create a new workflow, then on the trigger node set Trigger Type to Mailhook. Optionally set an Address prefix (1 to 24 characters, default mh) such as acme-feed so the address is recognisable, then click Generate email address. Spojit produces a unique address of the form acme-feed-<random16>@mailhook.spojit.com. Copy it and give it to the partner (or set up a forwarding rule that points their existing feed at it).
Add a From allowlist with the partner's sending address so unrelated mail cannot start runs, and optionally a Subject regex like ^Daily Feed if the partner uses a consistent subject. The trigger output is available downstream as {{ input }}, including {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and {{ input.attachments }} (each attachment reference is { id, filename, contentType }).
Step 2: Fetch the CSV with an Attachment node
Add an Attachment node. This node only works in mailhook workflows and fetches the actual bytes of an attachment the trigger referenced. Set Mode to Single so you get the first matching file as a single object, and set a Filename pattern glob such as *.csv (or something tighter like feed-*.csv) so a partner signature image or PDF cover sheet is ignored. Optionally set a Content type filter of text/csv. Turn on Fail if no attachment matches so a feed email that arrives without its CSV stops the run loudly instead of inserting nothing.
The node outputs { filename, contentType, size, content }, where content is the file encoded as base64. Note the output variable name (this tutorial uses attachment). Keep partner feeds under the per-attachment limit of 10 MB.
Step 3: Parse the CSV into rows
Add a Connector node in Direct mode, choose the CSV connector, and select the parse tool. Feed the file content into its input. Because {{ attachment.content }} is base64, decode it first if your CSV text is needed as plain text: add an Connector node using the Encoding connector with the base64-decode tool, mapping {{ attachment.content }} as the input, then pass that decoded text into parse.
The parse tool turns the CSV text into an array of row objects keyed by the header row, for example:
[
{ "sku": "AB-100", "name": "Widget", "qty": "40", "price": "12.50" },
{ "sku": "AB-101", "name": "Gadget", "qty": "", "price": "9.99" }
]
Store the parsed result in an output variable such as parsed so later steps can read {{ parsed.rows }} (or the equivalent array field the tool returns). If your partner sends extra leading junk lines or a non-standard delimiter, use the CSV connector's slice tool to drop header lines, or info to inspect the file before parsing.
Step 4: Loop over rows and validate each one
Add a Loop node in ForEach mode over the parsed array (for example {{ parsed.rows }}). Inside the loop body, add a Condition node that checks the row is usable before it reaches the database. For example, require a SKU and a numeric quantity:
{{ row.sku }} is not empty
AND {{ row.qty }} matches a number
For stricter checks, add a Connector node with the Validation connector inside the loop: use numeric against {{ row.qty }} or {{ row.price }}, or is-empty against {{ row.sku }}. Route only the rows that pass down the true branch. Rows that fail fall out of the loop iteration and are simply not inserted. If you want to keep a record of rejects, collect them into a variable and email them in Step 7.
Step 5: Insert the valid rows into MySQL
You have two clean options for the write, depending on how large a feed is.
Per-row insert (simple). On the true branch inside the loop, add a Connector node in Direct mode, choose the MySQL connector, and select the insert-rows tool. Set table to your destination table and map rows to a single-element array built from the current row, mapping CSV columns to your real column names:
{
"table": "partner_stock",
"rows": [
{
"sku": "{{ row.sku }}",
"product_name": "{{ row.name }}",
"quantity": "{{ row.qty }}",
"unit_price": "{{ row.price }}",
"partner": "{{ input.from }}"
}
]
}
Bulk insert (faster). Instead of inserting inside the loop, use the loop or a Transform node to collect every passing row into one array, then call insert-rows once after the loop with the whole array in rows. A single bulk insert is far quicker and gentler on the database than one call per row, which matters for feeds of thousands of rows.
If you need upsert behaviour (update existing SKUs rather than duplicating them), use the execute-query tool with an INSERT ... ON DUPLICATE KEY UPDATE statement and ? placeholders bound to the row values rather than insert-rows.
Step 6: Add an idempotency guard (optional but recommended)
Partners sometimes resend the same daily feed, or a forwarding rule fires twice. To avoid duplicate rows even across separate runs, give your table a unique constraint on the natural key (for example sku plus a feed date column) and use the execute-query upsert pattern from Step 5. Alternatively, record {{ input.messageId }} in a small log table and skip the run early with a Condition node if you have already processed that message. Spojit already deduplicates per message within its retention window, so this guard mainly protects against re-sent emails with new message IDs.
Step 7: Send a confirmation back to the partner
Add a Send Email node after the insert step. Because mailhook runs never reply automatically, use this node to acknowledge receipt. Set Recipients to {{ input.replyTo }} (the sender's reply address from the trigger), a templated Subject like Feed received: {{ input.subject }}, and a Body summarising how many rows were inserted and how many were skipped. External recipients must be on your org allowlist under Settings -> General -> Email recipients, and these emails count toward your monthly email allowance. To reply from your own domain instead of Spojit's built-in mail service, use the Resend or SMTP connector in a Connector node with its send-email tool.
Tips
- Prefer a single bulk
insert-rowscall over one call per row for large feeds: it is faster and uses far fewer connector calls. - Map CSV header names to MySQL column names explicitly in the payload. Partners rename headers without warning, so an explicit map fails clearly instead of silently writing wrong columns.
- Cast numeric and date fields in MySQL (or normalise them with the Date and Math connectors first). CSV values arrive as strings.
- Ask Miraxa, the intelligent layer across your automation, to scaffold the skeleton: try "Build a workflow that watches a mailhook, fetches the CSV attachment, loops over the rows, and bulk-inserts them into the MySQL table partner_stock." Then fine-tune each node in the properties panel.
Common Pitfalls
- Attachment node without a mailhook. The designer refuses to save an Attachment node unless the trigger is a Mailhook. Set the trigger first.
- Base64 content passed raw into parse. The Attachment node returns
contentas base64. Decode it with the Encoding connector'sbase64-decodetool before parsing, or the CSV will not read. - Schema drift. A partner adds or reorders columns and your inserts start failing or shifting values. Validate required headers early (use the CSV connector's
infotool or a Condition check) and alert on mismatch. - Wide-open mailhook. Without a From allowlist, anyone who learns the address can push rows into your database. Always set the allowlist, and use Regenerate address to rotate if an address leaks (the old address dies instantly).
- Attachment size limits. Defaults are 10 MB per attachment and 25 MB per run. Very large feeds should be split by the partner or delivered another way.
Testing
Before pointing the real partner at the mailhook, send a tiny test CSV (three or four rows, including one deliberately bad row) from an address on your From allowlist to the generated mailhook address. Open the run in execution history and check each node: confirm the Attachment node fetched the file, the CSV parse output has the right row count, the Loop and Condition dropped the bad row, and the MySQL insert-rows call reported the expected number of inserted rows. Query the table directly to confirm the data landed in the right columns. Once a small feed inserts cleanly, raise the volume and enable the workflow for live partner traffic.