# Search, then fetch

API reference

People, jobs, and companies are searched with the same Query Spec tree. Every search hands back encrypted IDs, and a detail route turns the IDs you keep into records.

<a id="two-step"></a>
## Search, then fetch

This is the one thing to understand before writing any code. A search response contains encrypted string IDs and no record data at all. To read the records you send those IDs to the detail route for the same dataset, in batches of up to 100. There is no endpoint that does both.

```bash
# 1. Search. The response is IDs, a total, and a next cursor.
ids=$(curl -s -X POST "https://mira-api.metix.ai/v1/companies/query" \
  -H "Authorization: Bearer $METIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"where": {"all": [{"field": "industry", "match": "biotechnology"}]}, "size": 25}' \
  | jq '.data.company_ids')

# 2. Fetch. The response is records, for the IDs step 1 returned.
jq -n --argjson ids "$ids" '{company_ids: $ids}' |
curl -X POST "https://mira-api.metix.ai/entity/v1/companies/detail-by-id" \
  -H "Authorization: Bearer $METIX_KEY" \
  -H "Content-Type: application/json" \
  -d @-

# {"code":200,"msg":"ok","data":{"total":25,"found":25,"not_found":[],"results":[ ... ]}}
```

**Why it is split.** Searching a market and reading records are priced differently, and the split is what lets you use that. A search bills one API Credit per twenty-five ids, a record costs one per five, so finding is roughly five times cheaper per row than reading. Sweep wide, keep the ids that matter, and pay for records only where you actually wanted them.

<a id="query-spec"></a>
## One grammar, three vocabularies

All three search routes take the same `where` tree: composers `all`, `any`, and `not` around leaves that apply one operator to one field. What changes between datasets is the field list, not the grammar.

```json
{
  "where": {"all": [
    {"field": "<name>", "<operator>": <value>}
  ]},
  "size": 25
}
```

