Document forensics API

Submit one original. Get source-bound forensic findings, metadata and a private PDF report. The API uses the same product balance and document history as the web app.

Manage API keysOpenAPI 3.1 specification

Production API activation is pending release. These are the implemented v1 endpoints; use your configured local or preview origin for development.

1. Create your account and key

Create an account with email and password. Account creation is free. A paid Professional subscription ($99/month, 1,000 documents) enables new API analyses. Create a key in Developer access and store it on your server. Keys are shown once, expire after 90 days, and can be revoked immediately. Never place keys in browser code.

2. Submit and inspect

Send a single, unencrypted PDF up to 3 MiB (3,145,728 bytes) and 40 pages. Preserve the original bytes. Metadata is always examined; there are no capability, tier or product selectors. Archives and multiple files are rejected.

# Keep this key in your server's secret store.
export ULTIWRITE_API_KEY='your-key'
export ULTIWRITE_ORIGIN='https://ultiwrite.co'

# Run once per document. Keep this file; reuse it after a lost response.
node -e "require('node:fs').writeFileSync('original.request-key', require('node:crypto').randomUUID(), {flag:'wx'})"
export ULTIWRITE_REQUEST_KEY="$(cat original.request-key)"

curl "$ULTIWRITE_ORIGIN/api/v1/analyses" \
  -H "Authorization: Bearer $ULTIWRITE_API_KEY" \
  -H "Idempotency-Key: $ULTIWRITE_REQUEST_KEY" \
  -F 'file=@original.pdf'

# Use the id returned by submission. Poll every 10 seconds.
curl "$ULTIWRITE_ORIGIN/api/v1/analyses/JOB_UUID" \
  -H "Authorization: Bearer $ULTIWRITE_API_KEY"

# Once status is completed:
curl "$ULTIWRITE_ORIGIN/api/v1/analyses/JOB_UUID/report.pdf" \
  -H "Authorization: Bearer $ULTIWRITE_API_KEY" \
  --fail --output forensic-report.pdf

A successful submission returns HTTP 202 with { "id": "…", "status_url": "/api/v1/analyses/…" }. Save the ID. Poll its status URL every 10 seconds. A completed response includes report with document hash, examination time, actual engine/profile versions, metadata, evidence-bound findings, completed coverage and file-specific limitations.

3. Handle durable state and retries

Jobs move through queued, running and completed or failed. Billing status is separate: reserved, settled or released. A quiet examination still uses one unit when required checks completed. Unsupported or failed processing never counts as a completed examination. Partial and unsupported checks remain visible in coverage; zero findings does not certify authenticity.

Persist a UUID Idempotency-Key before sending. If the connection drops, submit exactly the same bytes with that key. The existing job is returned without reserving again, including after its last unit was used. Different bytes with the same key return 409. A timeout keeps the reservation while the durable worker reconciles authoritative engine state.

Usage and limits

GET /api/v1/usage returns available and reserved units, API execution eligibility and current limits. Web and API submissions share a single balance. No automatic overages. Prepaid top-ups can supplement Professional usage. Subscription cancellation keeps the paid period; failed renewal grants nothing. Expired plans stop new API work; unrevoked, unexpired keys can still access saved reports without a new charge.

60 requests per minute across all keys in an account. At most three outstanding reserved API analyses; the canonical product queue has its own concurrency budget. A 429 response includes Retry-After. A 402 means more usage is required; 403 means Professional execution eligibility is missing; 401 means invalid, expired or revoked key; 404 also covers another account’s ID; 409 means a PDF is not ready or an idempotency conflict; 413/415/422 reject invalid input. A 503 means the service or required integration is unavailable; reuse your saved key after recovery.

Node.js

// Node.js 22+, on your server. Save requestKey before sending.
import { readFile } from 'node:fs/promises';
// Reuse the original.request-key file generated in the setup above.
const savedRequestKey = (await readFile('original.request-key', 'utf8')).trim();
const origin = process.env.ULTIWRITE_ORIGIN ?? 'https://ultiwrite.co';
const headers = { Authorization: 'Bearer ' + process.env.ULTIWRITE_API_KEY };
const form = new FormData();
form.set('file', new File([await readFile('original.pdf')], 'original.pdf'));
const response = await fetch(origin + '/api/v1/analyses', {
  method: 'POST', headers: { ...headers, 'Idempotency-Key': savedRequestKey },
  body: form,
});
if (!response.ok) throw new Error(await response.text());
const { id } = await response.json();
// Persist id. Polling and downloading never consume another unit.
const status = await fetch(origin + '/api/v1/analyses/' + id, { headers });
const analysis = await status.json();

Python

# pip install requests
import os, requests
from pathlib import Path
origin = os.environ.get('ULTIWRITE_ORIGIN', 'https://ultiwrite.co')
headers = {'Authorization': 'Bearer ' + os.environ['ULTIWRITE_API_KEY']}
# Reuse the original.request-key file generated in the setup above.
request_key = Path('original.request-key').read_text().strip()
with open('original.pdf', 'rb') as original:
    response = requests.post(origin + '/api/v1/analyses',
        headers={**headers, 'Idempotency-Key': request_key},
        files={'file': ('original.pdf', original, 'application/pdf')}, timeout=45)
response.raise_for_status()
job_id = response.json()['id']
status = requests.get(origin + '/api/v1/analyses/' + job_id,
    headers=headers, timeout=30)
status.raise_for_status()

History, cancellation and deletion

GET /api/v1/analyses?limit=50 lists your most recent documents (1–100). DELETE /api/v1/analyses/ID durably queues cancellation and deletion; poll until 404. Pending usage is released after canonical acknowledgement. Completed work remains settled. Source and report retention targets and verification limits are described in the Data Retention & Security policy; minimal billing/audit records remain. Deletion covers application and engine copies, including races with active work.

There are no expiring public artifact URLs to manage. Downloads check ownership each time. Browser sessions are separate from API keys, and this API never redirects to the main platform. Current v1 completion delivery uses polling; customer-supplied callback destinations are not accepted.