How to Create Shopify Orders from Emailed Wholesale Purchase Orders with a Mailhook
Give your wholesale buyers a dedicated email address, let them send purchase order PDFs to it, and have Spojit extract the line items and open a draft order in Shopify ready for your team to review.
What This Integration Does
Wholesale and B2B buyers rarely place orders through your storefront. They email a purchase order, usually as a PDF, to a sales inbox where someone re-keys every SKU and quantity into Shopify by hand. That manual step is slow and error prone, and a mistyped quantity becomes a fulfillment problem. This workflow gives you a single email address that turns any PO PDF into a structured Shopify draft order automatically, so your team reviews and confirms instead of transcribing.
The workflow starts from a Mailhook trigger: Spojit issues a unique address such as wholesale-po-a1b2c3d4e5f6g7h8@mailhook.spojit.com, and any mail sent to it pushes a run within seconds with no mailbox or inbox polling. An Attachment node fetches the PDF bytes, a Knowledge node embeds and queries that single document in a transient collection to pull out the SKUs and quantities, and a Connector node calls Shopify to create a draft order. Each inbound email runs independently, leaves one draft order behind for review, and is deduplicated per message so a forwarded copy does not create a duplicate draft. Nothing is shipped or charged automatically: the run ends with a draft a human approves inside Shopify.
Prerequisites
- A Shopify connection with permission to read products and create draft orders. See the Shopify connector article for setup.
- Optional: a Slack connection if you want a channel notification when a new draft is ready. See the Slack connector article.
- A consistent way buyers reference items: ideally the PO lists your Shopify SKU per line. If buyers use their own part numbers, keep a SKU mapping handy so you can correct lines during review.
- A sample wholesale PO PDF to test with, plus the email addresses your buyers send from if you plan to use a From allowlist.
Step 1: Add a Mailhook trigger and generate the address
Create a new workflow and set the Trigger node type to Mailhook. Set an Address prefix such as wholesale-po (1 to 24 characters), then click Generate email address. Spojit produces a unique address in the form <prefix>-<random>@mailhook.spojit.com. Copy it and share it with your wholesale buyers, or point a forwarding rule from your existing sales inbox at it.
To limit who can start runs, add a From allowlist with your buyers' sending addresses and an optional Subject regex such as (?i)purchase order|^PO[- ]. The Mailhook fires whether the address is in To, Cc, or Bcc. The trigger output is available downstream as {{ input }}, including {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and the attachment references in {{ input.attachments }}. For deeper trigger options, see Setting Up a Mailhook Trigger and Filtering Mailhook Emails.
Step 2: Fetch the PO PDF with an Attachment node
Add an Attachment node directly after the trigger. This node only saves in a workflow that has a Mailhook trigger, and it fetches the actual bytes of an attachment that the trigger referenced. Configure it to grab the PO PDF:
- Mode:
Single(most POs arrive as one PDF; the node returns the first match as an object). - Content type:
application/pdf. - Filename pattern:
*.pdfto be explicit. - Fail if no attachment matches: turn this on so an email with no PDF stops here instead of producing an empty draft order.
In Single mode the output is { filename, contentType, size, content }, where content is the base64 body you feed into the next node. Name the output variable poFile so you can reference {{ poFile.content }}. Attachments are capped at 10 MB each and 25 MB per run by default, which comfortably covers a typical PO.
Step 3: Embed the PO into a transient collection
Add a Knowledge node in Embed mode so Spojit can read the document with AI in the next step. Because you only need this one PO for the current run, pick Transient in the Collection dropdown. A transient collection is created per run, shared across nodes in the same run, and cleaned up automatically when the run finishes, so there is no file name or long-lived storage to manage. Configure:
- Collection:
Transient. - Document Type:
PDF. - Document Input:
{{ poFile.content }}(the base64 from the Attachment node). - Output Variable:
embedResult(holds the chunk count and metadata).
This splits the PO into searchable chunks so the query step can pull structured line items even from a multi-page or oddly formatted document. For background on collections, see Using Knowledge Nodes.
Step 4: Extract the line items with a Knowledge query and Response Schema
Add a second Knowledge node, this time in Query mode, with Collection set to Transient so it reads the document embedded in Step 3 within the same run. This is where Miraxa, the intelligent layer across your automation, reads the PO and returns clean structured data. Set the Prompt to describe exactly what you want pulled out:
Extract every line item from this wholesale purchase order. For each line,
return the product SKU, the quantity ordered, and the unit price if present.
Also return the buyer's PO number and the buyer company name. Ignore totals,
tax, shipping, and any commentary.
To make the output reliable for the next step, fill in the Response Schema so the result is forced into JSON instead of prose:
{
"type": "object",
"properties": {
"poNumber": { "type": "string" },
"buyer": { "type": "string" },
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "number" },
"unitPrice": { "type": "number" }
},
"required": ["sku", "quantity"]
}
}
},
"required": ["lineItems"]
}
Set the Output Variable to po. You can now reference {{ po.poNumber }}, {{ po.buyer }}, and the array {{ po.lineItems }} downstream. For more on forcing structured results, see How to Use Structured Output for Reliable AI Data Extraction.
Step 5: Create a Shopify draft order in Direct mode
Add a Connector node, choose your Shopify connection, and use Direct mode so the call is deterministic and costs no AI credits. Pick the create-order tool and map the extracted data into its inputs. Build the line items from {{ po.lineItems }}, mapping each entry's sku and quantity across, and tag the order so your team can spot wholesale POs. A typical payload looks like this:
{
"lineItems": {{ po.lineItems }},
"tags": ["wholesale", "PO:{{ po.poNumber }}"],
"note": "Auto-created from PO email from {{ input.from }}"
}
Keep this order unconfirmed so it lands as a draft your team reviews rather than a live, charged order. Map quantity from each line and let Shopify resolve the variant from the SKU. Name the output variable shopifyOrder so you can reference {{ shopifyOrder }} in the notification step. For the full input reference, see Using Connector Nodes in Direct Mode.
Step 6: Notify your team and reply to the buyer
Close the loop so a human knows a draft is waiting. Add a Connector node for Slack in Direct mode with the send-message tool, posting to a channel such as #wholesale-orders:
New wholesale draft from {{ po.buyer }} (PO {{ po.poNumber }}),
{{ input.attachments.length }} attachment(s), sent by {{ input.from }}.
Review and confirm in Shopify.
Then add a Send Email node to acknowledge the buyer, since a Mailhook never replies to the sender on its own. Set Recipients to {{ input.replyTo }}, a Subject like We received PO {{ po.poNumber }}, and a short Body confirming the order is being processed. External addresses must be on your org allowlist under Settings > General > Email recipients, so add your buyers there first.
Step 7: Add a review checkpoint for large or unusual orders (optional)
If some POs are big enough to warrant sign-off before they reach Shopify, insert a Human approval node between Step 4 and Step 5. Set a clear Label and Message (templated, e.g. Approve draft for {{ po.buyer }}, PO {{ po.poNumber }}), choose your Approval slots, and optionally set a Timeout (minutes). The workflow pauses until every slot is satisfied; approvers act in the Approvals inbox at /approvals. A rejection or timeout halts the run, so the draft order is only created when the order is approved. Wrap the Human node in a Condition on a line total threshold if you only want approval for large orders.
Tips
- Use Miraxa to scaffold this quickly. Try a prompt like
Build a workflow that watches a mailhook, fetches the PDF attachment, extracts SKUs and quantities into a Response Schema, and creates a draft Shopify order, then fine-tune each node in the properties panel. - Keep the Knowledge query Result Count high enough to cover multi-page POs so no line items are missed during synthesis.
- If buyers send several PDFs in one email, switch the Attachment node to
Multiplemode and wrap the embed and query in a Loop over{{ poFile.attachments }}. - Always use the same embedding model for embed and query within a collection; with a transient collection both steps share one run, so just leave the defaults consistent.
Common Pitfalls
- Buyer part numbers are not your Shopify SKUs. If
create-ordercannot resolve a variant, review the draft and correct the SKU, or maintain a mapping the query prompt can normalize against. - Forgetting to turn on Fail if no attachment matches lets a body-only email reach Shopify and create an empty draft. Turn it on.
- Received emails and their attachments are retained for 30 days, and each attachment is capped at 10 MB by default. Very large scanned POs may need to be split or compressed before sending.
- Letting the order go live instead of staying a draft means it could be charged or fulfilled before anyone checks the SKUs. Keep it unconfirmed and review in Shopify.
- The Send Email node only resolves upstream variables, so build your acknowledgement body from
{{ po }}and{{ input }}values, not workflow name or status.
Testing
Before sharing the address with buyers, email a single sample PO PDF to your generated Mailhook address from an allowlisted account. Watch the run in execution history and confirm each step: the Attachment node returned poFile with a non-zero size, the Knowledge query populated {{ po.lineItems }} with the right SKUs and quantities, and the Shopify create-order call produced a draft you can open in your store. Compare the draft line for line against the PDF, then test the edge cases: an email with no attachment (should stop at the Attachment node), a forwarded duplicate (should be deduplicated), and a PO with a SKU you do not stock (should surface clearly during review). Once a few real POs come through cleanly, hand the address to your buyers.