> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitsearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Search

> Run a natural language search against Orbit's people index. Returns ranked profiles. Supports global and directory scopes. Costs 2 credits per successful request.

The search endpoint accepts a plain English query and returns a ranked list of matching profiles. Orbit resolves your query through structured, semantic, and agentic search modes automatically — you don't need to specify a mode. Each successful request costs credits; requests that return no results or fail validation are refunded.

Requests authenticate with a developer API key that has the `search:read` scope. See [Authentication](/authentication).

<RequestExample>
  ```bash curl theme={"dark"}
  curl -X POST "https://api.orbitsearch.com/v2/developer/search" \
    -H "Authorization: Bearer sk_orb_REDACTED" \
    -H "Idempotency-Key: request-uuid-or-client-retry-key" \
    -H "Content-Type: application/json" \
    -d '{"query":"founders in sf","numUsers":10}'
  ```

  ```javascript JavaScript theme={"dark"}
  const response = await fetch("https://api.orbitsearch.com/v2/developer/search", {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_orb_REDACTED",
      "Idempotency-Key": "request-uuid-or-client-retry-key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      query: "founders in sf",
      numUsers: 10,
    }),
  });

  const data = await response.json();
  console.log(data.payload.users);
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://api.orbitsearch.com/v2/developer/search",
      headers={
          "Authorization": "Bearer sk_orb_REDACTED",
          "Idempotency-Key": "request-uuid-or-client-retry-key",
          "Content-Type": "application/json",
      },
      json={
          "query": "founders in sf",
          "numUsers": 10,
      },
  )

  data = response.json()
  print(data["payload"]["users"])
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={"dark"}
  {
    "status": "success",
    "searchId": "uuid",
    "payload": {
      "users": []
    }
  }
  ```
</ResponseExample>

## Headers

<ParamField header="Authorization" type="string" required>
  `Bearer sk_orb_...` with the `search:read` scope.
</ParamField>

<ParamField header="Idempotency-Key" type="string">
  A UUID or client-generated key for safe retries.
</ParamField>

<Tip>
  Always send an `Idempotency-Key` header. If you omit it, the server uses the request ID, which prevents duplicate charges within a single request but cannot deduplicate a client-side retry. Use a fresh idempotency key for every new request attempt.
</Tip>

## Body parameters

<ParamField body="query" type="string" required>
  Natural language description of the people you're searching for. Cannot be empty.

  Example: `"founders in sf"`, `"machine learning engineers at Series B startups"`
</ParamField>

<ParamField body="numUsers" type="number" required>
  Number of results to return. Must be a positive integer. Maximum value is `100`.
</ParamField>

<ParamField body="searchScope" type="object">
  Scope of the search. Omit this field or set `{"type":"global"}` to search the full Orbit index. Use a directory-scoped value to restrict results to one or more directories.

  <Expandable title="properties">
    <ParamField body="searchScope.type" type="string" required>
      One of: `"global"`, `"directory"`, `"directories"`
    </ParamField>

    <ParamField body="searchScope.directoryId" type="string">
      Required when `type` is `"directory"`. The UUID of a single directory to search.
    </ParamField>

    <ParamField body="searchScope.directoryIds" type="string[]">
      Required when `type` is `"directories"`. An array of directory UUIDs to search. All directories must belong to the same organization.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="structuredIntent" type="object">
  Optional structured search DSL. Use this when your integration already knows the exact filters or semantic clauses to run. `query` is still required and should describe the same user intent in plain English. See [Structured DSL](/api/search/structured-dsl) for the full schema.
</ParamField>

<ParamField body="includeMatchReason" type="boolean">
  Set to `true` to generate match reasons for top results. Defaults to `false`.
</ParamField>

<Note>
  * `userId` is not accepted in the request body.
  * `searchId` (search replay) is not accepted in the request body.
  * **Personal API keys can only use global search.** To use a directory scope, you must use an organization API key.
</Note>

## Search scope examples

<Tabs>
  <Tab title="Global">
    Omit `searchScope` entirely, or pass `{"type":"global"}`, to search the full Orbit index.

    ```json theme={"dark"}
    {
      "query": "founders in sf",
      "numUsers": 10,
      "includeMatchReason": true
    }
    ```
  </Tab>

  <Tab title="Single directory">
    Search within one directory. Requires an organization API key with a directory search grant.

    ```json theme={"dark"}
    {
      "query": "festival programmers",
      "numUsers": 10,
      "searchScope": {
        "type": "directory",
        "directoryId": "DIRECTORY_UUID"
      }
    }
    ```
  </Tab>

  <Tab title="Multiple directories">
    Search across several directories in a single request. All directories must belong to the same organization. Requires an organization API key.

    ```json theme={"dark"}
    {
      "query": "festival programmers",
      "numUsers": 10,
      "searchScope": {
        "type": "directories",
        "directoryIds": ["DIRECTORY_UUID_A", "DIRECTORY_UUID_B"]
      }
    }
    ```
  </Tab>
</Tabs>

## Structured DSL example

Use `structuredIntent` to provide explicit search clauses while still sending a human-readable `query`:

```json theme={"dark"}
{
  "query": "current OpenAI engineers in San Francisco with infrastructure experience",
  "numUsers": 10,
  "structuredIntent": {
    "version": "v1",
    "experiences": [
      { "organization": "OpenAI", "title": "engineer", "temporalScope": "current" }
    ],
    "geo": { "place": "San Francisco", "distance": "50km" },
    "semanticClauses": [{ "text": "infrastructure" }]
  }
}
```

The plain-English `query` is used for moderation, logging, summaries, match reasons, and search-quality training. It should align with `structuredIntent`; it does not need to restate every field.

`entityClauses` are not public yet. The server rejects entity clauses and server-owned resolved fields.

