Skip to content

Documents API

The Documents API exposes the lifecycle of asynchronously-submitted PDF redactions. When a PDF is submitted to POST /api/filter without ?async=false, Philter persists the request, returns 202 Accepted with a documentId, and processes it on a background worker. Use the endpoints below to track and retrieve the result.

Admin cross-user access: by default each endpoint operates on the calling user's own documents. An admin may target another user by adding an owner=<username> query parameter to any endpoint (list, status, download, delete). A non-admin that names another user as owner, or an owner that does not exist, receives 404 Not Found. Cross-user access is disabled by default; enable it with ADMIN_CROSS_USER_ACCESS_ENABLED=true (see Settings). While disabled, naming another user as owner also returns 404 Not Found. A deactivated user may be named as owner: deactivation keeps their data, and an admin reaches it as for an active user.

Philter serves HTTPS on port 8080 with a generated self-signed certificate by default, so the curl examples on this page pass -k. See the TLS settings to supply your own certificate.

How the queue is worked

A background worker claims pending documents and redacts them one after another, draining everything queued before it waits again. The wait between passes is philter.worker.poll-interval-ms (default 5,000ms), which is how long an idle worker sleeps rather than a limit on throughput: a backlog is worked through as fast as redaction runs, not one document per interval. The worker has its own thread, so a long document does not delay webhook delivery. A document that repeatedly kills the worker during computation is requeued at most three times, then failed if it is abandoned again.

Claims and recovery

Each attempt receives a unique claim token and a ten-minute lease. A separate heartbeat renews the lease every minute while the worker computes the redaction. Another instance can reclaim an expired computation; the old attempt can no longer publish completed-result evidence, store output, change the job to failed, or enqueue a completion/failure webhook. These guarantees are kept by the database and, with MongoDB storage, apply across instances regardless of the cache backend.

After computation, the worker atomically reserves publication before writing the ledger, completion metrics, or completion audit event. The job remains PROCESSING, with publication_started_at recorded in pending_documents. This reservation does not expire: automatic takeover could race evidence writes already issued by the old worker. A successful or failed terminal update must match the attempt token and processing state. An accepted terminal update atomically records notification_pending=true. A reconciler retries enqueueing after failures or restarts, including jobs failed by exhausted reclaim. The job ID is the delivery ID, so retrying an interrupted acknowledgement does not create another delivery.

A crash during publication leaves the job PROCESSING and requires operator recovery. Such jobs do not have completed_at, so their input does not expire under the finished-job TTL. To recover:

  1. Inspect the job's _id, claim_token, claimed_by, and publication_started_at. Stop the worker process that could resume that attempt and confirm its outstanding database commands have finished.
  2. Review the document's ledger chain, completion audit events, stored result, and webhook records. Evidence may be partial or complete even though the job still reports PROCESSING.
  3. Once the attempt is quiescent, conditionally mark the record FAILED, matching its observed _id, claim_token, status: PROCESSING, and publication timestamp. Set completed_at, notification_pending=true, and a recovery error, and remove input and input_encrypted_key. The reconciler sets retention_at after accepting the notification intent; do not set it during recovery. Preserve existing ledger evidence for review.
  4. If redaction is still needed, submit a new job with a new document ID. Do not clear the publication reservation and automatically replay the old document ID: its ledger or notifications may already exist.

Claim fencing governs job results and completed-result publication. Context mapping and disambiguation updates made during computation are not rolled back when an attempt loses ownership.

Statuses

A submitted document moves through one of the following statuses:

Status Meaning
PENDING Submitted, waiting for a worker to claim it.
PROCESSING Claimed by a worker and being redacted.
COMPLETE Redaction finished; the redacted bytes are available for download.
FAILED Redaction did not complete; error field in the status response explains it.

Terminal records are retained for PENDING_DOCUMENTS_TTL_SECONDS (default 7 days) from retention_at, set when notification enqueueing is acknowledged. Undispatched intents do not expire and deletion returns HTTP 409 with Retry-After: 5 while an intent is pending.

What Philter keeps, and for how long

An asynchronous redaction is the only place Philter keeps a whole document. The submitted document, the redacted document, and the configuration captured for the job are encrypted at rest under PHILTER_ENCRYPTION_KEY; the status, error message, file name, and policy name are not. Each part is removed at a defined point:

Part Kept until
The submitted document The job completes or fails. A job whose worker stops is recovered and retried; after three recoveries it fails, and its document is removed the same way.
The redacted document You delete it with DELETE /api/documents/{documentId}, or its record expires. Downloading it does not remove it.
The record: status, error, file name, policy, and the configuration captured for the job You delete it, or PENDING_DOCUMENTS_TTL_SECONDS (default 7 days) after the job finished and its notification was queued.

Deleting a document removes its whole record at once, including a job that has not run yet. A document that is processing cannot be deleted until it completes or fails. Expiry is applied by a TTL index with MongoDB, whose background task runs about once a minute, and by a check every minute with SQLite, so a record can outlast its retention period by a short time, longer when the database is busy. Download the result and delete it as soon as you have it to keep it for the least time.

When the document's context has the redaction ledger on, the ledger also records the original value of each redacted span, encrypted. Deleting the document or its expiry does not remove those entries; see How and When Ledger Entries Are Deleted.

List Documents

Method Endpoint Description
GET /api/documents Paginated list of submissions for the calling user.

Query Parameters

  • offset (optional, default 0)
  • limit (optional, default 25, max 100)
  • q (optional) - Only documents whose document ID or file name contains this value, ignoring case. Use it to find a document by the file name it was submitted with. See Listings.
  • status (optional) - Only documents with this status: PENDING, PROCESSING, COMPLETE, or FAILED, ignoring case. Any other value is refused with 400 Bad Request.
  • sort (optional, default submitted) - submitted, newest first by default, or fileName. With order (asc or desc); see Listings.

