API Reference

Base URL: https://dokyumi.com/api/v1

All endpoints return JSON. Authentication required on all requests.

Want the complete path? Follow a worked custom schema → multipart request → response and review-handling example.

Quickstart

Create a schema and test the complete response on a representative document.

1

Create an account

Sign up at dokyumi.com/dashboard. Free tier includes 25 extraction credits/month — no card required.

2

Define your schema

Go to Schemas → New Schema. Describe your document type and the fields you want to extract. AI will infer the full schema for you. Give it a slug like invoice-parser.

3

Generate an API key

Go to API Keys → New Key. Copy it now — it won't be shown again.

4

Send your first extraction

curl -X POST https://dokyumi.com/api/v1/extract \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -F "file=@invoice.pdf" \
  -F "schema=invoice-parser"
5

Handle the response

Check statuscompleted means there are no validation errors or known below-threshold confidence scores.review means you must inspect both validation.errors and validation.low_confidence_fields. Your structured data is in data.

Authentication

All requests require an API key passed as a Bearer token. Keys follow the format dk_live_*.

Authorization: Bearer dk_live_your_api_key_here
Never expose your API key. Always make extraction requests server-side. If a key is compromised, rotate it immediately from the API Keys page.

Scoped keys

Keys can be scoped to specific schemas. A scoped key will return a 403 if used to call an unauthorized schema. Create scoped keys for multi-tenant apps where each customer should only access their own schemas.

Unscoped keys (the default) can access all schemas in your organization.

POST

/api/v1/extract

Extract structured data from a document using a predefined schema. Returns structured JSON with a model-reported confidence map and validation details.

Request

Send a multipart/form-data request.

FieldTypeRequiredDescription
fileFileYesPDF, PNG, JPG, TIFF, or WEBP. Max 20MB.
schemaStringNoSchema slug. Defaults to your first active schema if omitted.

Code examples

cURL

curl -X POST https://dokyumi.com/api/v1/extract \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -F "file=@invoice.pdf" \
  -F "schema=invoice-parser"

JavaScript / TypeScript

async function extractDocument(file: File, schema: string) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('schema', schema)

  const response = await fetch('https://dokyumi.com/api/v1/extract', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer dk_live_your_api_key'
    },
    body: formData
  })

  if (!response.ok) {
    const err = await response.json()
    throw new Error(err.error)
  }

  const result: ExtractionResult = await response.json()
  
  if (result.status === 'review') {
    console.log('Validation errors:', result.validation.errors)
    console.log('Low confidence fields:', result.validation.low_confidence_fields)
  }

  return result
}

Python

import requests

def extract_document(file_path: str, schema: str, api_key: str) -> dict:
    with open(file_path, 'rb') as f:
        response = requests.post(
            'https://dokyumi.com/api/v1/extract',
            headers={'Authorization': f'Bearer {api_key}'},
            files={'file': f},
            data={'schema': schema}
        )
    
    response.raise_for_status()
    result = response.json()
    
    if result['status'] == 'review':
        print(f"Validation errors: {result['validation']['errors']}")
        print(f"Low confidence fields: {result['validation']['low_confidence_fields']}")
    
    return result

# Example
result = extract_document(
    'invoice.pdf',
    schema='invoice-parser',
    api_key='dk_live_your_api_key'
)
data = result['data']
print(data['vendor_name'], data['total_amount'])

Node.js (server-side)

import { readFileSync } from 'fs'
import FormData from 'form-data'
import fetch from 'node-fetch'

async function extractDocument(filePath, schema) {
  const form = new FormData()
  form.append('file', readFileSync(filePath), 'document.pdf')
  form.append('schema', schema)

  const res = await fetch('https://dokyumi.com/api/v1/extract', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer dk_live_your_api_key',
      ...form.getHeaders()
    },
    body: form
  })

  if (!res.ok) throw new Error((await res.json()).error)
  return await res.json()
}

Response Format

Every successful extraction returns this structure. Check status first, then read your fields from data.

