Documentation
PDI (Physical-Digital Interface) turns printed pages into a two-way interface to your application's state. You print structured documents, people mark them up with pens, and PDI returns the changes as events.
Quickstart
1 — Get an API key. Keys are provisioned per application (contact the platform admin, or use the admin endpoint if you operate this deployment). Keys look like pdi_live_… and are sent as a bearer token.
2 — Create a document from your records:
curl -X POST https://buildpdi.com/api/v1/documents \
-H "Authorization: Bearer $PDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "task-list",
"title": "Tasks — Thursday",
"items": [
{ "ref": "task_481", "text": "Submit travel voucher" },
{ "ref": "task_482", "text": "Finish reading" }
],
"blank_lines": 6
}'The response includes document.id (e.g. doc_8F31A2C4) and aprint_url. Open the print URL in a browser and print it — the sheet carries a QR marker identifying this exact printed instance.
3 — Scan it back. After someone writes on the sheet, photograph the whole page and post it:
curl -X POST https://buildpdi.com/api/v1/captures \
-H "Authorization: Bearer $PDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "image": "<base64 JPEG>", "mime_type": "image/jpeg" }'The QR in the photo identifies the document; you get back only what changed:
{
"capture": { "id": "cap_5A2E11D904" },
"document_id": "doc_8F31A2C4",
"events": [
{ "type": "field.checked", "field": { "record_ref": "task_482", "label": "Finish reading", ... }, "confidence": 0.99 },
{ "type": "field.added", "field": { "label": "Buy printer ink", "idx": 3, ... }, "confidence": 0.97 }
],
"state": { ...full current document state... }
}Apply the events in your own vocabulary: field.checked with arecord_ref means that record of yours was ticked on paper;field.added means a handwritten line entered the world and should become a new record. Scanning an unchanged sheet returns zero events — that is Differential PDI Sync, and it means the same physical sheet can be scanned every day for a week.
API reference
Base URL https://buildpdi.com/api/v1 · auth header Authorization: Bearer pdi_live_… · all bodies JSON.
POST /documents
| Field | Type | Notes |
|---|---|---|
template | string | "task-list" (more templates coming) |
title | string | Printed as the sheet heading |
items | array | { ref?, text, checked? } — ref is your record id, echoed back in events. Max 40. |
blank_lines | int | Ruled write-in lines below the items (0–15, default 6) |
meta | object | Anything; stored and returned as-is |
Returns 201 with { document, print_url }.
GET /documents · GET /documents/:id
List your documents, or fetch one with its full field state.
POST /captures
| Field | Type | Notes |
|---|---|---|
image | string | Base64 photo/scan of the whole sheet (JPEG or PNG, downscaled to ≤ ~1600px is plenty) |
mime_type | string | image/jpeg (default) or image/png |
document_id | string | Optional fallback used when the QR is not readable in the photo |
Optional header x-pdi-vision-key: an Anthropic API key to use as the vision model for this one request (bring-your-own-model). It is used once and never stored.
Events
| Type | Meaning |
|---|---|
field.checked | A checkbox that was empty at last sync now carries a mark. For printed items, field.record_ref is the ref you supplied. |
field.added | A blank write-in line now carries handwriting; field.label is the transcription. If its box is also ticked, field.checked is true. |
Sync rules
- Checks are one-way latches: paper can check a field, but a blank box never un-checks one that is already checked digitally. A completion is never lost to a stale sheet.
- Observations below 0.6 model confidence are ignored rather than guessed at — retake the photo.
- Re-reads of already-synced handwriting do not update the stored text, so transcription jitter never masquerades as edits.
SDK
A dependency-free JavaScript client for Node 18+ and browsers: buildpdi.com/sdk/pdi.js. Copy it into your project (or fetch it at build time) and:
const { PDI } = require("./pdi.js");
const pdi = new PDI({ apiKey: process.env.PDI_API_KEY });
const { print_url } = await pdi.documents.create({
template: "task-list",
title: "Prep list",
items: rows.map(r => ({ ref: r.id, text: r.name })),
});
const sync = await pdi.captures.process({ file: photoFile }); // browser
// or: await pdi.captures.process({ imageBase64, mimeType: "image/jpeg" });Keep the API key server-side. The recommended browser pattern is a thin proxy route on your own backend that adds the Authorization header.