Automate Invoice Processing from Gmail with n8n
n8n: Gmail trigger → IF MIME → Cryvis Invoice HTTP Request → Google Sheets + Slack. AP intake with success and error branches.
What we're building
Vendors email invoices. n8n watches Gmail, keeps only PDF/image attachments, extracts fields with Cryvis, appends a Sheets AP row, and posts a Slack summary. Ops leaves the inbox.
Gmail Trigger (label: Invoices)
|
v
IF / Switch: attachment MIME pdf|jpeg|png|webp
|
v
HTTP Request
POST https://api.cryvis.com/v1/documents/invoice
Form-Data: file ← attachment binary
|
v
IF {{ $json.success }} equals true
|
+-- true → Google Sheets Append
| → Slack: vendor + total + invoice #
|
+-- false → Slack error / error sheetDrive + Sheets only: Extract invoice data. DB insert: Invoice to database. Hub: /blog/n8n.
What you need
- n8n + Gmail OAuth (mailbox that receives vendor invoices)
- Gmail label
Invoices(filter rule applies it) - Google Sheet AP Inbox (columns below)
- Slack workspace + bot/credential for a channel
- Cryvis API key — Invoice API
Credits: 1 per PDF page or image.
Create the workflow
- Gmail Trigger — new email with label
Invoices(or poll unread) - Gmail — Get attachment / download (binary on the item)
- IF or Switch — MIME / filename allowlist
- HTTP Request — Cryvis invoice
- IF —
{{ $json.success }} - True: Google Sheets Append + Slack message
- False: Slack alert or Sheets Errors tab
- Optional: Gmail Mark as read / add label
Processed
If one email has multiple attachments, use a loop (Split Out / Item Lists) so each binary hits Cryvis once. Filter out logos and calendar junk before the loop burns credits.
Configure Gmail source
Trigger
- Label:
Invoicesonly — never “all mail” - Prefer unread + mark processed to avoid double runs
Attachment binary
- Confirm
binaryPropertyName(oftendataorattachment_0depending on node version) - HTTP Request Form-Data Input Data Field Name must match that property exactly
IF / Switch before Cryvis
mimeType is application/pdf
OR image/jpeg OR image/png OR image/webp
AND file size > 0Or filename ends with .pdf, .jpg, .jpeg, .png, .webp. Drop signatures and .eml wrappers.
Configure Cryvis HTTP Request
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://api.cryvis.com/v1/documents/invoice |
| Auth | Header Authorization: Bearer <API_KEY> |
| Body Content Type | Form-Data |
| Body | Name file, Type File, Input Data Field Name = attachment binary property |
Docs: extractInvoice. Multipart variants: HTTP Request OCR.
Phone-photo invoices work when the attachment is jpeg/png/webp. Inline images that are not attachments never reach this node — ask vendors to attach files.
When an email has invoice PDF + packing list PDF, the loop runs Cryvis per file. Add an IF on filename containing inv / invoice if vendors are consistent and you want to skip T&Cs PDFs.
Sheets mapping
Suggested headers:
Invoice Number | Invoice Date | Due Date | Currency | Vendor | Buyer | Subtotal | Tax | Total | Amount Due | Status | From | Subject | Attachment | Received At
| Column | Source |
|---|---|
| Invoice Number | {{ $json.data.invoice_number }} |
| Invoice Date | {{ $json.data.invoice_date }} |
| Due Date | {{ $json.data.due_date }} |
| Currency | {{ $json.data.currency }} |
| Vendor | {{ $json.data.seller.name }} |
| Buyer | {{ $json.data.buyer.name }} |
| Subtotal | {{ $json.data.subtotal }} |
| Tax | {{ $json.data.tax_total }} |
| Total | {{ $json.data.total_amount }} |
| Amount Due | {{ $json.data.amount_due }} |
| Status | static Received |
| From / Subject | Gmail fields from trigger |
| Attachment | attachment fileName |
| Received At | email date or {{ $now }} |
Keep line_items out unless you Split Out into a second sheet later.
Slack messages
Success (true branch):
Invoice extracted: {{ $json.data.invoice_number }}
Vendor: {{ $json.data.seller.name }}
Amount due: {{ $json.data.currency }} {{ $json.data.amount_due }}
From: {{ $json.from }} (map from Gmail node)Failure (false branch or Error Trigger):
Cryvis invoice failed
Subject: …
Attachment: …
success: {{ $json.success }}Do not dump full data with tax IDs into a public Slack channel.
Optional IF after success: if {{ $json.data.amount_due }} > threshold, post to #ap-high-value instead of (or in addition to) the default channel — Switch node with two Slack nodes.
Errors and dedupe
- Retry on Fail on HTTP Request for transient 5xx.
- Error Trigger workflow for hard failures (auth, node crash).
- Dedupe: before Sheets, IF a lookup finds the same
seller.name+invoice_numberalready in the sheet/DB → skip Append (vendors resend PDFs). - Missing
invoice_number: still Append, Status =In Review, checkbox/columnNeeds Manual Number.
Test
- Label one message with a known PDF as
Invoices. - Run once — Sheets Status
Received, Slack summary matches PDF. - Send an email with only a PNG logo — MIME IF stops before HTTP.
- Resend the same invoice — dedupe blocks the second row.
- Spot-check
amount_due,due_date,seller.tax_id(blank OK if PDF omitted it).
If HTTP succeeds but Sheets rejects a number cell, you mapped a formatted string — use Cryvis numbers directly.
Production considerations
- Dedicated label only.
- Sheets view filtered to Status =
Received, sorted by Due Date. - Multi-page statements burn one credit per page — Credits.
- Self-hosted: Gmail credential + execution binary retention; prune KYC/finance histories per policy.
- After Approve in Sheets, a second workflow can push Postgres/ERP — keep extraction and posting separate.
Next steps
Drive → Sheets: Extract invoice data. Drive → Postgres: Invoice to database.
Sign up · Pricing · Invoice · extractInvoice · /blog/n8n.