AI Template Generation
Tag: AI · Version: v1 · Stability: 🟠 Experimental (v0)
Generate an editable template draft from a reference image (or PDF) and/or a text description. The model returns a structured document of bands; the platform loads it into the designer as a fresh draft you refine and save — so AI is just another template author, never a new render path.
These endpoints are feature-flagged (Ai:Enabled) and their behaviour and
response shape may change without a major-version bump. Prototype with it; don't
build production dependencies on its output yet. Generations are metered under
the ai source.
The free tier allows 25 AI generations per month per workspace. Exceeding
the quota returns 402 Payment Required. The count resets on the 1st of
each calendar month.
Generate a template (streamed)
POST /api/ai/generate-band-template · JWT · multipart/form-data
This is the current path. The server drives two model passes and streams progress as Server-Sent Events: pass 1 authors the document's skeleton (its bands, data schema, paper, direction and fonts), then pass 2 fills one band at a time. Your client only reads the stream and loads the result.
| Form field | Type | Notes |
|---|---|---|
image | file | Optional reference. An image (max 8 MB) or a PDF (max 25 MB) — for a PDF, page 1 is rasterised server-side and used as the reference. |
prompt | string | Optional description, e.g. "like this, but a blue accent + add a discount row". |
html / css | string | Optional HTML (and separate CSS) to convert into a template. |
paper | JSON string | Optional paper, in the template's pageSize shape, e.g. {"id":"letter"}. Omit for automatic. |
At least one of image, html or prompt is required (otherwise 400). The image
carries the layout in one shot; the prompt refines it — the combination is the most
powerful input.
curl -N -X POST https://api.paperwright.dev/api/ai/generate-band-template \
-H "Authorization: Bearer <jwt>" \
-F "prompt=A two-page invoice with a logo top-left and a totals table" \
-F 'paper={"id":"letter"}' \
-F "image=@reference.png"
Anything that can fail with a real status code — bad input, an unreadable PDF, an
exceeded quota (402) — does so before the stream opens. After that the status
is already 200 and a problem arrives as an error event.
Stream events
The SSE event name is the stage:
| Event | When | Payload fields |
|---|---|---|
skeleton | Pass 1 finished. | bandsTotal |
band | One band was filled. | bandsTotal, bandsCompleted, bandId, failed |
done | Everything assembled. | template, and an optional message |
error | The run failed. | message |
event: skeleton
data: {"stage":"skeleton","bandsTotal":3}
event: band
data: {"stage":"band","bandsTotal":3,"bandsCompleted":1,"bandId":"b1","failed":false}
event: done
data: {"stage":"done","bandsTotal":3,"bandsCompleted":3,"template":{ … }}
done.template carries name, bands, schema, direction, bodyFont,
headingFont and pageSize.
-
An
imageelement may carrysrc: adata:URL cut from your reference (a logo or photo the model located in it). Each picture is at most about 150 KB and a page at most about 600 KB. -
When the request had a reference,
done.checkTokenis a one-time token (15 minutes) for a free self-check: send it as thecheckTokenfield ofrevise-documentwith the same reference and the step compares the draft with the reference (anyinstructionis ignored) and is not metered. -
Bands arrive flat. Each band has an
idand an optionalparentId(a nested detail band in a master-detail layout); rebuild the tree on your side. -
A band can fail alone. A band that fails twice comes back empty with
failed: truerather than failing the document. -
One document, one credit. Both passes together are metered as a single generation. A document may have at most 20 bands.
How the page is chosen
The document's paper is settled before any band is filled, first match wins:
- the
paperform field; - the page size of a reference PDF (matched to a preset, else a custom size);
- the model's pick from your request (
pageSizein the result, any of the 22 presets).
An invalid value falls back to A4. All coordinates in the result are final for the chosen page, so load it as is rather than re-fitting it.
Direction and fonts
Pass 1 picks the document direction (ltr or rtl) from the language the
document is written in, and lays out right-to-left documents from the right. It
also picks a bodyFont and a headingFont from the designer's 18 fonts, and text
and tables come back carrying them.
Refine one section
POST /api/ai/refine-band · JWT · application/json
Change one band without regenerating the document. Send the live document (so the
model sees any hand edits), the band's bandId, and the change in your own words:
{
"document": { "name": "Invoice", "bands": [ … ], "schema": [ … ], "direction": "ltr" },
"bandId": "b2",
"instruction": "Add a discount row under the subtotal"
}
{
"bandId": "b2",
"elements": [ … ],
"newFields": [ … ]
}
Replace that band's elements with elements. newFields lists any schema field the
change referenced that the document did not already have — add them to the schema,
or the new token prints its own braces. A refine is one plain JSON call (no
stream), is not metered as a generation, and is still subject to the daily
limit and your plan's entitlement.
Revise a document (streamed)
POST /api/ai/revise-document · JWT · multipart/form-data
One step over the whole document: by an instruction, by visual comparison with a reference, or both. The server renders the current draft, gives the model the render and the reference side by side, and streams the same events as generation.
| Form field | Type | Notes |
|---|---|---|
document | JSON string | Required. The live document in the AI wire shape (as for refine), with element ids, so changed elements keep their identity. Max 1 MB. |
content | JSON string | Required. The serialized template, used to render the current draft. Max 25 MB. |
sampleData | JSON string | Optional data to render the draft with. Max 1 MB. |
instruction | string | Optional, max 2000 characters. |
image | file | Optional reference, as for generation (image, or a PDF whose page 1 is used). |
At least one of instruction or image is required. Paper and direction stay as the
document has them.
- Pass 1 decides which bands change (
changed+ notes per band); pass 2 fills only those. Unchanged bands come back exactly as sent. - A band whose fill fails keeps its current elements and is reported as
failed; if every fill fails the stream ends witherror. donecarriestemplate(same shape as generation, elementids echoed) andunchanged: truewhen there was nothing to change.- Metering: one generation credit per step that changes something. Nothing to
change (including fills that come back exactly as sent), a step that fails, and a step
with a valid
checkTokenare not metered. The quota (402) and entitlement are checked before the stream opens; the endpoint is rate-limited like rendering.
Generate a template (legacy, single pass)
POST /api/ai/generate-template · JWT
The original single-pass endpoint. It takes the same optional image and prompt and returns a suggested name and a flat element list. It cannot produce page headers, several repeating bands or nested master-detail layouts, which is why new work uses the streamed endpoint above.
curl -X POST https://api.paperwright.dev/api/ai/generate-template \
-H "Authorization: Bearer <jwt>" \
-F "prompt=A modern invoice with a logo top-left and a totals table" \
-F "image=@reference.png"
Honest expectations
Image → layout is a draft, not a pixel-perfect clone. The model nails structure and placement; coordinates, fonts, and colours come out approximate. Treat it as "80% of the layout in seconds — finish in the designer."
Data handling
The image (or PDF page) and/or prompt are sent to the model provider (Anthropic) for generation. Fine for typical use; a no-retention / on-prem path is planned for high-compliance tiers.