Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.
Upload a document or image and KINDI returns the same file with each detected entity removed in place; same detection as /api/v1/redact, but for whole files.
Supported formats:
  • Documents: PDF, DOCX, XLSX, PPTX, CSV
  • Images: PNG, JPEG, TIFF
Scanned PDFs and images are OCR'd automatically, and PII inside images embedded in a document is detected and removed too.
File redaction is async: the upload returns a file_id immediately, a background worker processes the document, and you poll until it's ready, then download.
Every redacted file is independently re-verified before it's marked ready: KINDI re-extracts the output with a separate pipeline and checks that no must-redact PII survived. If verification can't prove the file is clean, the job fails closed (redaction_incomplete / verify_failed) rather than handing back a file that might still contain PII.

1. Upload

curl -s -X POST https://api.kindi.me/api/v1/files/redact \ -H "Authorization: Bearer $KINDI_KEY" \ -F "file=@patient-notes.pdf;type=application/pdf"
Response (201 Created):
{ "file_id": "f8a9b1c2-...", "state": "queued", "created_at": "2026-06-04T09:30:00Z", "expires_at": "2026-06-05T09:30:00Z", "poll_url": "/api/v1/files/f8a9b1c2-..." }

Mask your own terms too

The upload accepts the same glossary inputs as the text endpoints, as multipart form fields: glossary (a comma-separated list of inline terms) and/or glossary_id (a persisted glossary):
curl -s -X POST https://api.kindi.me/api/v1/files/redact \ -H "Authorization: Bearer $KINDI_KEY" \ -F "file=@contract.pdf;type=application/pdf" \ -F "glossary=Project Falcon,Bluebird" \ -F "glossary_id=f8a9b1c2-..."

2. Poll until ready

Poll the poll_url until state is ready or failed.
curl -s https://api.kindi.me/api/v1/files/$FILE_ID \ -H "Authorization: Bearer $KINDI_KEY"
{ "file_id": "f8a9b1c2-...", "state": "ready", "word_count": 128, "entity_count": 6, "expires_at": "2026-06-05T09:30:00Z", "download_url": "/api/v1/files/f8a9b1c2-.../download" }
state moves through queuedprocessingready, and settles in one of three terminal states:
stateMeaning
readyRedacted and independently re-verified; download_url is live.
failedThe job stopped; see error_code below. Nothing is returned.
expiredThe TTL elapsed. The redacted bytes are destroyed, but the row is kept for a while so you get a clear answer instead of a bare 404. Downloading returns 410 Gone with {"detail": "expired"}. Re-upload to get the file again.
On failed, an error_code explains why:
error_codeMeaning
unsupported_formatNot one of the supported types, or a corrupt / password-protected file.
redaction_incompletePII survived and couldn't be safely re-masked; failed closed.
verify_failedThe output couldn't be re-verified as clean; failed closed.
image_redact_failed / embedded_image_failedAn image (standalone or embedded) couldn't be processed.
too_many_pages / oversized_image / oversized_uncompressedA per-job limit was exceeded.
insufficient_tokensBalance exhausted (the job is not charged).
masker_unavailable / internal_errorTransient processing error; safe to retry.

3. Download the redacted file

curl -s -o redacted.pdf \ https://api.kindi.me/api/v1/files/$FILE_ID/download \ -H "Authorization: Bearer $KINDI_KEY"
The download streams back the same format you uploaded, with the PII removed in place and the document's author/title metadata stripped. Any of your keys can download a file you own.
Download responses other than 200:
StatusMeaning
404 not_readyThe job hasn't reached ready yet (or already failed). Keep polling.
410 expiredThe TTL elapsed and the bytes were destroyed. Re-upload.
404The file id isn't yours, or was deleted.

Manage stored files

Redacted files live for 24 hours then are deleted. Pin one to keep it longer, or delete it immediately.
MethodPathEffect
GET/api/v1/filesList your files (newest first)
POST/api/v1/files/{id}/pinKeep beyond the 24h TTL
POST/api/v1/files/{id}/unpinRestore the TTL
DELETE/api/v1/files/{id}Remove the file + its stored bytes now

Limits

  • 100 MB max upload (oversized uploads are rejected up front with 413 file_too_large)
  • 24-hour TTL (pin to extend)
  • 50 active files / 5 pinned per account
  • 2 concurrent jobs per account
  • 100 uploads per user per Asia/Riyadh day (429 too_many_files_today when exceeded). Every upload you made that day counts, including ones that were cancelled, failed, or have since expired. Resets at Riyadh midnight, the same boundary as the free-token quota.
Billing is metered by the document's word count, same rate as the text endpoints. A failed job is refunded automatically. Playground keys are exempt.

Two ways file redaction differs from the text API

Both differences are deliberate, and both make files redact more, not less.
Files are checksum-lenient. On /api/v1/mask and /api/v1/redact, an identifier that fails its check digit is dropped (see PII types). Inside a file it is still redacted. OCR routinely mangles one digit of a national ID or IBAN, and a mangled-but-obviously-an-ID number is exactly the thing you don't want surviving into a shared document, so the file path errs toward redacting.
Your entity toggles do not apply to files. The enabled-entity settings govern /mask and /redact only. File jobs always run the full entity set, so a document will have its ORGANIZATION, MRN, MONETARY_AMOUNT, and every other extended type redacted even if those types are switched off for your text calls.