PDF extraction
Extract invoice data from Gmail PDFs to Google Sheets with n8n
Build an n8n workflow that turns Gmail invoice PDFs into structured Google Sheets rows with okraPDF: upload, parse with a JSON schema, poll, append.
You can turn Gmail invoice PDFs into structured Google Sheets rows with a single n8n workflow and the okraPDF API — no manual data entry and no community node to install. Invoices and bank statements arrive as email attachments; instead of retyping vendor, invoice number, dates, and totals, this workflow sends each attachment to okraPDF, gets the fields back already typed, and appends a row to your sheet.
It uses built-in n8n nodes only — Gmail Trigger, IF, HTTP Request, Wait, and Google Sheets — so it imports on n8n Cloud or self-hosted n8n with no community node to install. The same shape works for the general PDF-into-Sheets pipeline; here we focus on the invoice-extraction case.
What you’ll build
Gmail Trigger (PDF attachment)
→ IF attachment is a PDF
→ POST /v1/files (store the binary, get a file id)
→ POST /v1/parse (parse it with a JSON schema)
→ Wait → GET /v1/jobs/{id} (poll until the job is terminal)
→ Google Sheets: append row (vendor, invoice #, dates, totals)
The key idea: instead of OCR-then-regex, you hand okraPDF a JSON schema describing the fields you want, and it returns them already typed and labeled. That’s the difference between parsing and pattern-matching — a new invoice layout doesn’t break a schema the way it breaks a regex.
Prerequisites
- An okraPDF API key from okrapdf.com/settings/keys. Structured extraction must be enabled on the account.
- Gmail and Google Sheets connected in n8n.
- n8n Cloud or a self-hosted instance.
Create one HTTP Header Auth credential in n8n for okraPDF:
- Name:
okraPDF API Key - Header Name:
Authorization - Header Value:
Bearer okra_YOUR_KEY_HERE
Every okraPDF HTTP Request node below uses Authentication: Generic Credential Type → Generic Auth Type: HTTP Header Auth. Attaching the credential is not enough on its own — that pairing is what makes n8n actually send the header.
Step 1 — Gmail Trigger
Add a Gmail Trigger and configure it to hand the attachment to the next node as binary:
- Simplify: off — you want the full message and its attachments.
- Download Attachments: on.
- Attachment Prefix:
attachment_.
The first attachment is now available as the binary field attachment_0.
Step 2 — Keep only PDFs
Add an IF node so non-PDF attachments are dropped:
{{ ($binary.attachment_0?.mimeType || '').includes('pdf')
|| ($binary.attachment_0?.fileName || '').toLowerCase().endsWith('.pdf') }}
Wire the true branch onward; leave the false branch empty to ignore non-PDFs.
Step 3 — Upload the PDF to okraPDF
Add an HTTP Request node, Upload to okraPDF:
- Method:
POST - URL:
https://api.okrapdf.com/v1/files - Authentication: HTTP Header Auth → your
okraPDF API Keycredential - Body: Form-Data (multipart). Add one parameter:
- Type:
n8n Binary File - Name:
file - Input Data Field Name:
attachment_0
- Type:
The response has a top-level id. That’s the reusable file id you parse next.
Step 4 — Parse with a JSON schema
Add an HTTP Request node, Parse invoice fields:
- Method:
POST - URL:
https://api.okrapdf.com/v1/parse - Authentication: the same okraPDF credential
- Body: JSON. Toggle the body field to expression mode (in n8n it then starts with
=) so{{ $json.id }}resolves to the uploaded file’s id — pasted as plain JSON the placeholder is sent literally and/v1/parse404s on the bogus id:
{
"parser": "gemini-vision",
"file": { "id": "{{ $json.id }}" },
"pages": "1-3",
"schema": {
"type": "object",
"properties": {
"vendor": { "type": "string", "description": "Supplier / biller name" },
"invoice_number": { "type": "string" },
"invoice_date": { "type": "string", "description": "ISO date if possible" },
"due_date": { "type": "string" },
"currency": { "type": "string" },
"subtotal": { "type": "string" },
"tax": { "type": "string" },
"total": { "type": "string" }
},
"required": ["vendor", "invoice_number", "total"]
}
}
Edit the properties to match your own fields. Sending a schema is what switches okraPDF from plain parsing to structured extraction.
/v1/parse is async: it returns 202 with a status_url pointing at /v1/jobs/{id}. You poll that next.
Step 5 — Poll the job
Add a Wait node, then an HTTP Request node, Get job result:
- Method:
GET - URL:
{{ ($json.status_url && $json.status_url.startsWith('http'))
? $json.status_url
: 'https://api.okrapdf.com/v1/jobs/' + ($json.id || $json.job_id) }}
That guard matters. On the first poll the 202 body carries an absolute status_url, but on each subsequent GET /v1/jobs/{id} the response returns a relative status_url (/v1/jobs/{id}). Feeding a relative URL straight into the HTTP Request node breaks the second poll — so only reuse status_url when it starts with http, otherwise rebuild it from the job id.
Then an IF node to decide whether the job is done:
{{ ['succeeded','completed','completed_with_errors','failed','canceled','cancelled']
.includes(($json.status || $json.internal_status || '').toLowerCase()) }}
Include completed_with_errors — it is a terminal status. Leave it out and a parse that finishes with partial page errors loops forever. Route the “not done yet” branch back to the Wait node; pace it with:
{{ Math.max(1, Math.ceil(($json.next_poll_after_ms ?? 15000) / 1000)) }}
Step 6 — Append the row to Google Sheets
Add a Google Sheets node, Append row, pointed at your invoice log. The extracted fields live under result.extracted.data:
| Column | Value |
|---|---|
| Vendor | ={{ $json.result?.extracted?.data?.vendor }} |
| Invoice # | ={{ $json.result?.extracted?.data?.invoice_number }} |
| Invoice date | ={{ $json.result?.extracted?.data?.invoice_date }} |
| Due date | ={{ $json.result?.extracted?.data?.due_date }} |
| Currency | ={{ $json.result?.extracted?.data?.currency }} |
| Total | ={{ $json.result?.extracted?.data?.total }} |
| Schema valid | ={{ $json.result?.extracted?.schema_valid }} |
| Pages | ={{ $json.result?.usage?.pages }} |
| Job ID | `={{ $json.id |
schema_valid tells you whether the model returned every field your schema required — a cheap quality gate before you trust a row. The parsed text itself, if you also want it, is at result.formats.text.content and result.formats.markdown.content.
Production notes
- Use n8n environment credentials, not a hardcoded key. Scope the API key to this workflow rather than a shared admin key.
- Add idempotency. n8n retries and Gmail can redeliver. Key rows on the file id or invoice number and upsert instead of blind-appending.
- Extraction has to be enabled on the okraPDF account, or
/v1/parseparses but returns noresult.extracted. - Watch
schema_valid. Routefalserows to a review tab instead of your clean ledger.
That’s the whole loop: Gmail in, structured fields out, one row per invoice. The same workflow handles bank statement PDFs — swap the schema for the fields you need. For the webhook-driven variant — where okraPDF calls n8n when a hosted PDF is ready instead of n8n polling — see automating PDF workflows with n8n webhooks. To send invoices in from a shared mailbox rather than your own, see the Gmail PDF email intake pattern.
FAQ
Can n8n extract data from PDF invoices?
Yes. n8n has no native PDF-extraction node, but its built-in HTTP Request node can call the okraPDF API: POST /v1/parse with a JSON schema returns invoice fields already typed and labeled. No community node is required, so the workflow runs on n8n Cloud or self-hosted n8n.
How do I get invoice fields from a PDF into Google Sheets?
Upload the PDF to POST /v1/files, parse it with POST /v1/parse and a JSON schema, poll GET /v1/jobs/{id} until the job is terminal, then map result.extracted.data.* to columns in a Google Sheets Append row node.
Do I need OCR for scanned invoices?
No separate step. okraPDF’s gemini-vision parser reads scanned and image-only PDFs, so the same workflow handles both digital and scanned invoices.
Why use a JSON schema instead of regex?
A schema describes the fields you want and survives layout changes; a regex breaks the moment a vendor moves a total or relabels a field. okraPDF also returns schema_valid, so you can route incomplete extractions to a review tab instead of trusting them blindly.
Ready to wire it up? Grab a key at okrapdf.com and start with a single test invoice.