Tax Form OCR API
Upload a W-2, 1099, or other US tax form, receive a webhook with the full field map: wages, withholding, employer/employee info, state breakdowns, control numbers, and any other labels detected on the page.
- PAT auth on the
/api/v1/agent/*endpoints - Webhook-driven (no polling)
- Multi-form PDFs (returns one entry per detected form)
- Raw label → value map (no opinionated schema)
Prerequisites
- A Personal Access Token from Account → API. Set it as
PAT=dcp_…. - An HTTPS endpoint to receive webhooks. The examples use webhook.site as a throwaway receiver for testing.
End-to-end example
bash
#!/usr/bin/env bash
set -euo pipefail
PAT="${PAT:?Set PAT to your dcp_… token}"
BASE="https://www.docuclipper.com"
PDF="w2.pdf"
# 1. Throwaway public receiver for the demo. In production, point at your own HTTPS endpoint.
TOK=$(curl -s -X POST https://webhook.site/token -d '{}' -H 'Content-Type: application/json' | jq -r .uuid)
RECEIVER="https://webhook.site/$TOK"
cleanup() {
[ -n "${WEBHOOK_ID:-}" ] && curl -s -X DELETE -H "Authorization: Bearer $PAT" "$BASE/api/v1/agent/webhooks/$WEBHOOK_ID" >/dev/null
curl -s -X DELETE "https://webhook.site/token/$TOK" >/dev/null
}
trap cleanup EXIT
# 2. Register webhook
WEBHOOK_ID=$(curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d "{\"url\":\"$RECEIVER\",\"events\":[\"form.extraction.completed\",\"document.extraction.failed\"]}" \
"$BASE/api/v1/agent/webhooks" | jq -r .id)
# 3. Get presigned upload URL + PUT the file to S3
PRESIGN=$(curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d "{\"filename\":\"$PDF\",\"mimetype\":\"application/pdf\"}" \
"$BASE/api/v1/agent/documents/upload-url")
S3_URL=$(echo "$PRESIGN" | jq -r .url)
DOC_ID=$(echo "$PRESIGN" | jq -r .document.id)
curl -s -o /dev/null -X PUT -H "Content-Type: application/pdf" --data-binary "@$PDF" "$S3_URL"
# 4. Create job
JOB_ID=$(curl -s -X POST -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d "{\"documents\":[$DOC_ID],\"jobType\":\"Form\"}" \
"$BASE/api/v1/agent/jobs" | jq -r .jobId)
echo "job $JOB_ID — waiting for form.extraction.completed…"
# 5. Wait for the webhook. NOTE: this bash flow skips HMAC verification —
# see the Python or Node.js tab for production code.
DEADLINE=$(( $(date +%s) + 600 ))
while [ $(date +%s) -lt $DEADLINE ]; do
PAYLOAD=$(curl -s "https://webhook.site/token/$TOK/requests" | jq -r --arg jid "$JOB_ID" --arg ev "form.extraction.completed" '
.data[]? | select(.headers["x-docuclipper-event"][0]==$ev) | select(.content|fromjson|.job.id==$jid) | .content' | head -1)
if [ -n "$PAYLOAD" ]; then echo "$PAYLOAD" | jq .data; exit 0; fi
sleep 2
done
echo "timed out" >&2; exit 1Webhook payload
Real data field from a successful run on a real W-2.
json
{
"2666986": {
"formMode": [
{
"pageNumber": 0,
"rowNumber": 0,
"form": {
"a Employee's SSA number": "354-70-8923",
"b Employer's FED ID number": "45-4346475",
"1 Wages, tips, other comp.": "8469.85",
"2 Federal income tax withheld": "684.21",
"3 Social security wages": "8524.07",
"4 Social security tax withheld": "528.49",
"5 Medicare wages and tips": "8524.07",
"6 Medicare tax withheld": "123.60",
"12a": "D 54.22",
"12b": "DD 3292.88",
"16 State wages, tips, etc.": "8469.85",
"17 State income tax": "358.62",
"State": "IL",
"C Employer's name, address, and ZIP code": "SUN BASKET INC 5215 HELLYER AV STE 250 SAN JOSE CA 95138",
"e/f Employee's name, address and ZIP code": "ROY A ACUP 503 PARK STREET 1C WATERLOO IL 62298",
"d Control number": "100000 LOS2/W7U"
}
}
// … one entry per detected form on the document
]
}
}Field reference
| Field | Type | Description |
|---|---|---|
| [documentId].formMode[] | array | One entry per detected form on the document |
| formMode[].pageNumber | number | Source page (0-indexed) |
| formMode[].rowNumber | number | Order within the document |
| formMode[].form | object | Raw field map — keys are the exact field labels as printed on the form (e.g. "1 Wages, tips, other comp.") |
Notes & gotchas
- Field labels are raw OCR strings. Different forms (and different employers) produce slightly different labels — e.g. a W-2 may report wages under
"1 Wages, tips, other comp."on one variant and a near-duplicate string on another. Map to your own schema with tolerant matching. - Numeric values are returned as strings (e.g.
"528.49"). Cast on your side. - Multi-form documents (a stack of W-2s) return one entry per detected form, in document order.