← Back to blog

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 sheet

Drive + 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

  1. Gmail Trigger — new email with label Invoices (or poll unread)
  2. Gmail — Get attachment / download (binary on the item)
  3. IF or Switch — MIME / filename allowlist
  4. HTTP Request — Cryvis invoice
  5. IF{{ $json.success }}
  6. True: Google Sheets Append + Slack message
  7. False: Slack alert or Sheets Errors tab
  8. 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: Invoices only — never “all mail”
  • Prefer unread + mark processed to avoid double runs

Attachment binary

  • Confirm binaryPropertyName (often data or attachment_0 depending 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 > 0

Or filename ends with .pdf, .jpg, .jpeg, .png, .webp. Drop signatures and .eml wrappers.

Configure Cryvis HTTP Request

SettingValue
MethodPOST
URLhttps://api.cryvis.com/v1/documents/invoice
AuthHeader Authorization: Bearer <API_KEY>
Body Content TypeForm-Data
BodyName 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

ColumnSource
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 }}
Statusstatic Received
From / SubjectGmail fields from trigger
Attachmentattachment fileName
Received Atemail 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

  1. Retry on Fail on HTTP Request for transient 5xx.
  2. Error Trigger workflow for hard failures (auth, node crash).
  3. Dedupe: before Sheets, IF a lookup finds the same seller.name + invoice_number already in the sheet/DB → skip Append (vendors resend PDFs).
  4. Missing invoice_number: still Append, Status = In Review, checkbox/column Needs Manual Number.

Test

  1. Label one message with a known PDF as Invoices.
  2. Run once — Sheets Status Received, Slack summary matches PDF.
  3. Send an email with only a PNG logo — MIME IF stops before HTTP.
  4. Resend the same invoice — dedupe blocks the second row.
  5. 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.