API
Philter has no built-in user interface: everything, including administration, is done through its API. A user interface signs people in with their own username and password through the Sign-in API and uses a session key for each person, as Philter UI, in development, is being built to; see Sign-in Security.
Philter's API has the following sections:
- Redaction API - Submit text or PDFs for redaction. In Philter 4.0, PDF requests are asynchronous by default; the redacted bytes are downloaded via the Documents API.
- Documents API - List, poll, download, and delete asynchronous PDF redactions.
- Webhooks - Receive signed HTTP notifications when an asynchronous redaction completes or fails.
- Policies API - Create, modify, and delete policies.
- Custom Lists API - Create, modify, and delete custom lists. Custom lists are referenced by policies to identify terms to redact.
- Always/Never Redact Lists API - Get, replace (POST), and append to (PUT) the account's always-redact and never-redact lists, applied across all of your policies.
- Contexts API - Create, update, and delete contexts and inspect their entries. Contexts maintain referential integrity across documents.
- Redaction Ledger API - List, read, verify, export, and delete redaction ledger chains.
- Legal Holds API - Set, list, and release legal holds that block deletion of evidence.
- Users API - Create, list, promote, deactivate, and reactivate users, and manage their passwords.
- Sign-in API - Exchange a username and password for a session key. Disabled by default.
- API Keys API - Create, re-scope, and revoke API keys.
- Limits API - Read the limits and rules Philter enforces, and what the caller may do.
- Settings API - Read and change the deployment's administrator settings.
- Audit API - List and export the audit log.
Listings
Every endpoint that lists things returns an object with the items on the requested page and total, how many items the listing has across every page, so a client can show a count and page through without guessing:
{
"contexts": [
{ "name": "default", "createdAt": "2026-10-01T12:00:00.000Z" },
{ "name": "claims-2025", "createdAt": "2026-10-05T10:20:30.000Z" }
],
"total": 2
}
Listings share these query parameters:
offset(default0) - How many items to skip.limit(default25, at most100) - How many items to return. A larger value is reduced to100.sort- The field to order by, from the listing's fields in the table below. An unknown field is refused with400 Bad Requestnaming the fields it accepts.order-ascordesc. Left out, the listing's default sort keeps its default direction, and any other sort is ascending. Anything else is refused with400 Bad Request.q- Where a listing supports it, only items whose field (see the table) containsq, ignoring case.qis matched as plain text, not as a pattern.
Items with the same value for the sort field are kept in a stable order, so paging does not skip or repeat them. total counts the items that match q and any filters.
| Listing | Items | sort (default first) |
q searches |
Other filters |
|---|---|---|---|---|
GET /api/policies |
policies |
name, created, updated |
name | all_users, managed, deleted (the managed and deleted listings sort by name only) |
GET /api/policies/{policyName}/versions |
versions |
revision (newest first) |
||
GET /api/contexts |
contexts |
name, created |
name | all_users |
GET /api/contexts/{name}/entries |
entries |
created (newest first), reads |
||
GET /api/lists |
lists |
name |
name | all_users |
GET /api/holds |
holds |
set (newest first), reference |
reference | all_users |
GET /api/documents |
documents |
submitted (newest first), fileName |
document id or file name | status |
GET /api/ledger/{documentId} |
entries |
chain order only | ||
GET /api/ledger |
chains |
created (newest first), filename |
document id or file name | all_users |
GET /api/users |
users |
username, created |
username | role, active |
GET /api/api-keys and GET /api/users/{username}/api-keys |
apiKeys |
created (oldest first) |
session |
|
GET /api/audit |
events |
timestamp (newest first), by order only |
event, from, to, owner |
|
GET /api/pii-counts |
counts |
day (newest first), context, documents |
from, to, context, owner |
|
GET /api/webhook/deliveries |
deliveries |
created (newest first), by order only |
Errors
Every error Philter returns under /api is a JSON object with the same fields, whatever refused the request and whatever the request's Accept header asked for:
{
"message": "This API key does not have the 'policies:read' scope.",
"reason": "missing_scope"
}
message- What happened, written for a person to read. It may change between releases; do not match on it.reason- Why the request was refused, as a stable code. Use it in code to decide what to do.field- Present when a request is refused as invalid and the invalid parameter or body field is known, such assortorpassword.
A refusal with a specific reason gives it, as listed below. Any other error gives the reason its status implies: invalid_request (400), invalid_credentials (401), forbidden (403), not_found (404), method_not_allowed (405), not_acceptable (406), conflict (409), payload_too_large (413), unsupported_media_type (415), rate_limited (429), service_unavailable (503), or internal_error (500).
Request parameters that cannot be parsed are refused on any endpoint with 400 Bad Request and invalid_request: a parameter with no name (?=) in the query string or a form body, or more than 1,000 parameters. A form body too large to parse is refused with 413 and payload_too_large.
| Status | reason |
Meaning |
|---|---|---|
| 401 | invalid_credentials |
The API key is missing, malformed, or unknown, or is a revoked long-lived key. Sign-in also gives it for a wrong username or password, without saying which. |
| 401 | session_expired |
The session key has ended: it passed its idle timeout or maximum lifetime, was revoked, or signed out. Sign in again. |
| 401 | user_deactivated |
The API key's user is deactivated. |
| 403 | missing_scope |
The API key does not hold the scope the endpoint requires. |
| 403 | admin_required |
The operation requires an administrator. |
| 403 | feature_disabled |
The operation is turned off in this deployment, such as ledger deletion. |
| 403 | password_change_required |
The session key may only change its user's password until it is changed. |
| 403 | mfa_enrollment_required |
The session key may only enroll its user in MFA until enrollment is confirmed. |
| 403 | wrong_password |
The current password is not correct. |
| 400, 403 | invalid_code |
The MFA code is not valid: 400 when confirming enrollment, 403 when removing it. |
| 403 | mfa_locked |
The user's MFA is locked after repeated bad codes; an administrator must unlock it. |
| 403 | scope_not_held |
The calling key cannot create a key, widen a key's scopes, or grant scopes it does not hold. |
| 404 | not_found |
Nothing with that name, id, or path exists, or the owner does not exist or may not be reached. The two read the same, so a value cannot be used to discover what exists. |
| 409 | self_action_refused |
The caller cannot do this to their own user or to the key making the request, such as deactivating themselves or revoking the calling key. |
| 409 | last_admin |
The user is the last active administrator. |
| 409 | already_exists |
A user with that username already exists, active or deactivated. |
| 409 | mfa_unavailable, mfa_already_enrolled, mfa_not_enrolled |
MFA is not turned on in this deployment, or the user is already enrolled, or not enrolled. |
| 409 | changed_concurrently |
Another request changed the same thing first, such as the password. Read it again and retry. |
| 409 | admin_change_in_progress |
Another change to the administrators is in progress, such as a role change or deactivation through another instance. Retry-After gives the seconds to wait. MongoDB storage only. |
| 409 | not_a_session_key |
Only a session key can sign itself out. |
| 409 | webhook_not_set |
No webhook is set, so there is nothing to test. |
| 409 | externally_managed |
The signing key is managed outside Philter. |
| 409 | context_in_use |
The context has queued or running redactions. |
| 409 | document_not_ready |
The document is still being redacted. |
| 410 | document_failed |
The document's redaction failed, so there is nothing to download. |
Some refusals carry their own reasons, documented with their endpoints: the policy conflicts (policy_exists, policy_managed, policy_changed, policy_default), context_exists and context_limit_reached, list_exists, hold_exists and operation_in_progress, redact_list_changed, entry_unreadable, and sign-in's locked and rate_limited.
Request ids
Every response under /api carries an X-Request-Id header with an id Philter gives the request, whether the request succeeded or was refused:
HTTP/1.1 404
X-Request-Id: 01929f2e-6c3a-7b1d-9e4f-3a2b1c0d9e8f
Content-Type: application/json
{"message":"Not found.","reason":"not_found"}
Every audit event the request causes records the same id as its request_id, so a client that keeps the id, or a person who quotes it, can find exactly what the request did with GET /api/audit. The exception is redaction itself: document_redaction_initiated and document_redaction_completed record the document id (X-Document-Id, or documentId for an asynchronous PDF) as their request_id, so a queued redaction's start and its later completion share one id with its ledger chain. Philter always makes its own id and does not take one from the request. A request refused by the web server before it reaches Philter, such as one with a malformed path, has no id.
OpenAPI Specification
Philter's API is described by an OpenAPI specification generated from the application's source. The OpenAPI export integration test regenerates it and checks it against the registered routes and the committed copy. Run that test when changing an endpoint; a successful ordinary compilation alone does not refresh the published artifact. You can always find it in any of these places:
- In this documentation: openapi.json. This is the copy built with these pages, from the same source revision, and needs no running instance.
- In the GitHub repository:
docs/docs/api_and_sdks/openapi.json. This is the committed copy, reflecting the latest code onmain. - From a running Philter instance:
https://<your-philter-host>:8080/v3/api-docs. This is the live specification served by that instance, reflecting its version and configuration.
Use the specification to generate an API client for your language (for example with OpenAPI Generator). See Client SDKs for more.
Interactive API Reference (Swagger UI)
Every running Philter instance serves an interactive API reference (Swagger UI) generated from the live OpenAPI specification. It lists every endpoint with its parameters, request and response schemas, and lets you try requests directly from the browser.
- Swagger UI:
https://<your-philter-host>:8080/swagger-ui/index.html
To authorize a request from Swagger UI, send your API key as a bearer token in the Authorization header (see API Keys and Authentication).
Securing Philter's API
Philter serves its API over HTTPS, using a self-signed certificate generated on first start unless you supply your own. See TLS for how to replace the certificate or terminate TLS in front of Philter.
Rate limiting
Philter enforces the limits that depend on who a request is for, which only Philter can see:
- Password guessing. Failed sign-ins and wrong current passwords when changing a password lock the user for a while, and sign-in is limited per client address.
- Asynchronous documents. Queue admission limits active jobs and input bytes per account and overall.
- Request size. Request bodies are capped by
MAX_FILE_SIZE_BYTESfor documents andMAX_FILE_SIZE_BYTES_OTHERfor everything else.
Philter does not limit how many requests a client or API key makes. Put a reverse proxy, load balancer, or API gateway in front of Philter to limit request rates, as you would for any API. Consider tighter per-key limits on the two endpoints that return original values in the clear, so a stolen or misused key cannot reverse a whole dataset quickly:
POST /api/reidentifyandPOST /api/reidentify/text(scopereidentify), where each request can carry as many values, or as much text, as fit inMAX_FILE_SIZE_BYTES_OTHER.GET /api/ledger/{documentId}/export(scopeledger:export).
For example, with NGINX, keyed by the Authorization header so each API key is limited separately:
limit_req_zone $http_authorization zone=philter_reversal:10m rate=10r/m;
location ~ ^/api/reidentify(/text)?$ {
limit_req zone=philter_reversal burst=5 nodelay;
proxy_pass https://philter:8080;
}
location ~ ^/api/ledger/[^/]+/export$ {
limit_req zone=philter_reversal burst=5 nodelay;
proxy_pass https://philter:8080;
}
Grant reidentify and ledger:export only to the keys that need them, and watch the redaction_reversed and redaction_ledger_exported events in the audit log for unexpected volume.
SDKs
The Philter Java SDK provides convenient methods for using Philter's API. For other languages, generate a client from the OpenAPI specification above. See Client SDKs for more information.
Complete endpoint inventory
The endpoint inventory lists every HTTP operation, its required scope, and its detailed reference. Additional API sections include the Redaction Ledger API, Legal Holds API, policy history and rollback, re-identification, and public health and signing keys.