API reference
Every /v1 endpoint, grouped by tag, with its parameters, schemas and a curl example.
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
/v1/forms/analyzeAuth: 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
file | form | file | yes | The blank PDF form, at most your plan's max_file_mb. |
name | form | string | no | A name for the form. Defaults to the file name. |
pages | form | string | no | 1-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_template | form | boolean | no | true: 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. |
access | form | "public" | "restricted" | no | The 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_blanks | form | boolean | no | Only 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:
| Field | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | – |
status | string | yes | – |
mode | "ai" | "acroform" | yes | acroform: 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). |
pages | array of integer | yes | The 1-based pages the job covers, sorted. |
cost | integer | yes | Page scans reserved for this job (0 for acroform). |
extra_blanks | boolean | null | no | Only 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_pages | array of integer | null | no | With extra_blanks: the 1-based pages the vision pass covers (what cost pays for). |
extra_blanks_available | boolean | null | no | Only 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):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
402 | insufficient_scans (with need, have) |
403 | plan_required / template_frozen |
404 | not_found |
408 | invalid_request (the upload was too slow) |
409 | template_limit |
413 | file_too_large (with max_bytes) |
422 | not_pdf / pdf_password_required / pdf_fill_forbidden / filled_output / too_many_pages / invalid_request |
429 | rate_limited / daily_limit / too_many_active_jobs / queue_full (with Retry-After) |
503 | unavailable |
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
/v1/forms/acroformAuth: 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | body | string | yes | The 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):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | pdf_password_required / pdf_fill_forbidden / invalid_schema / template_needs_migration / invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
# 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.pdfJobs
Poll an analyze job and fetch its result.
Get an analyze job's status and result
/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).
| Name | In | Type | Required | Description |
|---|---|---|---|---|
job_id | path | string | yes | – |
include | query | string | null | no | render adds the raw v5 schema (page images blanked) once done. |
Returns 200 with JobStatus:
| Field | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | – |
status | "queued" | "running" | "done" | "error" | yes | – |
mode | "ai" | "acroform" | yes | acroform: 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). |
phase | string | null | yes | – |
progress | integer | yes | – |
pages | array of integer | yes | – |
cost | integer | yes | – |
created_at | string (date-time) | yes | – |
template_id | string | null | yes | Set when the job saved a template (save_as_template, or a template re-analysis). |
error | JobError | null | yes | – |
result | AgentSchema | null | yes | The agent schema, once the job is done. |
render | object | null | no | Only with ?include=render on a done job: the raw v5 schema, page images blanked. |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | template_frozen (the saved template) |
404 | not_found |
410 | result_expired |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
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
/v1/templatesAuth: Bearer API key
Active templates, newest first. Frozen templates and unfinished drafts (still awaiting the owner's AI/manual setup choice) are not listed.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Page size (1–100). Default 50. |
cursor | query | string | null | no | next_cursor from the previous page. |
Returns 200 with TemplateList:
| Field | Type | Required | Description |
|---|---|---|---|
templates | array of TemplateSummary | yes | – |
next_cursor | string | null | yes | Pass as ?cursor= for the next page; null at the end. |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
curl "https://api.fenfill.com/v1/templates" \ -H "Authorization: Bearer $FENFILL_API_KEY"Get a template's agent schema
/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).
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
include | query | string | null | no | render adds the raw v5 schema (page images blanked). |
Returns 200 with AgentSchema:
| Field | Type | Required | Description |
|---|---|---|---|
schema_version | integer | yes | – |
template_id | string | null | yes | – |
name | string | null | yes | – |
updated_at | string | null | no | Only on a saved template: when its schema last changed. Pass it back unchanged as expected_updated_at to PATCH /v1/templates/{id}. |
page_count | integer | yes | – |
pages | array of AgentPage | yes | – |
fields | array of ScalarEntry | SignatureEntry | CombEntry | ChoiceEntry | TableEntry | TableRowsEntry | yes | – |
render | object | null | no | Only with ?include=render: the raw v5 template schema, page images blanked. |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
curl "https://api.fenfill.com/v1/templates/$TEMPLATE_ID" \ -H "Authorization: Bearer $FENFILL_API_KEY"Correct a template's layout
/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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
schema | body | V5TemplateSchema | yes | The 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_at | body | string (date-time) | yes | The 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:
| Field | Type | Required | Description |
|---|---|---|---|
template_id | string | yes | – |
updated_at | string (date-time) | yes | – |
result | AgentSchema | yes | – |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
409 | template_conflict (with current_updated_at) / job_in_progress (a re-analysis is running) |
413 | request_too_large (with max_bytes) |
422 | invalid_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) |
429 | rate_limited (with Retry-After): too many updates, or too many in progress |
503 | unavailable |
# 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.jsonDownload the blank template PDF
/v1/templates/{template_id}/pdfAuth: 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
Returns 200 with application/pdf: The blank template PDF.
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
curl "https://api.fenfill.com/v1/templates/$TEMPLATE_ID/pdf" \ -H "Authorization: Bearer $FENFILL_API_KEY" \ -o blank-form.pdfRe-analyze pages of a saved template
/v1/templates/{template_id}/analyzeAuth: 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
pages | body | string | yes | The 1-based pages to analyze, e.g. "2" or "1-3,5". |
kind | body | "scratch" | "find" | "relabel" | "label_missing" | null | no | scratch: 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_at | body | string | null | no | The template's updated_at from GET. Required unless estimate is true; a newer one is 409 template_conflict. |
estimate | body | boolean | no | true: 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.):
| Field | Type | Required | Description |
|---|---|---|---|
kind | "scratch" | "find" | "relabel" | "label_missing" | yes | – |
pages | array of integer | yes | The 1-based pages the run would cover, sorted. |
cost | integer | yes | Page scans the run would charge now. |
free_label_pages | integer | yes | How many of pages the monthly included label pages would cover (label_missing only). |
free_label_pages_left | integer | yes | Included label pages left this month after the run. |
scans_left | integer | yes | The workspace's page scans left now. |
insufficient | boolean | yes | true: the run would be refused with 402 insufficient_scans. |
Returns 202 with AnalyzeAccepted:
| Field | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | – |
status | string | yes | – |
mode | "ai" | "acroform" | yes | acroform: 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). |
pages | array of integer | yes | The 1-based pages the job covers, sorted. |
cost | integer | yes | Page scans reserved for this job (0 for acroform). |
extra_blanks | boolean | null | no | Only 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_pages | array of integer | null | no | With extra_blanks: the 1-based pages the vision pass covers (what cost pays for). |
extra_blanks_available | boolean | null | no | Only 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):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
402 | insufficient_scans (with need, have) |
403 | plan_required / template_frozen |
404 | not_found |
409 | template_conflict (with current_updated_at) / job_in_progress |
422 | invalid_request / too_many_pages / kind_required |
429 | rate_limited / daily_limit / too_many_active_jobs / queue_full (with Retry-After) |
503 | unavailable |
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
/v1/templates/{template_id}/recipientsAuth: 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).
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
label | body | string | yes | Who the link is for, as the sender will see it (e.g. 'Anna K.'). |
expires_at | body | string (date-time) | null | no | When the link stops working (ISO 8601, UTC if no offset). Omit for no expiry. |
Request body: application/json (RecipientCreate).
Returns 201 with Recipient:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | – |
label | string | null | yes | – |
url | string | yes | – |
status | "pending" | "opened" | "completed" | "sent_back" | yes | pending → opened → completed. sent_back is reserved (not yet emitted). |
opened_at | string (date-time) | null | yes | – |
completed_at | string (date-time) | null | yes | – |
created_at | string (date-time) | yes | – |
expires_at | string (date-time) | null | yes | – |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
409 | template_not_restricted / template_frozen |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
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
/v1/templates/{template_id}/recipientsAuth: Bearer API key
The template's live (not revoked) recipient links, newest first, at most 500. Revoking happens in the fenfill app.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
template_id | path | string | yes | – |
Returns 200 with RecipientList:
| Field | Type | Required | Description |
|---|---|---|---|
recipients | array of Recipient | yes | – |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
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
/v1/accountAuth: 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:
| Field | Type | Required | Description |
|---|---|---|---|
workspace_id | string | yes | – |
tier | string | yes | – |
scans_remaining | integer | null | yes | – |
max_pages | integer | null | yes | How far into a document the plan reads (its first N pages). |
max_pages_per_job | integer | yes | The 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_mb | integer | null | yes | – |
branding | BrandingFlags | yes | – |
Errors (codes):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
curl "https://api.fenfill.com/v1/account" \ -H "Authorization: Bearer $FENFILL_API_KEY"The workspace's branding logo
/v1/account/logoAuth: 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):
| Status | Code |
|---|---|
401 | invalid_api_key / key_revoked |
403 | plan_required / template_frozen |
404 | not_found |
422 | invalid_request |
429 | rate_limited (with Retry-After) |
503 | unavailable |
curl "https://api.fenfill.com/v1/account/logo" \ -H "Authorization: Bearer $FENFILL_API_KEY" \ -o logo