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.
Create an account
Sign up at dokyumi.com/dashboard. Free tier includes 25 extraction credits/month — no card required.
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.
Generate an API key
Go to API Keys → New Key. Copy it now — it won't be shown again.
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"
Handle the response
Check status —completed 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
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.
/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.
| Field | Type | Required | Description |
|---|---|---|---|
| file | File | Yes | PDF, PNG, JPG, TIFF, or WEBP. Max 20MB. |
| schema | String | No | Schema 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"
}
}| Field | Type | Description |
|---|---|---|
| id | string | Unique extraction ID in UUID format. |
| status | string | completed · review. Failures use a non-2xx error envelope. |
| request_id | string | Request correlation ID, also returned in the X-Request-Id header. |
| schema | string | The schema slug selected for the extraction. |
| data | object | Model-produced fields generated against your schema. Inspect validation before treating the data as schema-valid. |
| confidence | object | Model-reported confidence scores (0.0–1.0). The map may omit fields; reported scores below the schema threshold contribute to review status. |
| validation.valid | boolean | Whether the extracted data passed schema type validation. |
| validation.errors | array | Zod validation issues if valid is false. |
| meta.credits_used | number | Credits charged for this extraction based on page count. |
| meta.ocr_cached | boolean | If 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 })
}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"
}| Status | Error message | Cause & fix |
|---|---|---|
| 400 | Missing file | No file was attached. Add a file field to your multipart request. |
| 400 | No schema found | The schema slug doesn't exist or is inactive. Check the slug in your dashboard. |
| 401 | Missing or invalid API key | The Authorization header is missing or doesn't start with Bearer dk_live_. |
| 401 | Invalid API key | Key not found or revoked. Generate a new key in your dashboard. |
| 402 | Not 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. |
| 429 | Monthly extraction limit reached (quota_exceeded) | You've hit your plan's monthly limit. Upgrade or wait for the next billing cycle. |
| 403 | API key not authorized for this schema | You're using a scoped key that doesn't include this schema. Use an unscoped key or update the key's scopes. |
| 500 | Extraction failed | Unexpected 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
| Plan | Credits/mo | Schemas | White-label sites |
|---|---|---|---|
| Free | 25 | 2 | 1 |
| Starter $99/mo | 500 | 10 | 5 |
| Growth $499/mo | 3,000 | 50 | 25 |
| Enterprise custom | Custom | Unlimited | Unlimited |
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):
| Mode | Pipeline | Best for |
|---|---|---|
| standard | Mistral OCR → Claude | Most documents. Repeat files can reuse cached OCR. |
| vision | Claude Vision (direct) | Complex layouts, charts, mixed content where visual context matters. |
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./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}Something missing from these docs?
Email hello@dokyumi.com — we usually respond same day.