Docs menu

API reference

Every /v1 endpoint, grouped by tag, with its parameters, schemas and a curl example.

View this page as Markdown

Before you start

This page is generated from the API's OpenAPI document. Every example reads your key from $FENFILL_API_KEY; base URL, authentication, errors and limits are on the API overview. The live reference is at api.fenfill.com/v1/docs, and the OpenAPI document at api.fenfill.com/v1/openapi.json.

Forms

Analyze a blank PDF form into a fillable schema, or download a template as a fillable PDF.

Analyze a blank PDF form

POST/v1/forms/analyze

Auth: Bearer API key

Queue a blank PDF for analysis. Poll Location (GET /v1/jobs/{job_id}) until it is done. A PDF with native form fields costs nothing (unless detect_extra_blanks adds a vision pass); otherwise one page scan per analyzed page is reserved now and refunded if the job fails.

NameInTypeRequiredDescription
fileformfileyesThe blank PDF form, at most your plan's max_file_mb.
nameformstringnoA name for the form. Defaults to the file name.
pagesformstringno1-based pages to analyze, e.g. "1-3,5". Omit for every page. One AI job covers at most max_pages_per_job pages (GET /v1/account): split a longer document into several jobs. If the chosen pages carry native form fields, the PDF's native fields are read instead (every page up to max_pages, with no per-job cap), at no cost.
save_as_templateformbooleannotrue: save the result as a template in the workspace. false: keep it only as this job's result (24 h, or 1 h after the first fetch). Default false.
accessform"public" | "restricted"noThe saved template's access; only with save_as_template=true. public (the default): anyone with the template's link can fill it. restricted: workspace members and per-recipient links only. Recipient links (POST /v1/templates/{id}/recipients) need a restricted template.
detect_extra_blanksformbooleannoOnly for a PDF with native form fields (ignored otherwise: AI analysis already finds every blank). true: after reading the native fields, a vision pass over the chosen pages (at most max_pages_per_job) adds blanks that have no native field, such as a signature or date line; native fields win where they overlap. Costs one page scan per page (the job is then reported as mode ai). false: uses no page scans; the 202 says whether such blanks seem likely (extra_blanks_available). Default false.

Request body: multipart/form-data.

Returns 202 with AnalyzeAccepted:

FieldTypeRequiredDescription
job_idstringyes–
statusstringyes–
mode"ai" | "acroform"yesacroform: the PDF has native form fields, read using no page scans. ai: page scans are reserved: analyzed with AI, or an AcroForm PDF analyzed with detect_extra_blanks=true (its native fields are still read using no page scans; the scans pay for the vision pass over extra_blanks_pages).
pagesarray of integeryesThe 1-based pages the job covers, sorted.
costintegeryesPage scans reserved for this job (0 for acroform).
extra_blanksboolean | nullnoOnly on an AcroForm PDF analyzed with detect_extra_blanks=true: always true. The native fields are read using no page scans, then a vision pass over extra_blanks_pages adds blanks that have no native field (native fields win where they overlap). Costs one scan per page in extra_blanks_pages.
extra_blanks_pagesarray of integer | nullnoWith extra_blanks: the 1-based pages the vision pass covers (what cost pays for).
extra_blanks_availableboolean | nullnoOnly on an AcroForm job that uses no page scans: true when the chosen pages show blanks no native field covers (underscore runs, label-and-line blanks, signature/date captions). A heuristic hint that uses no page scans: analyze again with detect_extra_blanks=true to find them.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
402insufficient_scans (with need, have)
403plan_required / template_frozen
404not_found
408invalid_request (the upload was too slow)
409template_limit
413file_too_large (with max_bytes)
422not_pdf / pdf_password_required / pdf_fill_forbidden / filled_output / too_many_pages / invalid_request
429rate_limited / daily_limit / too_many_active_jobs / queue_full (with Retry-After)
503unavailable
Terminal
curl -X POST "https://api.fenfill.com/v1/forms/analyze" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -F "file=@blank-form.pdf" \  -F "save_as_template=true"

Download a template as a fillable PDF

POST/v1/forms/acroform

Auth: Bearer API key

A saved template's PDF with a fillable field on every blank, for any PDF reader. The fields are empty: this returns the blank form only and takes no values (fill it on your machine). Needs the Team plan or above. Shares the PDF download limits of GET /templates/{template_id}/pdf: past either, 429 with Retry-After.

NameInTypeRequiredDescription
template_idbodystringyesThe id of a template in this workspace.