A leaf carries exactly one operator, so a bounded range is an `all` of two leaves rather than `gte` and `lte` in one node. The field decides which operators it takes: free-text fields take `match`, fields compared whole (categories, names, codes, yes or no) take `eq` or `in`, and numbers and dates take the range operators. [GET /contract](https://mira-api.metix.ai/docs/api.md#system) lists them for every field, and the [query spec](https://mira-api.metix.ai/docs/api/query-spec.md) page covers composition, operators, relative dates, same-record scopes, and cursor paging.

<a id="pages"></a>
## Sizes, totals and paging

A search takes an optional `size` and answers with the ids it found, a `total`, and a `next` cursor while more remain. The same three rules hold on all three datasets.

| Field | Rule |
| --- | --- |
| size | Omit it and you get 100. The maximum is 10,000. Every id in the window is billable, so ask for the page you intend to read rather than the ceiling. |
| total | How many matched, not how many came back. An exact integer below 100,000 and the string "100000+" at or above it, so read it as a union rather than as a number. |
| next | Send it back as after, with the same where tree, for the following page. It is absent once there is nothing left to fetch. |

<!-- POST /v1/jobs/query -->
```jsonc
// Page 1 response
{"data": {"job_ids": ["Jb71xKcAoP"], "total": 4278, "next": "<opaque cursor>"}}

// Page 2 request: the same where, and next sent back unchanged as after
{"where": {"all": [{"field": "title", "match": "data engineer"}]},
 "size": 100,
 "after": "<the next value from page 1>"}
```

`total` is the field to read before paging at all: it tells you whether the filter is narrow enough to be worth walking, and it costs nothing extra to look at.

**Pages are not a snapshot.** The cursor carries a position in the sort order and nothing else, so the server stores no state and a cursor never expires. It is sealed and only means something to the search that issued it: send it back unchanged, because an edited or hand-written cursor is refused with [invalid_cursor](https://mira-api.metix.ai/docs/reference/errors.md#paging). The cost of that is consistency between pages: a record indexed while you are paging can appear later, move, or be missed. If you need an exact set, narrow the query rather than paging deeper.

<a id="cost"></a>
## What a call costs

API Credits are spent per result, not per request, and the rate is the same on every plan. A larger plan buys a larger monthly allowance rather than a different rate.

| Call | API Credits | Charged when |
| --- | --- | --- |
| Query Spec search | ceil(ids / 25) | Results came back. A search that matched nothing is free |
| Natural-language people search | 5 + ceil(ids / 25) | Any successful response, including one that matched nothing |
| Detail | ceil(records / 5) | Records came back. Ids that resolved to nothing are free |

Nothing is charged for a refused request, a timeout or a 5xx, so a malformed query costs only the round trip. [credits](https://mira-api.metix.ai/docs/credits.md) carries the settlement rules and the plan allowances.

<a id="auth"></a>
## Authentication

Every route below the system endpoints takes a bearer key. Create one at https://platform.metix.ai/api-keys; keys begin with `metix_`, and one issued before that rename begins with `mira_` and is still valid.

```bash
export METIX_KEY="metix_xxxxxxxxxxxx"

curl -s "https://mira-api.metix.ai/auth/key/status" \
  -H "Authorization: Bearer $METIX_KEY"
```

[GET /auth/key/status](https://mira-api.metix.ai/docs/api.md#auth) reports whether the key is active, its scopes, its rate limit, and the quota it has left, which makes it the cheapest way to tell a bad key apart from an exhausted balance. A 401 carries [missing_api_key](https://platform.metix.ai/docs/quickstart.md#run) or [invalid_api_key](https://platform.metix.ai/docs/quickstart.md#run) and `docs_url` to the [run](https://platform.metix.ai/docs/quickstart.md#run). Other public error_code values are on the [error-codes](https://mira-api.metix.ai/docs/reference/errors.md#error-codes) page.

<a id="datasets"></a>
## The three datasets

Everything above is the same on all three. What differs is the field vocabulary, and each dataset carries its own: a page for searching it, and a page for the record that comes back. They share no field names, so a body written for one is refused by the other two.

| Dataset | Search | Record |
| --- | --- | --- |
| People | [people](https://mira-api.metix.ai/docs/api/people.md) | [record](https://mira-api.metix.ai/docs/api/people/record.md) |
| Jobs | [jobs](https://mira-api.metix.ai/docs/api/jobs.md) | [record](https://mira-api.metix.ai/docs/api/jobs/record.md) |
| Companies | [companies](https://mira-api.metix.ai/docs/api/companies.md) | [record](https://mira-api.metix.ai/docs/api/companies/record.md) |

The three are joined by identifiers rather than by names. A job carries the hiring company's id and a profile carries each employer's, and both resolve at the companies detail route, so you can walk from a search on one dataset into records on another without matching strings.

**Contact.** Contact unlock turns a person you found into a way to reach them: a personal email, a work email or a phone number, charged per value returned and never for one we could not find. Probe asks whether a value exists before you spend on it. See [contact](https://mira-api.metix.ai/docs/api/contact.md).

<a id="system"></a>
## System routes

`/version` needs no key and answers two questions. `version` is the release this build calls itself. `contract_hash` fingerprints the published contract: it changes when a route, a parameter, a limit or a documented response shape changes, and not otherwise, which makes it the one to pin.

```bash
curl -s "https://mira-api.metix.ai/version"

# {"code":200,"msg":"ok","data":{
#   "version":"2.1.2","contract_hash":"08f0406173520512"}}
```

Pin `contract_hash` when you integrate. A request that these pages say should work and does not is worth checking against it before anything else: the same value means the contract has not moved and the problem is in the query, and a different one means it has, and the [changelog](https://mira-api.metix.ai/docs/reference/changelog.md) says how.

[GET /contract](https://mira-api.metix.ai/docs/api.md#system) returns that contract as JSON: every route with its parameters and limits, and the queryable field vocabulary for each dataset. It takes your key and costs nothing. These pages are written; that is generated from the API answering you, so it describes what is being served rather than what was true when a page was last edited.

```bash
curl -s "https://mira-api.metix.ai/contract" \
  -H "Authorization: Bearer $METIX_KEY" | jq '.data.endpoints[].path'
```

| Method | Path | Purpose |
| --- | --- | --- |
| GET | /version | Read the deployed version and contract fingerprint |
| GET | /contract | Read the live contract: routes, query vocabulary, the operators each field takes, limits, billing |
| GET | /docs | Teaching catalog for this API: pages, examples, field notes, error recovery |
| GET | /auth/key/status | Inspect authenticated key status, scopes, rate limit, and quota projection |