## Streaming

Use `POST /v2/developer/search/sse` when you want the initial results immediately and match-reason updates as they are generated. The SSE endpoint accepts the same request body as synchronous search, including `structuredIntent`, `searchScope`, and `includeMatchReason`.

```bash theme={"dark"}
curl -N -X POST "https://api.orbitsearch.com/v2/developer/search/sse" \
  -H "Authorization: Bearer sk_orb_REDACTED" \
  -H "Idempotency-Key: request-uuid-or-client-retry-key" \
  -H "Content-Type: application/json" \
  -d '{"query":"founders in sf","numUsers":10,"includeMatchReason":true}'
```

```text theme={"dark"}
event: initial
data: {"status":"success","payload":{"users":[]}}

event: search_id
data: {"searchId":"uuid"}

event: update
data: {"id":"PROFILE_ID","matchReason":{"reason":"..."}}
```

Validation, authentication, rate-limit, and credit failures before the stream starts return regular JSON errors. After the stream starts, errors are emitted on the event stream.

## Response

<ResponseField name="status" type="string" required>
  Always `"success"` for a 200 response.
</ResponseField>

<ResponseField name="searchId" type="string" required>
  Unique identifier for this search request. Useful for support and debugging.
</ResponseField>

<ResponseField name="payload" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="payload.users" type="object[]" required>
      Array of matched profile objects, ranked by relevance. Each object includes an `id` field containing the canonical public profile ID you can pass to the [Profile Read](/api/search/profile-read) endpoint, plus public result metadata such as `displayName`, `personName`, `avatarUrl`, `sourceCount`, `generationLevel`, `city`, `age`, and `bio` when available. Empty top-level legacy fields such as `username`, `link`, `response`, and `sources` are not returned. The array is empty when no profiles matched.
    </ResponseField>
  </Expandable>
</ResponseField>

### Response headers

| Header                              | Description                                                  |
| ----------------------------------- | ------------------------------------------------------------ |
| `X-Developer-API-Credits-Remaining` | Your remaining credit balance after this request was charged |

## Credits

Each successful search costs **2 credits** by default, regardless of `numUsers` or whether you use the synchronous or SSE endpoint. Credits are reserved before the search runs, so out-of-credit requests fail immediately without consuming search resources. If a search returns no results or fails, the reservation is refunded.

## Rate limits

**25 requests per second** per API key, with bursts up to **100 requests**. Rate limits are tracked per key, not per IP address. A `429` response does not consume credits.

## Error responses

<AccordionGroup>
  <Accordion title="400 — Bad request">
    | Code                                           | Description                                                                                     |
    | ---------------------------------------------- | ----------------------------------------------------------------------------------------------- |
    | `developer_query_required`                     | `query` is missing or empty                                                                     |
    | `developer_num_users_required`                 | `numUsers` is missing or not a valid positive integer                                           |
    | `developer_num_users_limit_exceeded`           | `numUsers` exceeds the maximum of 100                                                           |
    | `developer_user_id_override_forbidden`         | Request body includes a `userId` field, which is not permitted                                  |
    | `developer_search_replay_unsupported`          | Request body includes a `searchId` field; search replay is not supported for developer API keys |
    | `structured_intent_invalid`                    | `structuredIntent` is malformed or contains unsupported values                                  |
    | `structured_intent_version_unsupported`        | `structuredIntent.version` is not `"v1"`                                                        |
    | `structured_intent_entity_clauses_unsupported` | `entityClauses` are not part of the public structured DSL                                       |
    | `structured_intent_server_owned_field`         | Request body includes a server-owned structured intent field                                    |
  </Accordion>

  <Accordion title="401 — Unauthorized">
    | Code              | Description                                               |
    | ----------------- | --------------------------------------------------------- |
    | `missing_api_key` | No `Authorization: Bearer sk_orb_...` header was provided |
  </Accordion>

  <Accordion title="402 — Payment required">
    | Code                                 | Description                                                   |
    | ------------------------------------ | ------------------------------------------------------------- |
    | `developer_api_credits_insufficient` | Your remaining credits are lower than the cost of this search |
  </Accordion>

  <Accordion title="403 — Forbidden">
    | Code                                        | Description                                                                            |
    | ------------------------------------------- | -------------------------------------------------------------------------------------- |
    | `invalid_api_key`                           | The key is malformed, revoked, expired, or unknown                                     |
    | `missing_api_key_scope`                     | The key does not have the `search:read` scope                                          |
    | `developer_api_key_organization_required`   | A directory scope was requested, but the key is a personal API key                     |
    | `developer_api_key_organization_forbidden`  | The organization API key is not authorized for the requested organization search scope |
    | `search_directory_access_api_key_forbidden` | The organization API key does not have a matching directory search grant               |
  </Accordion>

  <Accordion title="404 — Not found">
    | Code                         | Description                                                                                                                                                                                                 |
    | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `search_directory_not_found` | One or more requested directories do not exist, are archived, or are unavailable for search. Also returned when a search runs successfully but no profiles matched — in this case, no credits are consumed. |
  </Accordion>

  <Accordion title="409 — Conflict">
    | Code                                     | Description                                                                                          |
    | ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
    | `developer_api_idempotency_key_conflict` | The idempotency key was reused with different request parameters. Use a different key.               |
    | `developer_api_idempotency_key_refunded` | The idempotency key belongs to a previously refunded request. Send a fresh idempotency key to retry. |
  </Accordion>

  <Accordion title="429 — Rate limited">
    | Code                             | Description                                              |
    | -------------------------------- | -------------------------------------------------------- |
    | `developer_api_key_rate_limited` | You have exceeded the Search rate limit for this API key |
  </Accordion>
</AccordionGroup>
