DocuClipper logo

Check OCR & Data Extraction API

Extract check number, date, amount, payee, memo, MICR routing, and bank from scanned or photographed checks. VLM-powered; agreement between courtesy + legal amounts is returned as a confidence signal.

  • PAT auth on the /api/v1/agent/* endpoints
  • Webhook-driven (no polling)
  • Per-check self-consistency flags (isCheckConsistent, isMicrValid)
  • Handwritten-field detection

Prerequisites

  1. A Personal Access Token from Account → API. Set it as PAT=dcp_….
  2. An HTTPS receiver for the webhook. The runnable examples use webhook.site as a throwaway endpoint for testing.

End-to-end example

Note the subType: "checkImages" flag — this is what switches the worker from statement-table parsing to per-check VLM extraction.

bash
#!/usr/bin/env bash
set -euo pipefail
PAT="${PAT:?Set PAT to your dcp_… token}"
BASE="https://www.docuclipper.com"
PDF="checks.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\":[\"bank_statement.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\":\"ExtractData\",\"subType\": \"checkImages\"}" \
  "$BASE/api/v1/agent/jobs" | jq -r .jobId)
echo "job $JOB_ID — waiting for bank_statement.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 "bank_statement.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 check.

json
{
  "2666983": {
    "checks": {
      "bankMode": {
        "transactions": [
          {
            "memo": "Avance Professional Service - Accounting Service for October (Check #7203)",
            "amount": -550,
            "date": "20251028000000",
            "name": "Avance Professional Service Acc",
            "payee": "Avance Professional Service Acc",
            "checkNumber": "7203",
            "metadata": {
              "confidence": 1,
              "extractor": "vlm",
              "amountText": "five hundred and fifty dollars",
              "legalAmountNumeric": 550,
              "routingNumber": "021100361",
              "bank": "JPMorgan Chase Bank, N.A.",
              "isCheckConsistent": true,
              "isMicrValid": true,
              "docTypeGuess": "check",
              "handwrittenFields": ["date", "payee", "courtesyAmount", "legalAmount", "memo", "signature"],
              "payee": "Avance Professional Service",
              "checkMemo": "Accounting Service for October"
            },
            "pageNumber": 2
          }
        ],
        "totalCredits": "0.00",
        "totalDebits": "-550.00",
        "numCredits": 0,
        "numDebits": 1
      },
      "metadata": []
    }
  }
}

Field reference

FieldTypeDescription
[documentId].checks.bankMode.transactions[]arrayOne entry per check on the document
transactions[].amountnumberSigned amount (negative = check written / outgoing)
transactions[].checkNumberstringCheck number from MICR + courtesy/legal lines
transactions[].datestringCheck date, YYYYMMDDHHMMSS
transactions[].payeestringPayee from the "Pay to the order of" line
transactions[].memostringMemo line text
transactions[].metadata.routingNumberstringRouting number from MICR
transactions[].metadata.bankstringBank name resolved from the routing number
transactions[].metadata.amountTextstringLegal amount in words (e.g. "five hundred dollars")
transactions[].metadata.legalAmountNumericnumberNumeric form of the legal amount, for cross-check vs courtesy amount
transactions[].metadata.isCheckConsistentbooleanCourtesy ↔ legal amount agreement signal
transactions[].metadata.isMicrValidbooleanMICR mod-10 routing-number checksum validates
transactions[].metadata.docTypeGuessstring"check" if VLM is confident the page is a check
transactions[].metadata.handwrittenFieldsarrayFields detected as handwritten — e.g. payee, courtesyAmount, legalAmount, signature

Notes & gotchas

  • The check extractor runs full-page VLM on each page. Multi-check PDFs return one entry per page in bankMode.transactions[].
  • When isCheckConsistent is false, the courtesy and legal amounts disagree — treat the check as needing human review.
  • docTypeGuess values other than "check"(e.g. "deposit_slip") indicate the VLM thinks the page isn't actually a check.