How to Turn Inbound Vendor Emails into NetSuite Records
Build a Spojit workflow that catches vendor notification emails on a Mailhook, reads the PDF attachment with AI, and upserts a matching NetSuite record without any manual data entry.
What This Integration Does
Vendors send notifications as emails with a PDF attached: a purchase order acknowledgement, a shipment notice, a remittance advice. Someone normally opens each one, retypes the reference number and amounts into NetSuite, and forwards a copy to the team. This workflow does all three steps automatically. The moment a vendor email lands on your Mailhook address, Spojit pulls the PDF, extracts the fields you care about with an AI query, writes them into NetSuite as a record, and posts a short confirmation to Slack so the team can see it happened.
The run model is push-based and async. A Mailhook trigger gives you a dedicated address; any mail sent there starts a run within seconds, with no mailbox or polling involved. The full email is available as {{ input }}, including attachment references. An Attachment node fetches the PDF bytes, a Knowledge node embeds that single document into a transient collection and queries it for structured fields, and a NetSuite Connector node upserts the record keyed by the vendor reference. Because the upsert is keyed by an external ID, re-runs of the same email update the existing record instead of creating duplicates. Each inbound message is deduplicated, so a vendor resending the same notification will not create a second run.
Prerequisites
- A NetSuite connection with permission to create and update the record type you are targeting (for example a vendor bill or custom record). Test it first with the
verify-connectiontool. - A Slack connection authorized to post to your notifications channel, plus the channel ID or name you want to post to.
- A decision on which NetSuite record type and external ID you will key on. The external ID must come from a field that appears in the vendor PDF (for example the PO number or invoice number) so re-runs stay idempotent.
- A sample vendor PDF on hand for testing, and the field names NetSuite expects in the record body.
Step 1: Catch the vendor email with a Mailhook trigger
Add a Trigger node and set Trigger Type to Mailhook. Set an Address prefix such as vendor, then click Generate email address to get a unique address like vendor-3f8a1c9b2d4e6f70@mailhook.spojit.com. Copy it and point your vendor notifications at it: either give the address to the vendor directly, or add a forwarding rule in your mailbox so matching vendor mail is forwarded there.
To keep the workflow focused, add a From allowlist with your vendor domains and an optional Subject regex such as (?i)purchase order|remittance|shipment. Once the trigger fires, the email is available downstream as {{ input }}, including {{ input.subject }}, {{ input.from }}, {{ input.replyTo }}, and the attachment references in {{ input.attachments }}.
Step 2: Fetch the PDF with an Attachment node
Add an Attachment node directly after the trigger. The designer only allows this node when a Mailhook trigger is present. Set Mode to Single so you get the first matching attachment as an object, set Content type to application/pdf, and set the Filename pattern to *.pdf. Turn on Fail if no attachment matches so an email with no PDF stops cleanly rather than running the rest of the workflow on empty data.
The node outputs { filename, contentType, size, content }, where content is the base64-encoded PDF. Reference it later as {{ attachment.content }}. Keep in mind the default limits: 10 MB per attachment and 25 MB per run.
Step 3: Embed the PDF into a transient collection
Add a Knowledge node in Embed mode. In the Collection dropdown choose Transient: a transient collection is auto-created for this single run, shared with the next Knowledge node in the same run, and cleaned up automatically when the run finishes, which is exactly what you want for a one-off invoice extraction. Transient mode needs no file name and no embedding model.
Set Document Type to PDF and set Document Input to the attachment bytes:
{{ attachment.content }}
Give it an Output Variable such as embedResult so you can confirm the chunk count in the run logs if you need to debug.
Step 4: Extract structured fields with a Knowledge query
Add a second Knowledge node in Query mode and set its Collection to Transient as well, so it reads the document embedded in Step 3. Write a clear Prompt describing the fields to pull:
Extract the following from this vendor document:
the vendor name, the vendor reference or PO number,
the document date, the currency, the total amount,
and each line item with its description, quantity,
and unit price. If a field is missing, return null.
To get reliable, machine-usable output, set a Response Schema so the result is forced into a fixed JSON shape:
{
"type": "object",
"properties": {
"vendorName": { "type": "string" },
"reference": { "type": "string" },
"documentDate": { "type": "string" },
"currency": { "type": "string" },
"totalAmount": { "type": "number" },
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unitPrice": { "type": "number" }
}
}
}
},
"required": ["reference", "totalAmount"]
}
Set the Output Variable to extracted. Downstream you can now read clean values such as {{ extracted.reference }} and {{ extracted.totalAmount }} instead of parsing free text.
Step 5: Upsert the NetSuite record
Add a Connector node in Direct mode, select your NetSuite connection, and choose the upsert-record tool. This tool creates the record if the external ID is new and updates it if it already exists, which keeps the workflow idempotent across resends.
Map the three inputs. Set recordType to the NetSuite record type you decided on in the prerequisites (for example vendorBill). Set externalId to the extracted vendor reference so it stays stable across re-runs:
{{ extracted.reference }}
Set body to the record payload, mapping the extracted fields to the NetSuite fields your account expects:
{
"entity": "{{ extracted.vendorName }}",
"tranDate": "{{ extracted.documentDate }}",
"currency": "{{ extracted.currency }}",
"memo": "Imported from vendor email {{ input.subject }}",
"total": {{ extracted.totalAmount }}
}
Give the node an output variable such as nsResult so the next step can reference the record. If your line items need to be written into a NetSuite sublist, add a Loop node over {{ extracted.lineItems }} after this step and call the NetSuite add-sublist-item tool inside the loop.
Step 6: Post a confirmation to Slack
Add a final Connector node in Direct mode, select your Slack connection, and choose the send-message tool. Set the channel and a short message that confirms what was written:
Vendor record upserted in NetSuite.
Vendor: {{ extracted.vendorName }}
Reference: {{ extracted.reference }}
Total: {{ extracted.currency }} {{ extracted.totalAmount }}
From email: {{ input.from }}
If you would rather acknowledge the vendor directly, add a Send Email node instead and set its recipient to {{ input.replyTo }}. Mailhook runs are always async and send no automatic reply, so a Send Email node is the only way to write back to the sender.
Tips
- Keep both Knowledge nodes on Transient. Embedding a one-off PDF into a persistent collection would leave it in your archive forever; transient collections discard themselves at the end of the run.
- Always key the upsert on a value that is unique per document, such as the PO or invoice number. If two different documents could share a reference, combine fields (for example
{{ extracted.reference }}-{{ extracted.documentDate }}) to keep external IDs unique. - Use the Subject regex and From allowlist on the Mailhook to drop newsletters and out-of-office replies before they ever reach the AI query, which saves credits.
- Need to scaffold this faster? Ask Miraxa, the intelligent layer across your automation, with a prompt like "Build a workflow that watches a mailhook, extracts the PDF invoice, and upserts a NetSuite record," then fine-tune each node in the properties panel.
Common Pitfalls
- Embedding model mismatch. Transient collections avoid this for you, but if you ever switch to a persistent collection, always embed and query with the same embedding model or the query returns nothing.
- No PDF on the email. With Fail if no attachment matches turned off, a body-only email flows downstream with empty content and produces a junk record. Turn the option on so those runs stop at Step 2.
- Oversized attachments. PDFs over the 10 MB per-attachment or 25 MB per-run defaults will not be fetched. Ask vendors to send slimmer files or split large statements.
- NetSuite field names. The
bodyyou send must use the exact field IDs your NetSuite record type expects. Confirm them against a real record before going live, or the upsert will reject the payload.
Testing
Before pointing real vendor traffic at the workflow, send one sample email with a known PDF to your Mailhook address from an allowlisted sender. Open the run in the execution history and check each step: the Attachment node should show the right filename and a non-zero size, the Knowledge query should return your structured extracted object, and the NetSuite step should report a created or updated record. Then resend the exact same email and confirm the second run updates the same record instead of making a duplicate. Once that round-trip looks right, widen the From allowlist to all your vendors and enable the workflow.