API overview
The /v1 API's base URL, authentication, errors, rate limits and versioning.
Base URL
Every endpoint lives under one base URL, over HTTPS:
https://api.fenfill.com/v1Requests and responses are JSON, except the analyze upload (multipart/form-data) and the two file downloads (a PDF, a logo image). Each endpoint is listed in the API reference. The same reference is served live, generated from the running API, at api.fenfill.com/v1/docs, and the OpenAPI document at api.fenfill.com/v1/openapi.json.
Authentication
Every request carries a workspace API key, as a Bearer token in the Authorization header:
curl https://api.fenfill.com/v1/account \ -H "Authorization: Bearer $FENFILL_API_KEY"- Keys look like
ff_live_followed by 43 characters. Create and revoke them in Dashboard → Settings → API keys. API access is included on Pro and above. - The key is read only from the
Authorizationheader, never from a query parameter or another header. - A key is shown once, when it is created. fenfill stores only a hash of it, so a lost key can't be shown again: create a new one.
- A workspace can have up to 10 active keys. A key acts for its workspace, and stops working when it is revoked or when the person who created it leaves the workspace.
- Every request is scoped to the key's workspace. A template or job in another workspace answers
404 not_found, exactly like one that doesn't exist.
Errors
Every response that isn't a 2xx has the same JSON body: a stable machine-readable code and a message written for people.
{ "error": { "code": "rate_limited", "message": "Too many analyze requests for this API key. Please slow down and retry.", "retry_after": 6 }}Some errors add fields next to code and message: retry_after (seconds) on a 429, need and have on insufficient_scans, max_bytes on a 413, current_updated_at on template_conflict, and details on invalid_schema and template_needs_migration. Branch on code, not on the message or the status: the list of codes is closed.
| Code | Status | Meaning |
|---|---|---|
invalid_api_key | 401 | The key is missing, malformed or unknown. Send it as Authorization: Bearer ff_live_…. |
key_revoked | 401 | The key was revoked, or the person who created it left the workspace. Create a new one. |
plan_required | 403 | The workspace's plan doesn't include API access (Pro and above do), or the feature asked for (POST /v1/forms/acroform needs the Team plan or above). It works again after an upgrade. |
template_frozen | 403 | The template is frozen because the workspace is over its plan's template limit. 409 on POST /v1/templates/{template_id}/recipients. |
pdf_password_required | 422 | The PDF needs a password to open. Upload a copy without the open password. |
pdf_fill_forbidden | 422 | The PDF's author doesn't allow it to be filled in. |
filled_output | 422 | The PDF is a form already filled with fenfill. Only blank forms can be analyzed: upload the original blank PDF. |
too_many_pages | 422 | The chosen pages are more than one analysis covers. Split the document into several jobs with pages (see max_pages_per_job on GET /v1/account). |
not_pdf | 422 | The file is not a PDF. |
daily_limit | 429 | The workspace reached its daily AI processing limit. Try again later. |
too_many_active_jobs | 429 | The workspace already has as many analyze jobs queued or running as it may. Wait for one to finish. |
queue_full | 429 | The analysis queue is full right now. Retry after Retry-After seconds. |
rate_limited | 429 | The key sent too many requests. Retry after Retry-After seconds. |
template_limit | 409 | Saving would take the workspace past its plan's template limit. Delete a template, or analyze without save_as_template. |
template_not_restricted | 409 | Recipient links need a template whose access is restricted. |
template_conflict | 409 | The template changed since expected_updated_at. The body carries current_updated_at: fetch the template again, re-apply your edits and retry. |
invalid_schema | 422 | The edited template doesn't validate. details lists each problem as {path, reason} (at most 50). |
template_needs_migration | 422 | The stored template holds data from an older version that the API can't save back. Make the change in the fenfill web editor. |
insufficient_scans | 402 | The workspace has too few page scans left for this job. The body carries need and have. |
job_in_progress | 409 | An analysis of this template is already queued or running. Wait for it to finish, then fetch the template again. |
kind_required | 422 | The chosen pages already have fields: pass kind (find, relabel, label_missing, or scratch to replace them). |
not_found | 404 | No such resource in this workspace. A resource in another workspace answers the same way. |
result_expired | 410 | The job's unsaved result is gone (kept 24 hours after the job finishes and 1 hour after its first fetch), or its saved template was deleted. Analyze the PDF again. |
invalid_request | 422 | The request is malformed; the message says what to change. Sometimes 400, and 408 when an upload was too slow and the server stopped waiting. |
file_too_large | 413 | The file is larger than the plan allows. The body carries max_bytes. |
request_too_large | 413 | The edited template is too large to save in one request. The body carries max_bytes. |
unavailable | 503 | The service is unavailable for a moment. Retry shortly. |
internal_error | 500 | Something went wrong on our side. The error has been reported. |
A finished job that failed is not an error response: GET /v1/jobs/{job_id} answers 200 with status set to error and an error object of its own, with codes such as pdf_corrupt, no_fields_found or job_timeout.
Rate limits
| Limit | Applies to | When it's exceeded |
|---|---|---|
| 60 requests a minute | Each API key, every endpoint | 429 rate_limited |
| 10 analyses a minute | Each API key, POST /v1/forms/analyze and POST /v1/templates/{template_id}/analyze | 429 rate_limited |
| 10 PDF downloads a minute | Each API key, GET /v1/templates/{template_id}/pdf and POST /v1/forms/acroform together | 429 rate_limited |
| 30 template updates a minute | Each API key, PATCH /v1/templates/{template_id} | 429 rate_limited |
| A few analyze jobs queued or running at once | Each workspace | 429 too_many_active_jobs |
| A daily AI processing limit | Each workspace | 429 daily_limit |
Each per-key limit allows a burst: a quiet key may spend a full minute's requests at once, and is then held to the steady rate.
Every 429 carries a Retry-After header in whole seconds, and the same number as retry_after in the body. Wait that long before retrying. While a job runs, GET /v1/jobs/{job_id} also sends Retry-After as a polling hint (10 seconds while queued, 3 while running), so a few jobs polled as told stay under the per-key limit.
The limits can change to protect the service; this page always shows the current ones. Don't spread requests over several keys or workspaces to get around them (Terms §11b).
Page scans
Analyzing forms uses page scans from your plan, at the same rate as in the web app:
- Analyzing a flat or scanned form uses one page scan per analyzed page.
- A form that already has fillable fields is read directly, with no AI, and uses no page scans, unless you opt in to
detect_extra_blanks, which uses one page scan per page it covers. - Re-analyzing pages of a saved template (
POST /v1/templates/{template_id}/analyze) uses one page scan per page.label_missingfirst uses the label pages your plan includes each month. Passestimate: trueto see the cost without starting anything. - Filling a form happens on your machine with the MCP server and uses no page scans.
The scans for a job are reserved when it is accepted, and returned automatically, usually within the hour, if the job fails on our side. A request refused before the analysis starts uses none. cost on the 202 and on the job says how many a job reserved, and GET /v1/account returns scans_remaining. Plans and their allowances are on Pricing.
Versioning
The API is versioned in its path: /v1. We may add to it at any time, such as new endpoints, new optional parameters or new fields in a response, so ignore fields you don't know.
Before we remove or change anything in a way that breaks existing integrations, we give at least 30 days' notice, by email or in the changelog, unless the change is urgently needed for security or legal reasons. See Terms §11b.