Request body: application/json (AcroformExportRequest).

Returns 200 with application/pdf: The blank form with fillable fields, as an attachment.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422pdf_password_required / pdf_fill_forbidden / invalid_schema / template_needs_migration / invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
# Needs the Team plan or above. Saves the blank form with fillable fields; no values are sent.curl -X POST "https://api.fenfill.com/v1/forms/acroform" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -H "Content-Type: application/json" \  -d '{"template_id": "<template id>"}' \  -o blank-form.pdf

Jobs

Poll an analyze job and fetch its result.

Get an analyze job's status and result

GET/v1/jobs/{job_id}

Auth: Bearer API key

Status while queued/running, with a polling hint: Retry-After: 10 while queued, 3 while running (so three jobs polled as told stay under the 60/min key limit). Once done, result is the agent schema. An unsaved result is kept 24 hours after the job finishes and 1 hour after its first fetch, then 410 (as is a saved one whose template was deleted).

NameInTypeRequiredDescription
job_idpathstringyes–
includequerystring | nullnorender adds the raw v5 schema (page images blanked) once done.

Returns 200 with JobStatus:

FieldTypeRequiredDescription
job_idstringyes–
status"queued" | "running" | "done" | "error"yes–
mode"ai" | "acroform"yesacroform: the PDF has native form fields, read using no page scans. ai: page scans are reserved: analyzed with AI, or an AcroForm PDF analyzed with detect_extra_blanks=true (its native fields are still read using no page scans; the scans pay for the vision pass over extra_blanks_pages).
phasestring | nullyes–
progressintegeryes–
pagesarray of integeryes–
costintegeryes–
created_atstring (date-time)yes–
template_idstring | nullyesSet when the job saved a template (save_as_template, or a template re-analysis).
errorJobError | nullyes–
resultAgentSchema | nullyesThe agent schema, once the job is done.
renderobject | nullnoOnly with ?include=render on a done job: the raw v5 schema, page images blanked.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403template_frozen (the saved template)
404not_found
410result_expired
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/jobs/$JOB_ID" \  -H "Authorization: Bearer $FENFILL_API_KEY"

Templates

The workspace's templates: their agent schemas and blank PDFs.

List the workspace's templates

GET/v1/templates

Auth: Bearer API key

