PDF in. Clean JSON out.
One endpoint that reads any business document — invoices, contracts, registration forms — and returns structured, semantic JSON. No templates to configure, no fields to map.
Every request carries your organisation's API key in the x-api-key header. Your organisation has one key; owners and admins can view (masked) and rotate it any time at app.blueport.io → PDF2JSON → API key. Rotation invalidates the old key immediately — treat the key as a secret, keep it server-side, never ship it in a browser or mobile app.
An optional x-model header (or ?model=) picks the extraction engine: hkm — the default — is the fast, economical engine; snm is the premium engine for documents where maximum field accuracy matters (legal contracts, financial statements). The engine used is echoed back in meta.model.
Send the PDF either way — or import the ready-made
Postman collection
(File → Import → paste the URL, then set the apiKey variable):
Raw binary — simplest, no encoding step:
curl -X POST https://pdf.blueport.io/pdf2json \ -H "x-api-key: YOUR_API_KEY" \ -H "content-type: application/pdf" \ --data-binary @invoice.pdf
JSON body — when the PDF is already in memory:
// Node.js const pdf = fs.readFileSync("invoice.pdf") .toString("base64"); const r = await fetch( "https://pdf.blueport.io/pdf2json", { method: "POST", headers: { "x-api-key": process.env.PDF2JSON_KEY, "content-type": "application/json", }, body: JSON.stringify({ pdf }), }); const { ok, data } = await r.json();
Salesforce Apex — e.g. converting an invoice attached to a record. Add https://pdf.blueport.io as a Remote Site (Setup → Remote Site Settings) and store the key in a protected Custom Setting or Named Credential, never in code:
// Callout limit is 120s - matches the extraction worst case.
public class Pdf2JsonService {
public static Map<String, Object> extract(Blob pdfFile) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://pdf.blueport.io/pdf2json');
req.setMethod('POST');
req.setHeader('x-api-key', Pdf2Json_Settings__c.getOrgDefaults().Api_Key__c);
req.setHeader('content-type', 'application/json');
req.setTimeout(120000);
req.setBody(JSON.serialize(new Map<String, String>{
'pdf' => EncodingUtil.base64Encode(pdfFile)
}));
HttpResponse res = new Http().send(req);
Map<String, Object> body =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
if (res.getStatusCode() != 200 || body.get('ok') != true) {
throw new CalloutException('PDF2JSON failed: ' + body.get('error')
+ ' (request_id ' + body.get('request_id') + ')');
}
return (Map<String, Object>) body.get('data');
}
}
// Usage - from a ContentVersion on the record, in a @future/queueable
// context (synchronous Apex should not hold a 10-25s callout):
ContentVersion cv = [SELECT VersionData FROM ContentVersion WHERE Id = :cvId];
Map<String, Object> doc = Pdf2JsonService.extract(cv.VersionData);
System.debug(doc.get('documentType'));
The data object is the document, understood: a documentType, then logical entity groups with camelCase keys, ISO dates, and real JSON numbers for money. The shape follows the document — an invoice yields dealer/customer/vehicle/pricing groups, a registration form yields its own sections.
{
"ok": true,
"data": {
"documentType": "TAX INVOICE",
"pages": 2,
"currency": "AUD",
"dealer": { "name": "Example Motors Pty Ltd", "abn": "12 345 678 901", … },
"invoice": { "invoiceNo": "10023456", "date": "2026-08-01", … },
"vehicle": { "vin": "6XY12345678900123", "odometer": 1, … },
"pricing": { … }
},
"meta": { "request_id": "req_a1b2c3", "duration_ms": 17000,
"model": "hkm", "pages": 2, "billed_units": 1 }
}
meta.request_id in any support conversation — it identifies the exact request in our logs (the document itself is never stored).| Status | Meaning | What to do |
|---|---|---|
| 401 | Missing or invalid API key | Check the header name and value; confirm the key wasn't rotated |
| 400 | Body isn't a PDF / no PDF found | Send raw with content-type: application/pdf or JSON {"pdf": "<base64>"} |
| 413 | PDF over 8 MB | Compress or split the document |
| 422 | Extraction declined | The document was rejected by content safety — contact support with the request_id |
| 502 | Extraction returned unusable output | Rare and transient — retry once |
| 500 | Service error | Retry with backoff; persistent failures → support |
• Latency: extraction is synchronous and typically 10–25 seconds per document — set your HTTP client timeout to 120 s and call it from a background job, not a user-facing request.
• Metering: a document includes up to 5 pages — longer files count one document per started block of 5 (a 20-page file counts as 4 documents), and premium-engine (snm) extractions count double. The exact charge is returned as meta.billed_units; failed calls are never billed.
• Retries: safe to retry on 5xx; only successful calls count against your plan.
• Usage: your document count and history are on your dashboard at app.blueport.io; you'll be notified as you approach your monthly allowance.
• Privacy: documents are processed in-flight and never persisted — only usage counts and timings are recorded.