← Back to blog

Invoice OCR to Postgres (or MySQL) with n8n

n8n strength vs spreadsheet zaps: Drive → Cryvis Invoice API → IF success → Postgres/MySQL INSERT with typed columns.

What we're building

Spreadsheets are fine for pilots. Finance systems want typed rows in Postgres or MySQL. n8n ships native Postgres and MySQL nodes — you extract with Cryvis, then INSERT without bouncing through Airtable as a middle database.

Google Drive Trigger
        |
        v
Download → $binary.data
        |
        v
HTTP Request
  POST https://api.cryvis.com/v1/documents/invoice
  Form-Data: file = data
        |
        v
IF {{ $json.success }} equals true
        |
        v
Postgres (or MySQL): Insert
  invoice_number, dates, seller, totals, source_file

Compared to a Zapier → Airtable path: one less SaaS copy of vendor tax IDs, SQL constraints, and joins to your ERP staging tables. Sheets version of the same extract: Extract invoice data. Hub: /blog/n8n.

What you need

  • n8n with Postgres or MySQL credentials (Cloud or self-hosted; self-hosted often sits on the same VPC as the DB)
  • Drive folder for inbound invoices
  • Cryvis API key — Invoice API · extractInvoice
  • Table DDL below (or adapt)

Credits: 1 per page/image.

Suggested schema (Postgres)

CREATE TABLE invoices (
  id              BIGSERIAL PRIMARY KEY,
  invoice_number  TEXT,
  invoice_date    DATE,
  due_date        DATE,
  currency        TEXT,
  seller_name     TEXT,
  seller_tax_id   TEXT,
  buyer_name      TEXT,
  subtotal        NUMERIC(14, 2),
  tax_total       NUMERIC(14, 2),
  total_amount    NUMERIC(14, 2),
  amount_due      NUMERIC(14, 2),
  source_file     TEXT,
  drive_file_id   TEXT,
  extracted_at    TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE (seller_name, invoice_number)
);

MySQL: same columns; use AUTO_INCREMENT and DATETIME(6) as you prefer. The unique key stops vendor resends from double-inserting.

Workflow nodes

  1. Google Drive Trigger (or Schedule + list new files)
  2. Google Drive — Download → $binary.data
  3. IF — MIME / extension allowlist (optional but recommended)
  4. HTTP Request — Cryvis
  5. IF{{ $json.success }} equals true
  6. Postgres / MySQL — Insert (or Execute Query with parameterized INSERT)
  7. Optional: Google Drive — Move to Processed; false branch → error log table

Cryvis HTTP Request

SettingValue
MethodPOST
URLhttps://api.cryvis.com/v1/documents/invoice
AuthAuthorization: Bearer <API_KEY> (Header Auth credential)
Body Content TypeForm-Data
ParameterName file, Type File, Input Data Field Name data

Full multipart notes: HTTP Request OCR setup.

Map expressions into INSERT

ColumnExpression
invoice_number{{ $json.data.invoice_number }}
invoice_date{{ $json.data.invoice_date }}
due_date{{ $json.data.due_date }}
currency{{ $json.data.currency }}
seller_name{{ $json.data.seller.name }}
seller_tax_id{{ $json.data.seller.tax_id }}
buyer_name{{ $json.data.buyer.name }}
subtotal{{ $json.data.subtotal }}
tax_total{{ $json.data.tax_total }}
total_amount{{ $json.data.total_amount }}
amount_due{{ $json.data.amount_due }}
source_fileDrive file name from download node
drive_file_idDrive file id from trigger

Use the Postgres node’s column mapping UI, or Execute Query:

INSERT INTO invoices (
  invoice_number, invoice_date, due_date, currency,
  seller_name, seller_tax_id, buyer_name,
  subtotal, tax_total, total_amount, amount_due,
  source_file, drive_file_id
) VALUES (
  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
)
ON CONFLICT (seller_name, invoice_number) DO NOTHING;

Wire query parameters from the same {{ $json.data.* }} expressions. Prefer ON CONFLICT DO NOTHING (or MySQL INSERT IGNORE / ON DUPLICATE KEY) over silent duplicate finance rows.

Null and type pitfalls

  • Dates may be null if OCR cannot read them — allow NULL columns or route to an invoices_review table via IF {{ $json.data.invoice_number }} is empty.
  • Numbers from Cryvis are JSON numbers — map to NUMERIC, not text with currency symbols.
  • seller.tax_id is often null; that is valid.

Error handling

HTTP Request
  retry on fail: 2–3 (5xx only in practice)
        |
        +-- IF success false → INSERT into invoice_errors (file, status, body snippet)
        |
Error Trigger workflow → Slack: workflow + node + message (no full PII dump)

Self-hosted: put the DB credential on a restricted network path; do not expose Postgres to the public internet just for n8n Cloud if policy forbids it — run n8n in the same private network instead.

Test

  1. Insert one fixture PDF into Drive.
  2. Run workflow; confirm one row in invoices.
  3. Re-drop the same file (or re-run) — unique constraint / ON CONFLICT should block a second row.
  4. Upload a PNG logo — MIME IF should stop before HTTP (no wasted credits).
  5. Spot-check amount_due and currency against the PDF.

Why not Sheets here

Sheets is fine for ops review. Databases win when you need:

  • Unique constraints on (seller, invoice_number)
  • Joins to purchase orders / vendors
  • Downstream dbt or ERP loaders reading SQL
  • Retention and access control that match your security review

Keep a parallel Sheets append only if humans need a live queue — or query Postgres from a BI tool.

Production checklist

  • Unique key on seller + invoice number
  • Move Drive file after successful INSERT
  • Error table + Error Trigger alert
  • Credit budget for multi-page statements — Credits
  • Credential rotation for Cryvis Bearer and DB user

Next steps

Gmail → Sheets + Slack triage: Automate invoice processing. Building-block HTTP: HTTP Request OCR.

Sign up · Pricing · Invoice · extractInvoice · /blog/n8n.