How to Extract Vendor Invoice Data from Emailed PDFs into NetSuite
Build a Spojit workflow that receives a vendor invoice PDF by email, reads the header and line items with a transient Knowledge query, and creates a matching vendor bill record in NetSuite.
What This Integration Does
Accounts payable teams spend hours retyping the same numbers from emailed invoice PDFs into their ERP: vendor, invoice number, date, due date, currency, line items, and totals. This workflow removes that step. A vendor emails an invoice to a dedicated Spojit address, the PDF is read and structured into clean JSON, and a vendor bill is created in NetSuite without anyone touching a keyboard. You get faster close, fewer keying errors, and a record that is consistent every time.
The workflow is triggered by a Mailhook: every email that lands at the generated address starts a run within seconds, with no mailbox or OAuth to manage. The Attachment node pulls the PDF bytes, a Knowledge node embeds that single document into a transient collection and then queries it for the fields you need, and a Connector node calls NetSuite to create the record. Each email produces one run and one NetSuite record. Runs are independent: if the same email arrives twice it is deduplicated per message, and re-sending a corrected invoice simply starts a fresh run that creates another record (use the invoice number to spot duplicates on the NetSuite side).
Prerequisites
- A NetSuite connection added under Connections - Add connection, with permission to create the record type you target (for example a vendor bill).
- The NetSuite internal IDs (or external IDs) for your vendors, or a reliable way to map a vendor name or email to a NetSuite entity. This tutorial maps by vendor name using
list-customers-style lookups against the vendor record type. - The exact NetSuite
recordTypeyou want to create (for examplevendorBill) and the field names that record expects, taken from your account's schema. - An email address or forwarding rule you control, so you can point real vendor invoices (or test PDFs) at the Mailhook address.
- Invoices that arrive as a PDF attachment under 10 MB. Image-only scans are supported because the Knowledge node can read PDFs and images through OCR.
Step 1: Receive the invoice with a Mailhook trigger
Open the Workflow Designer, add a Trigger node, and set Trigger Type to Mailhook. Set an optional Address prefix such as ap-invoices, click Generate email address, and copy the address (it looks like ap-invoices-1a2b3c4d5e6f7890@mailhook.spojit.com). Point your vendor notifications or a forwarding rule at that address.
To keep noise out, add an optional From allowlist of known vendor domains and a Subject regex such as invoice|bill. The trigger fires whether the address is in To, Cc, or Bcc. The email is available downstream as {{ input }}, including {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and {{ input.attachments }}.
Step 2: Fetch the PDF bytes with an Attachment node
Add an Attachment node. It only saves inside a Mailhook workflow, so place it right after the trigger. Configure:
- Mode:
Singleso you receive the first matching attachment as a single object. - Content type:
application/pdf. - Filename pattern:
*.pdfto ignore logos or signature images. - Fail if no attachment matches: turn this on so an email with no PDF stops the run cleanly instead of creating an empty record.
The node outputs { filename, contentType, size, content }, where content is the base64 PDF. You will feed content straight into the Knowledge node next. Attachments are capped at 10 MB each and 25 MB per run by default.
Step 3: Embed the PDF into a transient Knowledge collection
Add a Knowledge node in Embed mode. In the Collection dropdown choose Transient: a one-off collection is created for this run, shared with later nodes in the same run, and discarded automatically when the run ends. A transient collection needs no file name or embedding model, which is exactly right for a single invoice you do not want to archive.
- Document Type:
PDF(the node reads scanned, image-only invoices through OCR as well). - Document Input:
{{ attachment.content }}from the Attachment node. - Output Variable: name it
embed_resultso you can confirm the chunk count in the run logs.
If you would rather keep a searchable archive of every invoice as well, see How to Extract and Store Invoice Data in a Knowledge Collection, which uses a persistent collection instead.
Step 4: Query the invoice for structured fields
Add a second Knowledge node in Query mode, with Collection set to Transient so it reads the document embedded in Step 3. Write a precise Prompt that asks for exactly the fields you will post to NetSuite, and attach a Response Schema to force structured JSON. A schema like this keeps the output reliable:
{
"type": "object",
"properties": {
"vendorName": { "type": "string" },
"invoiceNumber": { "type": "string" },
"invoiceDate": { "type": "string" },
"dueDate": { "type": "string" },
"currency": { "type": "string" },
"subtotal": { "type": "number" },
"tax": { "type": "number" },
"total": { "type": "number" },
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unitPrice": { "type": "number" },
"amount": { "type": "number" }
}
}
}
}
}
Set Result Count to cover a multi-page invoice (8 to 10 chunks is usually enough), pick a capable Model for synthesis, and set the Output Variable to invoice. The structured result is then available as {{ invoice.total }}, {{ invoice.lineItems }}, and so on. This is the AI extraction step: it runs through Miraxa, the intelligent layer across your automation, so it costs AI credits per run.
Step 5: Match the vendor in NetSuite
Add a Connector node for NetSuite in Direct mode and select list-records to find the vendor entity whose name matches {{ invoice.vendorName }}. Set recordType to your vendor record type and pass a query that filters on the vendor name. Capture the result as vendor_lookup.
If your invoices reliably carry a stable identifier, you can instead store the vendor's external ID in the workflow and skip the lookup. Add a Condition node after the lookup to branch when no vendor is found: send the unmatched invoice to a person rather than creating a malformed record. The Send Email node in Step 7 covers that path.
Step 6: Create the vendor bill record in NetSuite
Add a Connector node for NetSuite in Direct mode and select create-record. Set recordType to your target type (for example vendorBill) and build the body from the extracted fields and the matched vendor. Map the values from {{ invoice }} and {{ vendor_lookup }} to the field names your NetSuite account expects:
{
"recordType": "vendorBill",
"body": {
"entity": { "id": "{{ vendor_lookup.items.0.id }}" },
"tranId": "{{ invoice.invoiceNumber }}",
"tranDate": "{{ invoice.invoiceDate }}",
"dueDate": "{{ invoice.dueDate }}",
"currency": { "refName": "{{ invoice.currency }}" },
"memo": "Imported from email {{ input.subject }}"
}
}
To attach individual line items, use add-sublist-item against the record returned by create-record, passing the line array from {{ invoice.lineItems }}; loop with a Loop node in ForEach mode if your line schema requires one call per line. If your account keys vendor bills by external ID and you want re-sent invoices to update in place rather than duplicate, use upsert-record with externalId set to {{ invoice.invoiceNumber }} instead of create-record.
Step 7: Confirm or escalate by email
Add a Send Email node so the run reports back. Send from Spojit's built-in mail service to your AP inbox with a subject like Invoice {{ invoice.invoiceNumber }} posted to NetSuite and a body summarising the vendor, total, and the new record. Reply to the sender by setting recipients to {{ input.replyTo }} when you want the vendor to know the invoice was received. On the no-vendor-match branch from Step 5, send the invoice details to a human for manual entry instead of posting it. Remember that external recipients must be on the org allowlist under Settings - General - Email recipients.
Tips
- Keep the Knowledge query prompt tightly scoped to the fields in your Response Schema. Asking only for what you post to NetSuite produces cleaner extraction and fewer hallucinated values.
- Use the same embedding behaviour for embed and query by leaving both Knowledge nodes on Transient; the run shares one collection so the query always sees the document you just embedded.
- Normalise dates and currency before posting. A small Transform node, or the date and math utility connectors, can reformat
{{ invoice.invoiceDate }}and round{{ invoice.total }}to the format NetSuite expects. - For multi-invoice emails, set the Attachment node to
Multipleand wrap Steps 3 to 6 in a Loop over{{ attachment.attachments }}.
Common Pitfalls
- NetSuite field names are account-specific.
create-recordrejects abodythat uses labels instead of the internal field IDs your record type defines, so confirm them withget-record-metadatabefore going live. - A missing vendor match silently produces a bad record if you skip the Condition branch. Always handle the no-match case rather than posting an invoice with an empty
entity. - Re-sent or forwarded invoices create a second record under
create-record. Switch toupsert-recordkeyed on the invoice number, or check for an existingtranIdfirst, if duplicates are a risk. - Image-only scans embed fine through OCR but extract less reliably on low-resolution faxes. Validate totals against the line-item sum and route mismatches to a person.
- Received emails are retained for 30 days and attachments are capped at 10 MB each. Larger invoice bundles need to be split before sending to the Mailhook address.
Testing
Before pointing real vendor traffic at the workflow, test against a NetSuite sandbox connection or a disposable vendor record. Email a single known invoice PDF to the Mailhook address, then open the run in execution history and inspect each step: confirm the Attachment node returned the right filename and a non-zero size, that the Knowledge query invoice output matches the PDF, and that create-record returned a record ID. Compare the posted totals against the PDF, then send a second copy to confirm your duplicate handling behaves as intended. Only when a few real invoices post cleanly should you point production vendor mail at the address.