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

FieldTypeNotes
templatestring"task-list" (more templates coming)
titlestringPrinted as the sheet heading
itemsarray{ ref?, text, checked? }ref is your record id, echoed back in events. Max 40.
blank_linesintRuled write-in lines below the items (0–15, default 6)
metaobjectAnything; 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

FieldTypeNotes
imagestringBase64 photo/scan of the whole sheet (JPEG or PNG, downscaled to ≤ ~1600px is plenty)
mime_typestringimage/jpeg (default) or image/png
document_idstringOptional 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

TypeMeaning
field.checkedA checkbox that was empty at last sync now carries a mark. For printed items, field.record_ref is the ref you supplied.
field.addedA blank write-in line now carries handwriting; field.label is the transcription. If its box is also ticked, field.checked is true.

Sync rules

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.