Active templates, newest first. Frozen templates and unfinished drafts (still awaiting the owner's AI/manual setup choice) are not listed.

NameInTypeRequiredDescription
limitqueryintegernoPage size (1–100). Default 50.
cursorquerystring | nullnonext_cursor from the previous page.

Returns 200 with TemplateList:

FieldTypeRequiredDescription
templatesarray of TemplateSummaryyes–
next_cursorstring | nullyesPass as ?cursor= for the next page; null at the end.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/templates" \  -H "Authorization: Bearer $FENFILL_API_KEY"

Get a template's agent schema

GET/v1/templates/{template_id}

Auth: Bearer API key

The fillable inputs of a template, in reading order, as agent-schema v1. updated_at is the version to pass back when correcting it (PATCH).

NameInTypeRequiredDescription
template_idpathstringyes–
includequerystring | nullnorender adds the raw v5 schema (page images blanked).

Returns 200 with AgentSchema:

FieldTypeRequiredDescription
schema_versionintegeryes–
template_idstring | nullyes–
namestring | nullyes–
updated_atstring | nullnoOnly on a saved template: when its schema last changed. Pass it back unchanged as expected_updated_at to PATCH /v1/templates/{id}.
page_countintegeryes–
pagesarray of AgentPageyes–
fieldsarray of ScalarEntry | SignatureEntry | CombEntry | ChoiceEntry | TableEntry | TableRowsEntryyes–
renderobject | nullnoOnly with ?include=render: the raw v5 template schema, page images blanked.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/templates/$TEMPLATE_ID" \  -H "Authorization: Bearer $FENFILL_API_KEY"

Correct a template's layout

PATCH/v1/templates/{template_id}

Auth: Bearer API key

Save corrections to a template's fields, groups and sections: send the whole edited v5 schema (as ?include=render returns it) with the updated_at it was based on. Only layout and the form's own wording are stored (labels, descriptions, placeholders, options), never fill values: an unknown or value-bearing key is refused. Page images, sizes and the page count stay as they are. If the template changed meanwhile, 409 template_conflict with current_updated_at: fetch it again and re-apply. 422 template_needs_migration when the stored template carries data from an older version that can't be saved back unchanged: make that edit in the web editor.

NameInTypeRequiredDescription
template_idpathstringyes–
schemabodyV5TemplateSchemayesThe full v5 template schema, as GET /v1/templates/{id}?include=render returns it, edited. Taken from it: fields, groups and each page's sections. Everything else (the page list, page images and sizes) stays the server's; the page count and version (5) must match.
expected_updated_atbodystring (date-time)yesThe template's updated_at the edit is based on (from GET). A newer one is 409.

Request body: application/json (TemplateUpdate).

Returns 200 with TemplateUpdated:

FieldTypeRequiredDescription
template_idstringyes–
updated_atstring (date-time)yes–
resultAgentSchemayes–

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
409template_conflict (with current_updated_at) / job_in_progress (a re-analysis is running)
413request_too_large (with max_bytes)
422invalid_schema (with details: [{path, reason}]) / invalid_request / template_needs_migration (the stored template holds data from an older version that the API can't save back; edit it in the web editor)
429rate_limited (with Retry-After): too many updates, or too many in progress
503unavailable
Terminal
# corrected-template.json: {"schema": <the edited v5 schema from ?include=render>, "expected_updated_at": "<updated_at from GET>"}curl -X PATCH "https://api.fenfill.com/v1/templates/$TEMPLATE_ID" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -H "Content-Type: application/json" \  -d @corrected-template.json

Download the blank template PDF

GET/v1/templates/{template_id}/pdf

Auth: Bearer API key

The template's blank PDF (decrypted server-side), for filling locally. Rate-limited per key per minute, and to a few at once across the service: past either, 429 with Retry-After.

NameInTypeRequiredDescription
template_idpathstringyes–

Returns 200 with application/pdf: The blank template PDF.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/templates/$TEMPLATE_ID/pdf" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -o blank-form.pdf

Re-analyze pages of a saved template

POST/v1/templates/{template_id}/analyze

Auth: Bearer API key

Run AI again on chosen pages of a saved template, using the PDF already stored with it (nothing is uploaded). Analyze pages never analyzed (pages[].analyzed false on GET) with scratch; find adds missed blanks; relabel / label_missing fix labels. Poll Location (GET /v1/jobs/{id}) until done: the result is the updated template. One page scan per page is reserved now and refunded if the job fails (label_missing uses the monthly included label pages first). With estimate: true the price is returned (200) and nothing starts.

NameInTypeRequiredDescription
template_idpathstringyes–
pagesbodystringyesThe 1-based pages to analyze, e.g. "2" or "1-3,5".
kindbody"scratch" | "find" | "relabel" | "label_missing" | nullnoscratch: analyze the pages from nothing, replacing their fields, groups and sections (for a page never analyzed, or one whose layout is wrong). find: add blanks the first pass missed, keeping everything there (deleted items stay deleted). relabel: re-derive labels, types, groups and sections, keeping the geometry. label_missing: fill in only missing labels (uses the monthly included label pages, then one scan per page). The others cost one scan per page. Omitted: scratch, allowed only when every chosen page has no fields.
expected_updated_atbodystring | nullnoThe template's updated_at from GET. Required unless estimate is true; a newer one is 409 template_conflict.
estimatebodybooleannotrue: price the run (200 RerunEstimate) and start nothing. Default false.

Request body: application/json (TemplateAnalyze).

Returns 200 with RerunEstimate (With estimate=true: the price; nothing is started.):

FieldTypeRequiredDescription
kind"scratch" | "find" | "relabel" | "label_missing"yes–
pagesarray of integeryesThe 1-based pages the run would cover, sorted.
costintegeryesPage scans the run would charge now.
free_label_pagesintegeryesHow many of pages the monthly included label pages would cover (label_missing only).
free_label_pages_leftintegeryesIncluded label pages left this month after the run.
scans_leftintegeryesThe workspace's page scans left now.
insufficientbooleanyestrue: the run would be refused with 402 insufficient_scans.

Returns 202 with AnalyzeAccepted:

FieldTypeRequiredDescription
job_idstringyes–
statusstringyes–
mode"ai" | "acroform"yesacroform: the PDF has native form fields, read using no page scans. ai: page scans are reserved: analyzed with AI, or an AcroForm PDF analyzed with detect_extra_blanks=true (its native fields are still read using no page scans; the scans pay for the vision pass over extra_blanks_pages).
pagesarray of integeryesThe 1-based pages the job covers, sorted.
costintegeryesPage scans reserved for this job (0 for acroform).
extra_blanksboolean | nullnoOnly on an AcroForm PDF analyzed with detect_extra_blanks=true: always true. The native fields are read using no page scans, then a vision pass over extra_blanks_pages adds blanks that have no native field (native fields win where they overlap). Costs one scan per page in extra_blanks_pages.
extra_blanks_pagesarray of integer | nullnoWith extra_blanks: the 1-based pages the vision pass covers (what cost pays for).
extra_blanks_availableboolean | nullnoOnly on an AcroForm job that uses no page scans: true when the chosen pages show blanks no native field covers (underscore runs, label-and-line blanks, signature/date captions). A heuristic hint that uses no page scans: analyze again with detect_extra_blanks=true to find them.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
402insufficient_scans (with need, have)
403plan_required / template_frozen
404not_found
409template_conflict (with current_updated_at) / job_in_progress
422invalid_request / too_many_pages / kind_required
429rate_limited / daily_limit / too_many_active_jobs / queue_full (with Retry-After)
503unavailable
Terminal
curl -X POST "https://api.fenfill.com/v1/templates/$TEMPLATE_ID/analyze" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -H "Content-Type: application/json" \  -d '{"pages": "2", "kind": "find", "expected_updated_at": "<updated_at from GET>"}'

Recipients

Per-recipient magic links on restricted templates.

Create a per-recipient link

POST/v1/templates/{template_id}/recipients

Auth: Bearer API key

A magic link for one recipient of a restricted template. The link's status moves pending → opened → completed as the recipient fills (no answers are ever sent back).

NameInTypeRequiredDescription
template_idpathstringyes–
labelbodystringyesWho the link is for, as the sender will see it (e.g. 'Anna K.').
expires_atbodystring (date-time) | nullnoWhen the link stops working (ISO 8601, UTC if no offset). Omit for no expiry.

Request body: application/json (RecipientCreate).

Returns 201 with Recipient:

FieldTypeRequiredDescription
idstringyes–
labelstring | nullyes–
urlstringyes–
status"pending" | "opened" | "completed" | "sent_back"yespending → opened → completed. sent_back is reserved (not yet emitted).
opened_atstring (date-time) | nullyes–
completed_atstring (date-time) | nullyes–
created_atstring (date-time)yes–
expires_atstring (date-time) | nullyes–

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
409template_not_restricted / template_frozen
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl -X POST "https://api.fenfill.com/v1/templates/$TEMPLATE_ID/recipients" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -H "Content-Type: application/json" \  -d '{"label": "Anna K."}'

List a template's recipient links

GET/v1/templates/{template_id}/recipients

Auth: Bearer API key

The template's live (not revoked) recipient links, newest first, at most 500. Revoking happens in the fenfill app.

NameInTypeRequiredDescription
template_idpathstringyes–

Returns 200 with RecipientList:

FieldTypeRequiredDescription
recipientsarray of Recipientyes–

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/templates/$TEMPLATE_ID/recipients" \  -H "Authorization: Bearer $FENFILL_API_KEY"

Account

The workspace behind the API key: plan, limits and branding.

The workspace behind this key

GET/v1/account

Auth: Bearer API key

Plan, remaining page scans, limits and branding flags. scans_remaining is the balance at the time of this request. max_pages is how far into a document the plan reads; max_pages_per_job is how many pages one AI analyze job may cover (split a longer document into several jobs with pages; a PDF with native form fields is read whole, up to max_pages).

No parameters.

Returns 200 with Account:

FieldTypeRequiredDescription
workspace_idstringyes–
tierstringyes–
scans_remaininginteger | nullyes–
max_pagesinteger | nullyesHow far into a document the plan reads (its first N pages).
max_pages_per_jobintegeryesThe most pages one AI analyze job may cover; split a longer document with pages. A PDF with native form fields is read whole (up to max_pages), with no per-job cap.
max_file_mbinteger | nullyes–
brandingBrandingFlagsyes–

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/account" \  -H "Authorization: Bearer $FENFILL_API_KEY"

The workspace's branding logo

GET/v1/account/logo

Auth: Bearer API key

The logo to stamp on filled PDFs; 404 when the plan has no custom branding or no logo is uploaded.

No parameters.

Returns 200 with image/png or image/jpeg: The logo image.

Errors (codes):

StatusCode
401invalid_api_key / key_revoked
403plan_required / template_frozen
404not_found
422invalid_request
429rate_limited (with Retry-After)
503unavailable
Terminal
curl "https://api.fenfill.com/v1/account/logo" \  -H "Authorization: Bearer $FENFILL_API_KEY" \  -o logo