Returns the documents on the requested page in documents, whatever their status, and total, how many match across every page. Each document's type says what the job produces: redaction, a redacted document, is the only type.

curl -k -H "Authorization: Bearer <token>" \
  "https://localhost:8080/api/documents?status=failed&offset=0&limit=25"
{
  "documents": [
    {
      "type": "redaction",
      "fileName": "patient-record.pdf",
      "status": "COMPLETE",
      "timestamp": "2026-05-22T20:00:00.000Z",
      "documentId": "c0c2c5a8-3a78-4e56-bf2a-44ad8b3a8e9f",
      "policyName": "hipaa",
      "policyVersion": 3,
      "policyContentHash": "9f2c..."
    }
  ],
  "total": 1
}

Get Status

Method Endpoint Description
GET /api/documents/{documentId}/status Current status of a single document.
curl -k -H "Authorization: Bearer <token>" \
  https://localhost:8080/api/documents/c0c2c5a8-3a78-4e56-bf2a-44ad8b3a8e9f/status
{
  "type": "redaction",
  "documentId": "c0c2c5a8-3a78-4e56-bf2a-44ad8b3a8e9f",
  "status": "PROCESSING",
  "effectiveConfigurationHash": "4b1e...",
  "policyName": "hipaa",
  "policyVersion": 3,
  "policyContentHash": "9f2c..."
}

type is the job's type, as in the listing. policyName, policyVersion, and policyContentHash name the policy the document was submitted with, and the same values are in the listing; see Which policy redacted a document.

Returns 404 Not Found if no document with that id exists for the calling user.

Download

Method Endpoint Description
GET /api/documents/{documentId} Download the redacted bytes.
Response Meaning
200 OK Redacted bytes. Content-Type matches the requested output (application/pdf or application/zip).
409 Conflict The document exists but the redaction has not yet completed. Poll the status endpoint and retry.
410 Gone The redaction failed. Inspect the status response (or webhook) for the error message.
404 Not Found No document with that id exists for the calling user (or it has been TTL-evicted).
curl -k -H "Authorization: Bearer <token>" \
  -o redacted.pdf \
  https://localhost:8080/api/documents/c0c2c5a8-3a78-4e56-bf2a-44ad8b3a8e9f

Delete

Method Endpoint Description
DELETE /api/documents/{documentId} Remove the record and any stored redacted bytes.

Returns 204 No Content on success, 404 Not Found if no document with that id exists for the calling user, and 409 Conflict with Retry-After: 5 while the document is PROCESSING or its webhook notification has not yet been sent. A PENDING document, which no worker has started, can be deleted.

curl -X DELETE -k -H "Authorization: Bearer <token>" \
  https://localhost:8080/api/documents/c0c2c5a8-3a78-4e56-bf2a-44ad8b3a8e9f

Which policy redacted a document

Philter reports the policy's name, revision, and content hash with every redaction and keeps every revision of every policy, so you can find the policy behind a redaction and read it as it was. For a synchronous redaction, keep the response headers or use a context with a ledger: without a ledger, Philter keeps no record of which policy it used.

  1. Find the policy and revision. Each /api/filter response, synchronous or the asynchronous 202, carries X-Philter-Policy-Name, X-Philter-Policy-Version, and X-Philter-Policy-Hash. For an asynchronous document, the status and listing responses carry the same values as policyName, policyVersion, and policyContentHash while the job is retained. When the context records a ledger, each entry carries them too, for as long as the ledger is kept.
  2. Read that revision with GET /api/policies/{name}/versions/{revision}. Revisions are kept after the policy is changed or deleted, and policyContentHash matches the revision's contentHash.

The policy revision does not include the custom-list contents, always/never-redact lists, or context settings in force at the time; those are not kept once the job is removed.

Notifications

If a webhook URL and secret are configured for your user, Philter sends a signed POST when each async redaction reaches COMPLETE or FAILED. See the Webhooks page for the headers, payload shape, and replay-protected signing scheme.

Configuration captured at submission

Async submission freezes the resolved policy, custom-list contents, managed FPE key/tweak, the values of env: keys, always/never-redact lists, and context settings, so a policy change after submission does not change a queued job. This configuration is stored, encrypted, only with the job, and is removed with it. X-Effective-Configuration-SHA256 in the 202 response, effectiveConfigurationHash in status, and effectiveHash in ledger entries are its SHA-256. The ledger hash binds this fingerprint, but once the job is gone the configuration itself cannot be recovered from it. The policy remains identifiable; see below. Context mappings, vector contents, and execution software remain live; this is a configuration guarantee, not a promise of byte-identical replay. Cached global lists are captured as observed at submission.

Admission limits active jobs and input bytes globally and per account; see queue settings. Owner limits return 429, and global limits or admission contention return 503, with Retry-After: 5.

A failed status response includes error. A failure you can act on, such as a policy Philter cannot load or a context that no longer exists, keeps its message. Any other failure, such as a document that cannot be parsed or a model service that cannot be reached, is reported as The redaction failed. An administrator can find the cause in Philter's log by the document ID., because its underlying message can name internal hosts and other details of the deployment. Philter logs the full error with the document ID. For example: {"documentId":"example","status":"FAILED","error":"The redaction failed. An administrator can find the cause in Philter's log by the document ID."}. Status also returns the captured effectiveConfigurationHash when present. Deletion returns 409 with Retry-After: 5 while the document is processing or notification reconciliation is pending, in addition to the 204 and 404 outcomes above.