Convert PDFs to JSON in n8n with Custom Extractors
Turn non-standard PDFs into JSON in n8n: create a Cryvis custom extractor, POST multipart $binary file, map data with Set/Code into Google Sheets or a webhook.
Standard invoices and receipts already have Cryvis built-ins. Everything else—utility bills, packing lists, inspection reports, vendor scorecards—needs a custom extractor: you define the JSON Schema once, then n8n posts the PDF to POST https://api.cryvis.com/v1/custom-extractors/:slug.
There is still no generic PDF-to-JSON dump endpoint. Custom extractors are how you get a stable JSON contract for documents Cryvis does not ship as /v1/documents/*.
Hub: n8n + Cryvis. Product: Custom extraction API. Schema reference: Custom extractor schemas.
When custom beats built-ins
Use a custom extractor when:
- The PDF is not an invoice, receipt, passport, PAN, Aadhaar, driver’s license, or Indian RC.
- You only need five fields from a dense form and want a closed schema (
additionalProperties: false). - Multiple vendors share an internal template your finance team already named.
Stay on built-ins when the document is that type—invoice HTTP is faster to map and already models sellers, line items, and totals. For KYC packs see Automate KYC.
End-to-end flow
Console: create extractor + schema
|
v
slug assigned
|
v
n8n: Watch Drive / Gmail attachment / Webhook
|
v
HTTP POST /v1/custom-extractors/<slug>
multipart field: file = $binary.data
|
v
Set / Code → data.<your keys>
|
v
Google Sheets / outbound WebhookCredits: 1 per PDF page or 1 per image. MIME: PDF, JPEG, PNG, WebP. Auth: Bearer API key.
Step 1 — Schema in Console
- Name the extractor (e.g.
Utility bill). - Document description: short phrase injected into the model prompt (
residential utility bill). - Root schema:
type: object,properties, prefer nullable unions["string", "null"]/["number", "null"]. - Mark critical IDs
x-cryvis-validation: hard; noisy OCR fieldssoft. - Save and copy the slug from the extractor detail page (path id used in the URL).
Minimal example for a utility bill:
{
"type": "object",
"properties": {
"account_number": {
"type": ["string", "null"],
"description": "Utility account or customer number",
"x-cryvis-validation": "hard"
},
"service_address": {
"type": ["string", "null"],
"description": "Service location as printed",
"x-cryvis-validation": "soft"
},
"billing_period_start": {
"type": ["string", "null"],
"format": "date",
"description": "Period start YYYY-MM-DD",
"x-cryvis-validation": "soft"
},
"billing_period_end": {
"type": ["string", "null"],
"format": "date",
"description": "Period end YYYY-MM-DD",
"x-cryvis-validation": "soft"
},
"amount_due": {
"type": ["number", "null"],
"description": "Total amount due",
"x-cryvis-validation": "soft"
},
"due_date": {
"type": ["string", "null"],
"format": "date",
"description": "Payment due date",
"x-cryvis-validation": "soft"
}
},
"required": ["account_number"],
"additionalProperties": false
}Upload a real sample in Console and inspect meta.validation.warnings before you trust n8n mappings.
Step 2 — n8n workflow: PDF → JSON → Sheets
Nodes
- Google Drive Trigger (or Watch) — PDF filter on inbox folder
- Google Drive → Download File →
$binary.data - HTTP Request — custom extractor
- Set (or Code) — flatten
data.*+meta.* - Google Sheets → Append Row (or Update if you upsert by account number)
HTTP Request
| Setting | Value |
|---|---|
| URL | https://api.cryvis.com/v1/custom-extractors/YOUR_SLUG |
| Method | POST |
| Body Content Type | Multipart Form-Data |
| Header / Credential | Authorization: Bearer sk_live_... |
Field file | Binary → Input Binary Field = data |
| Response Format | JSON |
Successful response shape
{
"success": true,
"data": {
"account_number": "48291033",
"service_address": "12 Oak St",
"billing_period_start": "2026-02-01",
"billing_period_end": "2026-02-28",
"amount_due": 94.2,
"due_date": "2026-03-15"
},
"meta": {
"document_type": "custom:YOUR_SLUG",
"request_id": "...",
"validation": {
"is_valid": true,
"warnings": [],
"confidence": 1
}
}
}Hard validation failures return 422 with error.code VALIDATION_ERROR—not a partial data object. Soft failures stay 200 with warnings in meta.validation.
Step 3 — Set flatten + Sheets
Set node:
account_number = {{ $json.data.account_number }}
service_address = {{ $json.data.service_address }}
amount_due = {{ $json.data.amount_due }}
due_date = {{ $json.data.due_date }}
is_valid = {{ $json.meta.validation.is_valid }}
request_id = {{ $json.meta.request_id }}
warning_count = {{ $json.meta.validation.warnings.length }}Use Code only when you need null-coalescing or dynamic keys. Map Sheets from Set fields (account_number, service_address, period dates, amount_due, due_date, is_valid, request_id) — not invoice field names.
Outbound webhook
Replace Sheets with HTTP Request POST JSON to your ingest URL (account_number, amount_due, request_id). Same Cryvis extract; only the sink changes.
Soft vs hard / errors
| Validation | Status | n8n tip |
|---|---|---|
| Soft warning | 200 | IF warning_count > 0 → Slack |
| Hard fail | 422 | Error Trigger → fix schema/doc |
| Bad key / no credits | 401 / 402 | Rotate credential / top up |
Custom data only has keys you declared. Error Trigger: Slack with file name (not PDF bytes). Gmail: same HTTP/Set path after filtering PDF attachments into $binary.data. Checklist: slug in URL, Console sample tested, multipart file = $binary.data, Sheets match schema.
Related reading
- Automate KYC
- Extract PAN card data
- Extract driver's license data
- Extract Indian vehicle RC
- /blog/n8n
CTA
Define a custom schema, then POST PDFs from n8n: /apis/custom. Docs: /docs/custom-extractors. Call https://api.cryvis.com/v1/custom-extractors/YOUR_SLUG with multipart file = $binary.data, flatten {{ $json.data.* }} with Set/Code, and push to Sheets or your webhook.