{
  "id": "6f8c2d4a-7b31-4e95-9a20-c1d7f6b84210",
  "status": "completed",          // "completed" | "review"
  "request_id": "8f72e3b4-849d-4b6d-9eb1-7a5dd60dc8a7",
  "schema": "invoice-parser",

  // Model-produced fields generated against your schema; inspect validation below
  "data": {
    "vendor_name": "Acme Corp",
    "invoice_number": "INV-2026-001",
    "invoice_date": "2026-02-12",
    "total_amount": 1250.00,
    "line_items": [
      {
        "description": "Consulting Services",
        "quantity": 10,
        "unit_price": 125.00,
        "amount": 1250.00
      }
    ]
  },

  // Model-reported confidence map: 0.0 – 1.0; fields may be omitted
  "confidence": {
    "vendor_name": 0.98,
    "invoice_number": 0.95,
    "invoice_date": 0.99,
    "total_amount": 0.97,
    "line_items": 0.88
  },

  // Validation result (Zod schema)
  "validation": {
    "valid": true,
    "errors": [],
    "low_confidence_fields": []   // Fields below your schema's confidence threshold
  },

  "meta": {
    "processing_time_ms": 3420,
    "page_count": 1,
    "credits_used": 1,
    "ocr_cached": false,          // true = cached OCR was reused
    "model": "anthropic/claude-sonnet-4"
  }
}
FieldTypeDescription
idstringUnique extraction ID in UUID format.
statusstringcompleted · review. Failures use a non-2xx error envelope.
request_idstringRequest correlation ID, also returned in the X-Request-Id header.
schemastringThe schema slug selected for the extraction.
dataobjectModel-produced fields generated against your schema. Inspect validation before treating the data as schema-valid.
confidenceobjectModel-reported confidence scores (0.0–1.0). The map may omit fields; reported scores below the schema threshold contribute to review status.
validation.validbooleanWhether the extracted data passed schema type validation.
validation.errorsarrayZod validation issues if valid is false.
meta.credits_usednumberCredits charged for this extraction based on page count.
meta.ocr_cachedbooleanIf true, cached OCR was reused, which can reduce processing work and latency.

Handling review Status

An extraction returns status: "review" when:

  • One or more fields have confidence scores below your schema's threshold (default: 0.8)
  • The extracted data fails Zod type validation (e.g., a date field returned a non-date string)

The data object is still populated — the extraction ran. But you should treat flagged fields with extra scrutiny. A common pattern:

const result = await extract(file, schema)

if (result.status === 'completed') {
  // No validation errors or known below-threshold model scores
  await saveToDatabase(result.data)

} else if (result.status === 'review') {
  // Inspect both kinds of review detail before using the data
  const lowConfidenceFields = result.validation.low_confidence_fields
  const validationErrors = result.validation.errors
  
  await saveToDatabase({
    ...result.data,
    _needs_review: true,
    _review_fields: lowConfidenceFields,
    _validation_errors: validationErrors
  })
  await notifyReviewer(result.id, { lowConfidenceFields, validationErrors })
}
Tip:Adjust your schema's confidence threshold in the dashboard. Setting it lower (e.g., 0.5) reduces review flags for documents where some ambiguity is acceptable. Setting it higher (e.g., 0.9) catches more edge cases.

TypeScript Types

Copy these into your project for full type safety.

// Paste into dokyumi.d.ts or your types file

export interface ExtractionResult<T = Record<string, unknown>> {
  id: string
  status: 'completed' | 'review'
  request_id: string
  schema: string
  data: T
  confidence: Partial<Record<keyof T & string, number>>
  validation: {
    valid: boolean
    errors: ValidationError[]
    low_confidence_fields: string[]
  }
  meta: {
    processing_time_ms: number
    page_count: number
    credits_used: number
    ocr_cached: boolean
    model: string
  }
}

export interface ValidationError {
  code: string
  message: string
  path: (string | number)[]
}

