---
updatedAt: 2026-08-24T18:16:39.000Z
agentTools:
  projectIndex: https://developer.breezy.hr/llms.txt
---

# /company/{id}/candidates/search

Free-text search across every candidate in the company, served by the same search index that powers in-app candidate search. This verb always returns the paginated envelope below - one request contract, one response contract.

`email_address` is not accepted here and returns `400`. Exact email lookup is the `GET` form of this path: it is served from the primary store and is immediately consistent, whereas this search is index-backed and eventually consistent, so a candidate created moments ago may not appear yet.

Access: results follow the caller. A company admin searches the whole company; any other token owner is limited to the positions they are a member of - the same scope in-app search applies to that user.

Query grammar: bare terms, quoted phrases (`"product manager"`), the boolean operators `AND`, `OR` and `NOT`, and a trailing `*` for prefix matching. With `match: prefix` (the default) the trailing wildcard is added for you, which is what the in-app search box does. Anything else that carries query-syntax meaning - `field:value`, regex delimiters, grouping, `+`, `-`, `&`, `|` - is searched as literal text, so a URL or a term like `C++` matches as typed. A leading wildcard, an unbalanced quote, or a query starting or ending with a boolean operator returns `400`.

Coverage: the index spans candidate name, email address, phone number, headline, tags, free-text and choice questionnaire responses, work history, education, and the position location. It does not cover uploaded resume or CV file contents, and it does not cover social profiles.

Results are position-scoped: a candidate record is a person on a position, so the same person appears once per matching position and `total` counts records, not people. `meta_id` is the stable person identifier if you need to group them yourself.

# OpenAPI definition

