Custodian
ReferenceHTTP API

Search

Find records by an exact-match search over a searchable field, by value or by blind index.

POST /v1/collections/:name/search finds records whose value on one field matches exactly. The field must be declared searchable in the schema. How you search depends on the field's sensitivity:

  • a plain field is searched by value — it is stored in the clear, and the server compares it directly;
  • a masked, tokenised or client_encrypted field is searched by a blind index — bytes you compute on your own device from a key the server never holds. The server matches on the index and never computes one itself. The blind index is produced on an enrolled device by the custodian command line; see Using the HTTP API.

You must hold a read grant on the field being searched; without it, a search is refused (see below).

Searching a plain field by value

POST /v1/collections/policies/search
Authorization: Bearer <token>
Content-Type: application/json

{ "field": "policy_number", "value": "PY-4471" }
{
  "field": "policy_number",
  "index_bits": null,
  "truncated": false,
  "results": [
    { "id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70", "token": null }
  ]
}

Searching an encrypted field by blind index

POST /v1/collections/policyholders/search
Authorization: Bearer <token>
Content-Type: application/json

{ "field": "sa_id_number", "blind_index": "<base64>" }
{
  "field": "sa_id_number",
  "index_bits": 16,
  "truncated": true,
  "results": [
    { "id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70", "token": "tok_9Xa1..." }
  ]
}

Reading the result

A result is deliberately thin: for each matching record it carries the record's identifier and the searched field's token, and nothing else. Returning the records' field values would turn a search into a way to harvest values a caller never identified.

Two fields make a candidate set unmistakable from an exact match:

  • index_bits is the width of the blind index the match used (null for a plain field, which is matched exactly);
  • truncated is true when that width is below the full width, so the results are a candidate set, not a confirmed match. A truncated index deliberately maps many values to the same index; you resolve the candidates' tokens to filter them on your device.

A search on a client_encrypted searchable field succeeds but carries a warning, so the trade is visible where it is made:

{
  "warning": "Field \"sa_id_number\" is client_encrypted and declared searchable. The server cannot read its values, but its blind index still discloses equality and frequency to anyone holding both the database and the index key; on a structured identifier that is enough to enumerate the value. This is a trade the schema may have made knowingly — the doctor command flags it as a misconfiguration."
}

The warning is advisory: the search still succeeds and returns its results. It marks that declaring a client_encrypted field searchable weakens the field — the blind index discloses equality and frequency even though the value itself stays unreadable. If you did not intend that trade, review the field's declaration in the schema; nothing about the search call itself needs to change to clear the warning.

The refusals

  • supplying a value on a field that requires a blind index is search_value_where_index_required:

    {
      "code": "search_value_where_index_required",
      "message": "Field \"sa_id_number\" is client_encrypted, whose value the server never holds, so it is searched by \"blind_index\" — computed on your device — not by a value."
    }
  • supplying a blind_index on a plain field is search_index_where_value_required:

    {
      "code": "search_index_where_value_required",
      "message": "Field \"policy_number\" is plain and stored in the clear, so it is searched by \"value\", not by a blind index."
    }

A field not in the collection is field_not_in_collection (422). A field that is not searchable, a missing field, or a search on a field you hold no read grant on, is a plain 422 or 403 naming the field, with no error code.

On this page