API documentation
The Snapfritz API lets your own software or an autonomous agent use the PDF service: upload a PDF, review and confirm the extracted fields, sign, and download the finished PDF.
Overview
- Base URL
https://snapfritz.com/api/v1- Format
- JSON (UTF-8); uploads as multipart/form-data or application/pdf
- Plan
- Platin – without it calls answer 403 plan_required
- Version
- v1 – additions are backwards compatible
The flow is always the same:
- Upload – fields are extracted right away
- Review and confirm the fields (or hand over to a human)
- Sign
- Download the finished PDF
Authentication
Every request carries an API key in the Authorization header. Create keys under Developer; they are shown once and stored only as a hash. A key belongs to exactly one account and may only do what its scopes allow.
Authorization: Bearer sf_live_…| Scope | Allows |
|---|---|
documents:read | Read documents and fields, query the allowance, download the signed PDF |
documents:write | Upload PDFs, confirm fields, pick checkbox options |
documents:sign | Sign documents with the account's stored signature – off by default |
Treat the key like a password: never commit it to a repository or ship it in a client app. If in doubt, revoke it under Developer and create a new one.
Quickstart
A complete run. Replace $DOC and $FIELD with the ids from the responses.
export SNAPFRITZ_API_KEY=sf_live_…
# 1. upload
curl -s -X POST https://snapfritz.com/api/v1/documents \
-H "Authorization: Bearer $SNAPFRITZ_API_KEY" \
-F file=@formular.pdf
# 2. confirm a field (omit "value" to accept the suggestion)
curl -s -X POST https://snapfritz.com/api/v1/documents/$DOC/fields/$FIELD/confirm \
-H "Authorization: Bearer $SNAPFRITZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": "Erika Muster"}'
# 3. sign (needs the documents:sign scope)
curl -s -X POST https://snapfritz.com/api/v1/documents/$DOC/sign \
-H "Authorization: Bearer $SNAPFRITZ_API_KEY"
# 4. download the filled PDF
curl -s https://snapfritz.com/api/v1/documents/$DOC/download \
-H "Authorization: Bearer $SNAPFRITZ_API_KEY" -o ausgefuellt.pdfThe same as a Python script
Confirms confidently recognised fields automatically and hands everything uncertain to a human.
import os
import requests
BASE = "https://snapfritz.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['SNAPFRITZ_API_KEY']}"}
with open("formular.pdf", "rb") as f:
doc = requests.post(f"{BASE}/documents", headers=H, files={"file": f}, timeout=120)
doc.raise_for_status()
doc = doc.json()
if doc["status"] == "failed":
raise SystemExit("PDF not readable")
for field in doc["fields"]:
if field["confirmed"] or field["state"] in ("not_applicable", "out_of_scope"):
continue
if field["state"] != "ok":
# unsure / missing / hand: decide yourself or hand over to a human
raise SystemExit(f"Needs a decision: {field['label']} -> {doc['reviewUrl']}")
requests.post(f"{BASE}/documents/{doc['id']}/fields/{field['id']}/confirm",
headers=H, json={}).raise_for_status()
requests.post(f"{BASE}/documents/{doc['id']}/sign", headers=H).raise_for_status()
pdf = requests.get(f"{BASE}/documents/{doc['id']}/download", headers=H)
open("ausgefuellt.pdf", "wb").write(pdf.content)Example upload response
Shortened to one field; values are examples.
{
"id": "3f0c2d5e-8a4b-4f0e-9c1a-2b7d6e5f4a31",
"filename": "formular.pdf",
"title": "Anmeldung",
"documentType": "registration",
"status": "ready_for_review",
"pageCount": 2,
"requiresStrongerSignature": false,
"requiresStrongerSignatureReason": null,
"createdAt": "2026-09-25T13:04:11.000Z",
"signedAt": null,
"sentAt": null,
"downloadable": false,
"reviewUrl": "https://snapfritz.com/documents/3f0c2d5e-8a4b-4f0e-9c1a-2b7d6e5f4a31",
"fields": [
{
"id": "b1f7c8a0-52de-4c1e-8f3a-9d0e7a6b5c42",
"page": 1,
"type": "text",
"label": "Nachname",
"semanticKey": "last_name",
"state": "ok",
"value": "Muster",
"source": "profile",
"confirmed": false,
"checkboxGroup": null
}
]
}Endpoints
All paths are relative to the base URL. Ids are UUIDs; foreign or malformed ids answer 404.
GET/usage
Allowances for the current period
Shows how many forms were finished and how many remain; `uploads` does the same for uploaded PDFs.
Responses
- 200 OK
GET/documents
List documents, newest first
Parameters
| Name | in | Type | Description |
|---|---|---|---|
limit | query | integer | optional · 1–100, default 25 |
offset | query | integer | optional · default 0 |
status | query | DocumentStatus | optional |
Responses
- 200 OK
POST/documents
Upload a PDF and extract its fields
Extraction runs synchronously (typically 10–60 s); the response is the finished document with all its fields. Send either multipart/form-data with a `file` part, or the raw PDF as application/pdf (name it via the X-Filename header). Max. 20 MB and 20 pages. Every upload counts against the upload allowance; unreadable scans end as status `failed` and don't count.
Request body
multipart/form-data (`file` part) or application/pdf
Responses
- 201 Created
- 402 Upload allowance used up (quota_exceeded)
- 413 Too large
- 415 Not a PDF
- 422 More pages than a form may have (too_many_pages)
- 429 Rate limited (10 uploads/min per key)
GET/documents/{id}
A document with its fields
Parameters
| Name | in | Type | Description |
|---|---|---|---|
id | path | string (uuid) | required |
Responses
- 200 OK
- 404 Not found
POST/documents/{id}/fields/{fieldId}/confirm
Confirm a field, optionally overriding its value
Omit `value` to accept the suggestion as is; `null` or an empty string clears it.
Parameters
| Name | in | Type | Description |
|---|---|---|---|
id | path | string (uuid) | required |
fieldId | path | string (uuid) | required |
Request body
{ "value": "Erika Muster" } – optional
Responses
- 200 OK
- 404 Not found
POST/documents/{id}/checkbox-groups/select
Pick one option of a mutually exclusive checkbox group
The other options of the group become `not_applicable`.
Parameters
| Name | in | Type | Description |
|---|---|---|---|
id | path | string (uuid) | required |
Request body
{ "fieldId": "<uuid>" }
Responses
- 200 OK
- 404 Not found
- 422 Field is not part of a checkbox group
POST/documents/{id}/sign
Fill the PDF and apply the profile signature
Needs the documents:sign scope (off by default). Fails with 409 while fields are unconfirmed, and with 422 if the document needs a stronger signature than we can provide or the profile has no signature. Idempotent on an already signed document.
Parameters
| Name | in | Type | Description |
|---|---|---|---|
id | path | string (uuid) | required |
Responses
- 200 OK
- 402 Allowance of finished forms used up, and additional forms off or at their cap (quota_exceeded)
- 403 Plan doesn't include signing for the organisation (plan_required)
- 409 Not ready to sign
- 422 Cannot be signed automatically
GET/documents/{id}/download
The filled, signed PDF
Answers 409 `not_signed` until a signed PDF exists.
Parameters
| Name | in | Type | Description |
|---|---|---|---|
id | path | string (uuid) | required |
Responses
- 200 OK
- 409 Not signed yet
Machine-readable description for client generators and agent frameworks: OpenAPI 3.1 (JSON)
Objects
The public shape of a document and its fields. Internal details such as storage locations are deliberately not part of it.
Document
| Name | Type | Description |
|---|---|---|
id | uuid | Document id |
filename | string | Name of the uploaded file |
title | string | null | Detected title of the form |
documentType | string | null | Detected document type |
status | string | Processing state, see below |
pageCount | integer | null | Number of pages |
requiresStrongerSignature | boolean | true: needs more than a simple electronic signature – signing via API is refused |
requiresStrongerSignatureReason | string | null | Why |
createdAt / signedAt / sentAt | date-time | null | ISO 8601 |
downloadable | boolean | true once a signed PDF can be downloaded |
reviewUrl | uri | Review screen in the browser – hand over here when a human must decide |
fields | Field[] | Only included for a single document and for uploads |
Field
| Name | Type | Description |
|---|---|---|
id | uuid | Field id |
page | integer | Page in the PDF |
type | text | date | checkbox | signature | |
label | string | Label in the form |
semanticKey | string | null | Meaning, e.g. the profile value used to fill it |
state | string | Reliability, see below |
value | string | null | Suggested or confirmed value |
source | profile | learned_cache | user_input | none | Where the value came from |
confirmed | boolean | Signing works once every relevant field is confirmed |
checkboxGroup | string | null | Same value = mutually exclusive options |
Field state (state)
ok | Recognised with confidence and filled. |
unsure | A suggestion exists, but it is uncertain – please check. |
missing | No value known – has to be supplied. |
hand | Handwritten or not machine-readable – better hand over to a human (reviewUrl). |
not_applicable | Doesn't apply here (e.g. a deselected checkbox option); needs no confirmation. |
out_of_scope | Not filled by Snapfritz; needs no confirmation. |
Document status (status)
received | Received, being processed. |
understanding | Detecting the document type. |
extracting | Reading the fields. |
ready_for_review | Extraction done – confirm the fields. |
in_review | Currently being reviewed in the browser. |
signed | Signed; the PDF can be downloaded. |
sent | Signed and sent. |
revoked | Revoked. |
failed | Unreadable; not counted against the allowance. |
Errors
Errors always have this shape; the code is stable and meant for programs, the message for humans:
{ "error": { "code": "not_found", "message": "Document not found." } }| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_parameter, missing_file | Malformed request |
| 401 | unauthorized | Key missing, unknown or revoked |
| 402 | quota_exceeded | Allowance of the period used up – quota.kind says whether finished forms (forms) or uploads (uploads) |
| 403 | plan_required, insufficient_scope | Plan has no API access or no signing for the organisation / key lacks the scope |
| 403 | forbidden | The document is shared with you only to view or fill in |
| 403 | key_owner_left | Whoever created the key has left the organisation – create a new key |
| 404 | not_found | Unknown, malformed or foreign id – including an organisation document not shared with you |
| 409 | conflict, not_signed | Wrong state, e.g. unconfirmed fields or nothing to download yet |
| 413 / 415 | payload_too_large, not_a_pdf, unsupported_media_type | Unsuitable upload |
| 422 | invalid, too_many_pages | Can't be done automatically (stronger signature needed, no signature on file …) / PDF of more than 20 pages (fields pages and limit) |
| 429 | rate_limited | Too many requests – honour the Retry-After header |
| 500 | internal_error | Something went wrong on our side – retry later |
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": {
"code": "quota_exceeded",
"message": "The plan's upload allowance for this period is used up.",
"quota": { "kind": "uploads", "plan": "platin", "used": 200, "limit": 200, "resetsAt": "2026-10-25T00:00:00.000Z" }
}
}Limits & allowance
- 60 requests per minute per key, of which at most 10 uploads. Beyond that: 429 with Retry-After (seconds); every response carries X-RateLimit headers.
- PDFs up to 20 MB and 20 pages. The plan counts finished forms (every successful sign) and, twice as many, readable uploaded PDFs.
- When either is used up, upload or sign answer 402 quota_exceeded. Nothing is billed on top. GET /usage tells you beforehand how much is left and when it renews.
- Extraction runs synchronously inside the upload request and can take up to about a minute – set your client timeout accordingly (≥ 120 s).
- Everything done with a key appears in the account's log as “API key <prefix>”.
For agents
- Check state and confirmed of each field. Auto-confirm only ok; for unsure, missing and hand decide yourself or hand over to a human via reviewUrl.
- Call GET /usage before uploading to avoid 402.
- On 429 wait for Retry-After, on 5xx retry with backoff. POST …/sign is idempotent; uploads are not – after a timeout check GET /documents first instead of blindly re-uploading.
- Grant documents:sign only to keys that really should sign, and keep read and write keys apart.
- Documents with requiresStrongerSignature = true can't be signed via the API – they belong in the review screen.