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:

  1. Upload – fields are extracted right away
  2. Review and confirm the fields (or hand over to a human)
  3. Sign
  4. 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_…
ScopeAllows
documents:readRead documents and fields, query the allowance, download the signed PDF
documents:writeUpload PDFs, confirm fields, pick checkbox options
documents:signSign 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.pdf

The 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

Scope: documents:read

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

Scope: documents:read

Parameters

NameinTypeDescription
limitqueryintegeroptional · 1–100, default 25
offsetqueryintegeroptional · default 0
statusqueryDocumentStatusoptional

Responses

  • 200 OK

POST/documents

Upload a PDF and extract its fields

Scope: documents:write

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

Scope: documents:read

Parameters

NameinTypeDescription
idpathstring (uuid)required

Responses

  • 200 OK
  • 404 Not found

POST/documents/{id}/fields/{fieldId}/confirm

Confirm a field, optionally overriding its value

Scope: documents:write

Omit `value` to accept the suggestion as is; `null` or an empty string clears it.

Parameters

NameinTypeDescription
idpathstring (uuid)required
fieldIdpathstring (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

Scope: documents:write

The other options of the group become `not_applicable`.

Parameters

NameinTypeDescription
idpathstring (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

Scope: documents:sign

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

NameinTypeDescription
idpathstring (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

Scope: documents:read

Answers 409 `not_signed` until a signed PDF exists.

Parameters

NameinTypeDescription
idpathstring (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

NameTypeDescription
iduuidDocument id
filenamestringName of the uploaded file
titlestring | nullDetected title of the form
documentTypestring | nullDetected document type
statusstringProcessing state, see below
pageCountinteger | nullNumber of pages
requiresStrongerSignaturebooleantrue: needs more than a simple electronic signature – signing via API is refused
requiresStrongerSignatureReasonstring | nullWhy
createdAt / signedAt / sentAtdate-time | nullISO 8601
downloadablebooleantrue once a signed PDF can be downloaded
reviewUrluriReview screen in the browser – hand over here when a human must decide
fieldsField[]Only included for a single document and for uploads

Field

NameTypeDescription
iduuidField id
pageintegerPage in the PDF
typetext | date | checkbox | signature
labelstringLabel in the form
semanticKeystring | nullMeaning, e.g. the profile value used to fill it
statestringReliability, see below
valuestring | nullSuggested or confirmed value
sourceprofile | learned_cache | user_input | noneWhere the value came from
confirmedbooleanSigning works once every relevant field is confirmed
checkboxGroupstring | nullSame value = mutually exclusive options

Field state (state)

okRecognised with confidence and filled.
unsureA suggestion exists, but it is uncertain – please check.
missingNo value known – has to be supplied.
handHandwritten or not machine-readable – better hand over to a human (reviewUrl).
not_applicableDoesn't apply here (e.g. a deselected checkbox option); needs no confirmation.
out_of_scopeNot filled by Snapfritz; needs no confirmation.

Document status (status)

receivedReceived, being processed.
understandingDetecting the document type.
extractingReading the fields.
ready_for_reviewExtraction done – confirm the fields.
in_reviewCurrently being reviewed in the browser.
signedSigned; the PDF can be downloaded.
sentSigned and sent.
revokedRevoked.
failedUnreadable; 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." } }
HTTPcodeMeaning
400invalid_json, invalid_parameter, missing_fileMalformed request
401unauthorizedKey missing, unknown or revoked
402quota_exceededAllowance of the period used up – quota.kind says whether finished forms (forms) or uploads (uploads)
403plan_required, insufficient_scopePlan has no API access or no signing for the organisation / key lacks the scope
403forbiddenThe document is shared with you only to view or fill in
403key_owner_leftWhoever created the key has left the organisation – create a new key
404not_foundUnknown, malformed or foreign id – including an organisation document not shared with you
409conflict, not_signedWrong state, e.g. unconfirmed fields or nothing to download yet
413 / 415payload_too_large, not_a_pdf, unsupported_media_typeUnsuitable upload
422invalid, too_many_pagesCan't be done automatically (stronger signature needed, no signature on file …) / PDF of more than 20 pages (fields pages and limit)
429rate_limitedToo many requests – honour the Retry-After header
500internal_errorSomething 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.