export interface ApiError {
  error: string
  code: string
  request_id?: string
  pages?: number
  page_limit?: number
}

// Example: typed extraction for invoices
interface InvoiceData {
  vendor_name: string
  invoice_number: string
  invoice_date: string       // ISO 8601
  total_amount: number
  line_items: {
    description: string
    quantity: number
    unit_price: number
    amount: number
  }[]
}

type InvoiceExtraction = ExtractionResult<InvoiceData>

Error Codes

Failed requests return a non-2xx JSON envelope with error and code. Most errors also include request_id; early authentication or validation failures may omit it. Page-limit errors additionally include pages and page_limit.

{
  "error": "Monthly extraction limit reached (500). Upgrade your plan to continue.",
  "code": "quota_exceeded",
  "request_id": "8f72e3b4-849d-4b6d-9eb1-7a5dd60dc8a7"
}
StatusError messageCause & fix
400Missing fileNo file was attached. Add a file field to your multipart request.
400No schema foundThe schema slug doesn't exist or is inactive. Check the slug in your dashboard.
401Missing or invalid API keyThe Authorization header is missing or doesn't start with Bearer dk_live_.
401Invalid API keyKey not found or revoked. Generate a new key in your dashboard.
402Not enough credits for this document (quota_exceeded)The post-OCR page count requires more credits than remain. The reserved credit is refunded; add credits or retry after the next billing reset.
429Monthly extraction limit reached (quota_exceeded)You've hit your plan's monthly limit. Upgrade or wait for the next billing cycle.
403API key not authorized for this schemaYou're using a scoped key that doesn't include this schema. Use an unscoped key or update the key's scopes.
500Extraction failedUnexpected error during processing. Retry once — if it persists, the document may be corrupted or unreadable.

Retry logic

async function extractWithRetry(file, schema, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch('https://dokyumi.com/api/v1/extract', {
      method: 'POST',
      headers: { 'Authorization': 'Bearer dk_live_...' },
      body: buildFormData(file, schema)
    })

    if (res.ok) return await res.json()

    const err = await res.json().catch(() => ({ error: 'HTTP ' + res.status }))

    // A rate-limit 429 is transient. A quota_exceeded 429 is not.
    if (res.status === 429 && err.code === 'rate_limited') {
      if (attempt === maxRetries - 1) throw new Error(err.error)
      const retryAfterSeconds = Number(res.headers.get('Retry-After'))
      const delayMs = Number.isFinite(retryAfterSeconds)
        ? Math.max(1000, retryAfterSeconds * 1000)
        : 1000 * Math.pow(2, attempt)
      await new Promise(r => setTimeout(r, delayMs))
      continue
    }

    // Other 4xx responses, including 402 and quota_exceeded 429, need caller action.
    if (res.status >= 400 && res.status < 500) {
      throw new Error(err.error)
    }

    // Retry 5xx with exponential backoff
    if (attempt < maxRetries - 1) {
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)))
    }
  }
  throw new Error('Max retries exceeded')
}

Rate Limits

PlanCredits/moSchemasWhite-label sites
Free2521
Starter $99/mo500105
Growth $499/mo3,0005025
Enterprise customCustomUnlimitedUnlimited

One credit covers a document of up to 5 pages; longer documents consume one additional credit per 5 pages, reported as meta.credits_used on the response. Self-serve plans reject documents over 50 pages with 413 page_limit_exceeded. Enterprise page limits follow the organization's configured contract limit, and 413 applies only when that configured limit is exceeded. Reserved customer credits are refunded, so a rejected request consumes no customer credit. Monthly credit limits apply per organization. New API keys default to 60 requests per minute; the configured limit on each key applies. Exceeding the active limiter returns 429 rate_limited with a Retry-After header. Need a different configured limit? Contact us.

File Requirements

Accepted formats

  • PDF (.pdf)
  • JPEG (.jpg, .jpeg)
  • PNG (.png)
  • WEBP (.webp)
  • TIFF (.tiff)