````json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Breezy HR Public API",
    "version": "3",
    "description": "The Breezy HR public API (v3) provides programmatic access to your recruiting data.\nAll endpoints are relative to `https://api.breezy.hr/v3`.\n\n## Authentication\n\nPass a **Personal Access Token (PAT)** in the `Authorization` header:\n```\nAuthorization: Bearer breezy_pat_<token>\n```\n\nAlternatively, call `POST /v3/signin` with email and password to receive a session\n`access_token`, then pass it the same way.\n\nBoth header forms are accepted for PATs and session tokens: `Authorization: Bearer <token>`\nand the bare `Authorization: <token>`. The `Bearer` scheme is matched case-insensitively.\n\n## IDs\n\nAll resource IDs are 12–14 character lowercase hex strings (e.g. `a3f9c1d2e4b5`).\nEU-region resources use 14-character IDs ending in `01`.\n\n## Dates\n\nAll date/timestamp fields are ISO 8601 strings.\n\n## Errors\n\nAll error responses use the shape:\n```json\n{ \"error\": { \"type\": \"string\", \"message\": \"string\" } }\n```\n\nEvery `/company/{companyId}/...` endpoint returns `403` with type `companyMembershipRequired`\nwhen the authenticated user is not a member of that company, for example after the user who\ncreated the token was removed from the account.\n"
  },
  "servers": [
    {
      "url": "https://api.breezy.hr/v3",
      "description": "Production"
    }
  ],
  "security": [
    {
      "TokenAuth": []
    }
  ],
  "tags": [
    {
      "name": "Candidates",
      "description": "Candidate management and all sub-resources"
    }
  ],
  "components": {
    "securitySchemes": {
      "TokenAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Personal Access Token (PAT) or session token from POST /v3/signin, passed in the Authorization header either bare or with a Bearer prefix (both forms are accepted). Declared as apiKey rather than http/bearer so that tools do not force the prefix - the bare form is equally valid."
      }
    },
    "schemas": {
      "CandidateSearchResults": {
        "description": "Paginated free-text candidate search results. Page until `next_page` is `null`.",
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Matching candidate records. Records are position-scoped, so this counts records rather than distinct people."
          },
          "total_capped": {
            "type": "boolean",
            "description": "`true` when more records matched than the search index will count exactly; `total` then reports the ceiling rather than the real figure."
          },
          "page": {
            "type": "integer",
            "description": "Zero-based index of the page returned."
          },
          "page_size": {
            "type": "integer",
            "description": "Records per page for this response."
          },
          "next_page": {
            "type": "integer",
            "nullable": true,
            "description": "Index of the next page, or `null` when there are no more pages (including at the end of the result window)."
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CandidateSearchRecord"
            }
          }
        }
      },
      "CandidateSearchRecord": {
        "description": "Candidate record returned by free-text candidate search. This is its own shape - it is deliberately not the ListCandidate projection, and fields are added to it only as a versioned change.",
        "type": "object",
        "properties": {
          "_id": {
            "type": "string",
            "description": "Candidate record id. A candidate is a person on a position."
          },
          "meta_id": {
            "type": "string",
            "description": "Stable person identifier, shared by every record for the same person across positions."
          },
          "name": {
            "type": "string"
          },
          "email_address": {
            "type": "string"
          },
          "phone_number": {
            "type": "string"
          },
          "headline": {
            "type": "string"
          },
          "origin": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "creation_date": {
            "type": "string"
          },
          "updated_date": {
            "type": "string"
          },
          "stage": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "source": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "position": {
            "type": "object",
            "properties": {
              "_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/company/{companyId}/candidates/search": {
      "post": {
        "operationId": "queryCompanyCandidates",
        "summary": "/company/{id}/candidates/search",
        "tags": [
          "Candidates"
        ],
        "parameters": [
          {
            "name": "companyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "description": "Free-text search across every candidate in the company, served by the same search index that powers in-app candidate search. This verb always returns the paginated envelope below - one request contract, one response contract.\n\n`email_address` is not accepted here and returns `400`. Exact email lookup is the `GET` form of this path: it is served from the primary store and is immediately consistent, whereas this search is index-backed and eventually consistent, so a candidate created moments ago may not appear yet.\n\nAccess: results follow the caller. A company admin searches the whole company; any other token owner is limited to the positions they are a member of - the same scope in-app search applies to that user.\n\nQuery grammar: bare terms, quoted phrases (`\"product manager\"`), the boolean operators `AND`, `OR` and `NOT`, and a trailing `*` for prefix matching. With `match: prefix` (the default) the trailing wildcard is added for you, which is what the in-app search box does. Anything else that carries query-syntax meaning - `field:value`, regex delimiters, grouping, `+`, `-`, `&`, `|` - is searched as literal text, so a URL or a term like `C++` matches as typed. A leading wildcard, an unbalanced quote, or a query starting or ending with a boolean operator returns `400`.\n\nCoverage: the index spans candidate name, email address, phone number, headline, tags, free-text and choice questionnaire responses, work history, education, and the position location. It does not cover uploaded resume or CV file contents, and it does not cover social profiles.\n\nResults are position-scoped: a candidate record is a person on a position, so the same person appears once per matching position and `total` counts records, not people. `meta_id` is the stable person identifier if you need to group them yourself.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "anyOf": [
                  {
                    "required": [
                      "query"
                    ]
                  },
                  {
                    "required": [
                      "filter_text"
                    ]
                  }
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256,
                    "description": "The free-text search term."
                  },
                  "filter_text": {
                    "type": "string",
                    "deprecated": true,
                    "description": "Deprecated alias for `query`, accepted so existing integrations keep working. Supplying both returns `400`, and responses to requests using it carry a `Deprecation` header."
                  },
                  "match": {
                    "type": "string",
                    "enum": [
                      "prefix",
                      "exact"
                    ],
                    "default": "prefix",
                    "description": "`prefix` appends a trailing wildcard to the term, matching in-app search. `exact` searches the term exactly as supplied."
                  },
                  "sort": {
                    "type": "string",
                    "enum": [
                      "relevance",
                      "updated",
                      "created"
                    ],
                    "default": "relevance",
                    "description": "`relevance` ranks best match first, as in-app search does. Relevance order can shift between requests, so use `updated` when paging through a whole result set."
                  },
                  "page_size": {
                    "type": "integer",
                    "default": 50,
                    "minimum": 1,
                    "maximum": 50,
                    "description": "Records per page. Out of range returns `400` rather than being silently clamped."
                  },
                  "page": {
                    "type": "integer",
                    "default": 0,
                    "minimum": 0,
                    "description": "Zero-based page index. `page * page_size + page_size` must not exceed 1000; beyond that returns `400`."
                  },
                  "archived": {
                    "type": "boolean",
                    "default": true,
                    "description": "When `true` (default) the search spans archived candidates so it covers the company's full history. Set to `false` to exclude them."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paginated free-text search results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CandidateSearchResults"
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `query`, an unknown parameter, `email_address` supplied, a non-integer or out-of-range page, a leading wildcard, an unbalanced quote, or a dangling boolean operator"
          },
          "403": {
            "description": "Company is not on a plan with API access"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Candidate search is temporarily unavailable"
          },
          "504": {
            "description": "The search did not complete within the server-side time limit"
          }
        }
      }
    }
  }
}
````