DocuClipper logo

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

  1. A Personal Access Token from Account → API. Set it as PAT=dcp_….
  2. 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 1

Webhook 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

FieldTypeDescription
[documentId].formMode[]arrayOne entry per detected form on the document
formMode[].pageNumbernumberSource page (0-indexed)
formMode[].rowNumbernumberOrder within the document
formMode[].formobjectRaw 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.