Size & quality

  • Max file size: 20MB
  • Recommended image resolution: 150+ DPI
  • Multi-page PDFs: supported
  • Scanned documents: supported
  • Handwritten text: limited accuracy

OCR modes

Schemas can be configured in two OCR modes (set per-schema in your dashboard):

ModePipelineBest for
standardMistral OCR → ClaudeMost documents. Repeat files can reuse cached OCR.
visionClaude Vision (direct)Complex layouts, charts, mixed content where visual context matters.
OCR caching: In standard mode, Dokyumi caches OCR results by file hash. If you extract the same document twice (same bytes), the OCR step is skipped on the second call — reducing OCR work and often improving response time. Each extraction still consumes credits. The meta.ocr_cached field tells you when this fires.
GET

/api/v1/schemas

List all active schemas available to your API key. Useful for dynamic schema selection or verifying which schemas a scoped key can access.

curl https://dokyumi.com/api/v1/schemas \
  -H "Authorization: Bearer dk_live_your_api_key"
// Response
{
  "schemas": [
    {
      "slug": "invoice-parser",
      "name": "Invoice Parser",
      "description": "Extracts vendor, amounts, and line items from invoices",
      "ocr_mode": "standard",
      "fields": [
        { "key": "vendor_name",    "type": "string",   "required": true },
        { "key": "invoice_number", "type": "string",   "required": true },
        { "key": "total_amount",   "type": "currency", "required": true },
        { "key": "line_items",     "type": "array",    "required": false }
      ],
      "extraction_count": 142
    }
  ]
}

Webhooks

Webhooks fire when a document is submitted through an upload site. The current allowances are 1 site on Free, 5 on Starter, and 25 on Growth; Enterprise limits are contract-specific. Configure your webhook URL in Sites → Settings.

Dokyumi sends a POST request to your URL. When the upload site has a webhook secret, X-Dokyumi-Signature contains a lowercase 64-character HMAC-SHA256 hex digest. A site without a provisioned secret sends that header empty, so provision and store a secret before relying on signed delivery, and reject unsigned requests at your receiver.

Webhook payload

// Headers
X-Dokyumi-Signature: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
X-Dokyumi-Event: extraction.completed
Content-Type: application/json

// Body
{
  "event": "extraction.completed",
  "extraction_id": "6f8c2d4a-7b31-4e95-9a20-c1d7f6b84210",
  "site_id": "2782c10f-9104-464b-8021-d0359f4df3f2",
  "timestamp": "2026-03-15T04:15:00.000Z",
  "data": {
    // Your extracted fields — same as the data object in the API response
    "vendor_name": "Acme Corp",
    "total_amount": 1250.00
  }
}

Verifying signatures

Node.js

const crypto = require('crypto')

function verifyWebhook(rawBody, signature, secret) {
  if (!secret || typeof signature !== 'string' || !/^[0-9a-f]{64}$/.test(signature)) {
    return false
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest()

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    expected
  )
}

// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-dokyumi-signature']
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature')
  }
  const payload = JSON.parse(req.body)
  // Handle payload.data
  res.sendStatus(200)
})

Python

import hmac
import hashlib
import re

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    if not secret or not re.fullmatch(r"[0-9a-f]{64}", signature or ""):
        return False
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# FastAPI example
@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-dokyumi-signature", "")
    if not verify_webhook(raw, sig, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")
    payload = json.loads(raw)
    return {"ok": True}
Delivery & retries:Dokyumi delivers each webhook once inline when the extraction completes. If your endpoint is briefly down or returns a non-2xx response, a background job automatically retries delivery — up to 6 total attempts, roughly every 10 minutes — until it succeeds or the attempts are exhausted. Your site's submission history shows whether delivery completed or is still pending. Make your receiving endpoint resilient (respond quickly with a 2xx and process asynchronously) so transient failures do not drop events.

Something missing from these docs?

Email hello@dokyumi.com — we usually respond same day.