Contexts API
The Contexts API provides endpoints for retrieving, creating, updating, and deleting contexts, and for listing, exporting, and importing the token-to-replacement mappings within a context.
Admin cross-user access: by default each endpoint operates on the calling user's own contexts. Because context names are unique only per user, an admin identifies another user's context by adding an
owner=<username>query parameter. A non-admin that names another user asowner, or anownerthat does not exist, receives404 Not Found. Cross-user access is disabled by default; enable it withADMIN_CROSS_USER_ACCESS_ENABLED=true(see Settings). While disabled, naming another user asowneralso returns404 Not Found. A deactivated user may be named asowner: 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
curlexamples on this page pass-k. See the TLS settings to supply your own certificate.
Get Context Names
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/contexts |
List contexts (paginated). |
Returns the contexts on the requested page in contexts and total, how many there are across every page. See Listings for paging, sorting, and searching. Each item is an object with the context's name and when it was created (createdAt). Its settings and size are returned by Get Context Details.
Query Parameters
offset(optional, default:0) - The number of contexts to skip.limit(optional, default:25, max:100) - The maximum number of contexts to return.q(optional) - Only contexts whose name containsq, ignoring case.sort(optional, default:name) -nameorcreated.order(optional, default:asc) -ascordesc.owner(optional) - The username of the user whose contexts to list. When omitted, the caller's own contexts are listed. Supplying anownerother than yourself requires admin privileges and cross-user access being enabled (ADMIN_CROSS_USER_ACCESS_ENABLED=true; disabled by default); otherwise it receives404 Not Found.all_users(optional, default:false) - List every user's contexts instead of the caller's. Each item then also has itsowner's username. Requires an administrator andADMIN_CROSS_USER_ACCESS_ENABLED=true(disabled by default), asownerdoes; otherwise it returns404 Not Found. Cannot be combined withowner.
Example request:
curl -k -H "Authorization: Bearer <token>" "https://localhost:8080/api/contexts?offset=0&limit=100"
Example response:
{
"contexts": [
{ "name": "default", "createdAt": "2026-10-01T12:00:00.000Z" },
{ "name": "my-context", "createdAt": "2026-10-05T10:20:30.000Z" }
],
"total": 2
}
Get Context Details
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/contexts/{name} |
Get the details of a context, where {name} is the name of the context to get. |
Query Parameters
owner(optional, admin only) - Username of another user whose context to get. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
Example request:
curl -k -H "Authorization: Bearer <token>" https://localhost:8080/api/contexts/my-context
Example response:
{
"size": 125,
"filterTypes": {
"email-address": 40,
"person": 83
},
"untyped": 2,
"entityTypeDisambiguation": true,
"disambiguationScope": "document",
"ledger": false
}
entityTypeDisambiguation- Whether entity type disambiguation is enabled for the context.disambiguationScope-documentorcontext: whether disambiguation learns within each document or across the context. SeedisambiguationScopeunder Create a Context.ledger- Whether the context records redactions in the ledger.size- The number of entries in the context.filterTypes- The number of entries for each filter type, sorted by filter type.untyped- The number of entries with no filter type. Only an import can create one.
The counts are computed in the database in one query and sum to size.
Create a Context
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/contexts |
Create a new context. |
Request Body
A JSON object. The settings use the names Get Context Details returns, and one left out takes its default.
name(required) - The name of the context to create. Context names are unique per user: if you already have a context with this name the request is rejected with409 Conflict. A name you use does not prevent another user from using the same name. Each user can have at most 10 contexts; see Capacity. Because the name is how the context is addressed in a request path, it cannot contain/,\,;,%, or control characters, and cannot be.or...entityTypeDisambiguation(optional, default:false) - Whether to enable entity type disambiguation for this context.disambiguationScope(optional, default:document) - What span disambiguation learns from, whenentityTypeDisambiguationis enabled. Case does not matter.document- Each document is disambiguated on its own, from what Philter learns in that document. Nothing is stored.context- What Philter learns is stored in Philter's database and used for every later document redacted in the context, up toMAX_VECTORS_PER_CONTEXTvectors. Stored vectors are kept if the scope or disambiguation changes, and deleted when the context is emptied or deleted.
ledger(optional, default:false) - Whether to enable the redaction ledger for this context.
The name and settings are not accepted as query parameters, and a request that sends one there is refused with 400.
Query Parameters
owner(optional, admin only) - Username of another user whose context to create the context for. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
Responses
201 Created- The context was created.400 Bad Request- Thenameis missing or blank or breaks the rule above,disambiguationScopeis notdocumentorcontext, or the name or a setting is sent as a query parameter. The body carries amessage, and afieldnaming what was refused.409 Conflict- The context was not created because of your existing contexts. The body'sreasonsays which, so a client need not read the message:context_exists- You already have a context with this name.context_limit_reached- You already have 10 contexts.
{
"message": "Maximum number of contexts reached.",
"reason": "context_limit_reached"
}
Example request:
curl -X POST -k -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
"https://localhost:8080/api/contexts" \
-d '{"name": "my-context", "entityTypeDisambiguation": true, "disambiguationScope": "context"}'
Update a Context
| Method | Endpoint | Description |
|---|---|---|
PATCH |
/api/contexts/{name} |
Change a context's entityTypeDisambiguation, disambiguationScope, and ledger settings. |
Only the settings given change. A setting left out keeps its current value, so changing one never changes another. Read the current values with Get Context Details.
Request Body
A JSON object with the settings to change. The names are those Get Context Details returns.
entityTypeDisambiguation(optional) - Whether to enable entity type disambiguation. Left out, it is unchanged.disambiguationScope(optional) -documentorcontext, as for Create a Context. Left out, it is unchanged.ledger(optional) - Whether to enable the redaction ledger. Left out, it is unchanged; turning it off stops recording redaction evidence for the context.
The settings are not accepted as query parameters, and a request that sends one there is refused with 400.
Query Parameters
owner(optional, admin only) - Username of another user whose context to update. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
Example request:
curl -X PATCH -k -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
"https://localhost:8080/api/contexts/my-context" -d '{"entityTypeDisambiguation": false, "ledger": true}'
Returns 200 OK on success, 400 Bad Request if no setting is given, disambiguationScope is not document or context (nothing changes, including the other settings given), or a setting is sent as a query parameter, and 404 Not Found if no context with that name exists for the calling user. Each change is recorded as a context_updated audit event naming the settings whose values changed.
Delete a Context
| Method | Endpoint | Description |
|---|---|---|
DELETE |
/api/contexts/{name} |
Delete a context, where {name} is the name of the context to delete. |
Query Parameters
owner(optional, admin only) - Username of another user whose context to delete. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
A delete is rejected with 409 Conflict if any asynchronously-submitted document referencing the context is still pending or processing. Either wait for the jobs to finish (poll the Documents API) or delete the pending jobs first.
Responses
204 No Content- The context was deleted.404 Not Found- No context with that name exists for the user (the body'smessageisContext does not exist.), or theownerdoes not exist or may not be reached.409 Conflict- The context has pending or processing asynchronous jobs.
Example request:
curl -X DELETE -k -H "Authorization: Bearer <token>" https://localhost:8080/api/contexts/my-context
List Context Entries
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/contexts/{name}/entries |
Paginated list of token-to-replacement mappings. |
Query Parameters
offset(optional, default:0)limit(optional, default:25, max:100)sort(optional, default:created) -created, newest first by default, orreads. See Listings.order(optional) -ascordesc.owner(optional, admin only) - Username of another user whose context to list entries from. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
Example request:
curl -k -H "Authorization: Bearer <token>" \
"https://localhost:8080/api/contexts/my-context/entries?offset=0&limit=25"
Example response:
{
"entries": [
{
"id": "6a1101d50737f1edfcf70d77",
"replacement": "{{{REDACTED-person}}}",
"filterType": "PERSON",
"reads": 14,
"timestamp": "2026-05-22T20:00:00.000Z"
}
],
"total": 1
}
The original token is never returned by this endpoint; only a keyed hash of it is stored.
Empty a Context
| Method | Endpoint | Description |
|---|---|---|
DELETE |
/api/contexts/{name}/entries |
Remove all entries from a context. |
Emptying a context also deletes the span-disambiguation vectors stored for it under the context disambiguation scope. Its settings are unchanged.
Query Parameters
owner(optional, admin only) - Username of another user whose context to empty. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
Removes every token-to-replacement mapping in the context but leaves the context itself in place. Returns 204 No Content on success, 404 Not Found if the context does not exist.
curl -X DELETE -k -H "Authorization: Bearer <token>" \
https://localhost:8080/api/contexts/my-context/entries
Delete a Single Entry
| Method | Endpoint | Description |
|---|---|---|
DELETE |
/api/contexts/{name}/entries/{entryId} |
Remove one entry by its id. |
Query Parameters
owner(optional, admin only) - Username of another user whose context to delete the entry from. Requires cross-user access to be enabled; otherwise it returns404 Not Found.
entryId is the id returned in the entries listing. Returns 204 No Content if deleted, 404 Not Found if no such entry exists for the calling user, 400 Bad Request if entryId is not a valid id.
curl -X DELETE -k -H "Authorization: Bearer <token>" \
https://localhost:8080/api/contexts/my-context/entries/6a1101d50737f1edfcf70d77
Export a Context's Mapping Table
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/contexts/{name}/entries/export |
Export the complete mapping table for a context as JSON. |
Exports every token-to-replacement mapping in the context, at most MAX_CONTEXT_SIZE entries, in a JSON form that can be re-imported into another of the same user's contexts, in this deployment or another with the same PHILTER_ENCRYPTION_KEY and a user with the same username (see Import below). Imported anywhere else, it will not match the original tokens during redaction.
Authorization: only the user that created the context or an admin may export it. Any other caller receives 404 Not Found, which is also returned when the context does not exist. The endpoint does not reveal the existence of a context you are not allowed to access.
Query Parameters
owner(optional) - The username of the context's owner. Because context names are unique only per user, an admin uses this to identify another user's context unambiguously. When omitted, the export applies to the caller's own context with the given name. Supplying anownerother than yourself requires admin privileges and cross-user access being enabled (ADMIN_CROSS_USER_ACCESS_ENABLED=true; disabled by default); otherwise it receives404 Not Found.
Returns 200 OK with the export document. The response is sent with a Content-Disposition header so it can be saved directly to a file.
Security: the export contains a keyed hash (HMAC-SHA256) of each original token, never the original value, along with its replacement. The key is derived from
PHILTER_ENCRYPTION_KEYand the context owner's username and never leaves the deployment, so the hash cannot be reversed by guessing candidate values, and another user cannot match it by redacting guesses in a context of their own. That matters because most of what Philter detects is low-entropy: an SSN is only 109 possibilities, a date of birth far fewer. A bare digest of one is recoverable by enumeration in seconds. The export still reveals the replacement values, so treat it as sensitive and transmit/store it securely.Portability: because the hash is keyed to the deployment's key and the owner's username, an export matches only in contexts owned by the same username: another of that user's contexts, or a context in another installation with the same
PHILTER_ENCRYPTION_KEYwhose user has that username. Imported into another user's context, the hashes do not match, and the original tokens are not stored, so they cannot be recomputed. An administrator moving a user's mappings imports them into that user's own context, withowner.
Example request:
curl -k -H "Authorization: Bearer <token>" \
"https://localhost:8080/api/contexts/my-context/entries/export" -o my-context-export.json
Example response body:
{
"version": 1,
"context": "my-context",
"count": 2,
"entries": [
{
"tokenHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"replacement": "David Jones",
"filterType": "PERSON",
"replacementUuid": false
},
{
"tokenHash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"replacement": "842-436-2042",
"filterType": "PHONE_NUMBER",
"replacementUuid": false
}
]
}
Import a Mapping Table into a Context
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/contexts/{name}/entries/import |
Import token-to-replacement mappings into a context. |
Accepts a document in the same format produced by the export endpoint and inserts its mappings into the named context. The context must already exist (create it first with POST /api/contexts).
Authorization: only the user that created the context or an admin may import into it. Any other caller receives 404 Not Found.
Query Parameters
on_conflict(optional, default:skip) - What to do when an incoming token already exists in the target context:skip- leave the existing replacement unchanged (preserves replacements already learned in this context).overwrite- replace the existing replacement with the imported one.
owner(optional) - The username of the context's owner. Because context names are unique only per user, an admin uses this to identify another user's context unambiguously. When omitted, the import applies to the caller's own context with the given name. Supplying anownerother than yourself requires admin privileges and cross-user access being enabled (ADMIN_CROSS_USER_ACCESS_ENABLED=true; disabled by default); otherwise it receives404 Not Found.
Behavior and Validation
- The payload is fully validated before anything is written, so a malformed entry cannot leave a partially-imported table. Each entry must have a valid 64-character hex
tokenHashand a non-emptyreplacement. - The export must come from a context owned by the same username, in a deployment sharing this one's
PHILTER_ENCRYPTION_KEY. Token hashes are keyed per user (see Export), and a hash from another user or a differently-keyed deployment is indistinguishable from a valid one: it will import successfully and then never match during redaction. - Imported entries start with a read count of zero. As with normal redaction, imports honor the context's
MAX_CONTEXT_SIZEand may evict least-read entries when the context is full.
Returns 200 OK with a summary, 400 Bad Request for an invalid on_conflict value or malformed payload, and 404 Not Found if the context does not exist or the caller is not authorized to access it.
Example request:
curl -X POST -k -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
--data-binary @my-context-export.json \
"https://localhost:8080/api/contexts/my-context/entries/import?on_conflict=skip"
Example response:
{
"total": 2,
"inserted": 2,
"overwritten": 0,
"skipped": 0
}
Capacity
Each user can have at most 10 contexts, including the default context every user starts with. The limit counts only the user's own contexts, and it is fixed: no setting changes it. Creating an eleventh returns 409 Conflict with reason set to context_limit_reached; delete a context to make room. A name you already use is reported as context_exists whether or not you are at the limit. An administrator creating a context with owner is held to the other user's limit.
Each context is bounded by MAX_CONTEXT_SIZE (default 10,000 entries; overridable via the MAX_CONTEXT_SIZE environment variable). When the limit is reached, the least-read entry is evicted before a new one is inserted (ties broken by oldest). Disambiguation vector storage is similarly bounded by MAX_VECTORS_PER_CONTEXT (default 100,000, FIFO eviction).
Deleting /api/contexts/{name}/entries/{entryId} requires both the named context and entry ID to match within the target owner. Using an entry ID from a different context returns 404 and leaves the mapping intact. JSON resource responses use Content-Type: application/json.