← Back to blog

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 Webhook

Credits: 1 per PDF page or 1 per image. MIME: PDF, JPEG, PNG, WebP. Auth: Bearer API key.

Step 1 — Schema in Console

In Console → Extractors:

  1. Name the extractor (e.g. Utility bill).
  2. Document description: short phrase injected into the model prompt (residential utility bill).
  3. Root schema: type: object, properties, prefer nullable unions ["string", "null"] / ["number", "null"].
  4. Mark critical IDs x-cryvis-validation: hard; noisy OCR fields soft.
  5. 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

  1. Google Drive Trigger (or Watch) — PDF filter on inbox folder
  2. Google Drive → Download File$binary.data
  3. HTTP Request — custom extractor
  4. Set (or Code) — flatten data.* + meta.*
  5. Google Sheets → Append Row (or Update if you upsert by account number)

HTTP Request

SettingValue
URLhttps://api.cryvis.com/v1/custom-extractors/YOUR_SLUG
MethodPOST
Body Content TypeMultipart Form-Data
Header / CredentialAuthorization: Bearer sk_live_...
Field fileBinary → Input Binary Field = data
Response FormatJSON

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

ValidationStatusn8n tip
Soft warning200IF warning_count > 0 → Slack
Hard fail422Error Trigger → fix schema/doc
Bad key / no credits401 / 402Rotate 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.

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.