# 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/v1
```

Requests 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](https://fenfill.com/docs/api/reference). The same reference is served live, generated from the running API, at [api.fenfill.com/v1/docs](https://api.fenfill.com/v1/docs), and the OpenAPI document at [api.fenfill.com/v1/openapi.json](https://api.fenfill.com/v1/openapi.json).

> **Privacy note:** The API has no endpoint that fills a form, and no request takes answers. Your agent fills forms on your machine with the MCP server's `fill_form` or `fill_template` ([MCP tools](https://fenfill.com/docs/mcp/tools)); only the blank form is uploaded, to analyze it.

## Authentication

Every request carries a workspace API key, as a Bearer token in the `Authorization` header:

```sh
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 `Authorization` header, 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.

> **Heads up:** Keep keys out of public code and shared files, and revoke a key straight away if you think it has leaked.

## Errors

Every response that isn't a `2xx` has the same JSON body: a stable machine-readable `code` and a message written for people.

**429 Too Many Requests**

```json
{
  "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](https://fenfill.com/terms#api)).

## 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_missing` first uses the label pages your plan includes each month. Pass `estimate: true` to 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](https://fenfill.com/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](https://fenfill.com/docs/changelog), unless the change is urgently needed for security or legal reasons. See [Terms §11b](https://fenfill.com/terms#api).

---

This is the Markdown version of https://fenfill.com/docs/api.
