API reference

Jobs

Search active job postings with a Query Spec tree and read the postings you select. Openings are a demand signal: who is hiring, for what, where, and at what pay.

Routes
2
Search
/v1/jobs/query
Search returns
encrypted IDs
Detail batch
up to 100 IDs
  • POST/v1/jobs/queryQuery Spec search
  • POST/entity/v1/jobs/detail-by-idFetch job records

Search, then fetch

Search never returns postings. It returns encrypted string job IDs, and you pass up to 100 of them to the detail route to read the records. There is no one-shot endpoint, so plan for two calls.

# 1. Search. Returns encrypted IDs, never postings.
curl -X POST "https://mira-api.metix.ai/v1/jobs/query" \
  -H "Authorization: Bearer $METIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "where": {"all": [
      {"field": "title", "match": "machine learning platform"},
      {"field": "country_iso_2", "eq": "US"},
      {"field": "posted", "gte": "now-30d"},
      {"any": [
        {"field": "seniority", "eq": "Mid-Senior level"},
        {"field": "seniority", "eq": "Director"}
      ]}
    ]},
    "size": 25
  }'

# 2. Read the postings, up to 100 IDs per call.
curl -X POST "https://mira-api.metix.ai/entity/v1/jobs/detail-by-id" \
  -H "Authorization: Bearer $METIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job_ids": ["Jb71xKcAoP", "Jb0mQ4ZtRe"],
    "_source": ["id", "title", "company_name", "location", "seniority", "posted"]
  }'

Two calls, always

A search response carries IDs and nothing else. If your code expects records back from a search, it will read an empty payload and conclude there is no data. Send the IDs to the detail route, in batches of up to 100.

When to use it

  • Measure hiring demand by role, function, seniority, company, or location.
  • Find who is hiring for a capability, then pivot to the companies and people behind it.
  • Filter by salary band or posting date to separate live demand from stale listings.

Request parameters and limits

Parameter
where
Type
object
Notes
Query Spec tree. Required by /v1/jobs/query
Parameter
size
Type
integer (1-10000)
Notes
Job IDs in one page. Omitting it returns 100
Parameter
after
Type
string
Notes
Cursor. Pass the next value from the previous response
Parameter
job_ids
Type
string[] (1-100)
Notes
IDs from a search, sent to the detail route
Parameter
_source / source
Type
array, object, or boolean
Notes
Optional field selection on detail
  • One operator per leaf. A salary band is an all of two leaves, not gte and lte in one node.
  • A field name that is not in this dataset's list is refused, so a typo is visible rather than returning a record without it. The three datasets do not share one vocabulary.
  • Search returns at most 10,000 IDs per page and 100 when size is omitted; detail accepts at most 100 IDs per request.
  • Jobs declare no scope nodes. One document is one posting, so there is nothing to group.
  • The response carries IDs and a cursor, not a match count. Page until next is absent if you need the size of a set.
  • The index holds daily active postings. A posting closed since your search can come back in not_found.

Credit cost

Job search costs ceil(returned IDs / 25) Credits, and 0 when it returns nothing. Job detail costs ceil(found records / 5) Credits, and not_found rows are free.

Queryable fields

These are the only names a leaf may put in field. The grammar around them, including composition, one operator per leaf, and relative dates, is on the Query Spec page. What a response may contain is a separate, wider list: a field can be returnable without being queryable.

titledescriptioncompany_name

What the posting is and who posted it.

Operators: match, eq, in, exists

{
  "where": {
    "all": [
      {
        "field": "title",
        "match": "backend engineer"
      },
      {
        "field": "application_active",
        "eq": true
      }
    ]
  }
}
seniorityemployment_typefunctionsindustries

How the posting is classified.

Operators: match, eq, in, exists

{
  "where": {
    "all": [
      {
        "field": "seniority",
        "eq": "Mid-Senior level"
      }
    ]
  }
}
citystatecountrylocationregions

Where the job is. country takes a name; country_iso_2 takes the code.

Operators: match, eq, in, exists

{
  "where": {
    "all": [
      {
        "field": "country",
        "match": "China"
      },
      {
        "field": "city",
        "match": "Shanghai"
      },
      {
        "field": "regions",
        "match": "Asia"
      }
    ]
  }
}
country_iso_2application_active

ISO 3166-1 alpha-2 code such as US. application_active is true on every posting in the index today, so it narrows nothing.

Operators: eq, in, exists

{
  "where": {
    "all": [
      {
        "field": "application_active",
        "eq": true
      }
    ]
  }
}
salary_minsalary_max

Advertised pay. exists separates postings that state a salary from those that do not.

salary_min, salary_max read on a record; filtering on them is being improved.

Operators: gte, lte, exists

Ask for salary_min and salary_max in _source and they come back on the record as normal. Using them as a filter answers with an error for now, rather than a set that looks right and is not, while we make the comparison reliable. Narrow with the other fields in this group and sort what comes back. Everything else here filters normally.

salary_currency

Currency of the advertised pay. Compare salary figures only within one currency.

Operators: match, eq, in, exists

{
  "where": {
    "all": [
      {
        "field": "salary_currency",
        "eq": "USD"
      }
    ]
  }
}
applicants_countrequired_months_of_experience

Competition for the role, and the experience the posting asks for.

applicants_count reads on a record; filtering on it is being improved.

Operators: gte, lte, exists

Ask for applicants_count in _source and it comes back on the record as normal. Using it as a filter answers with an error for now, rather than a set that looks right and is not, while we make the comparison reliable. Narrow with the other fields in this group and sort what comes back. Everything else here filters normally.

{
  "where": {
    "all": [
      {
        "field": "required_months_of_experience",
        "gte": 24
      }
    ]
  }
}
postedcreated_atupdated_at

Posting dates. Take YYYY-MM-DD or a relative offset such as now-30d.

posted reads on a record; filtering on it is being improved.

Operators: gte, lte, exists

Ask for posted in _source and it comes back on the record as normal. Using it as a filter answers with an error for now, rather than a set that looks right and is not, while we make the comparison reliable. Narrow with the other fields in this group and sort what comes back. Everything else here filters normally.

Fields with a fixed set of values

These are matched against the whole stored value, so the spelling and the capitalisation both have to match. Getting either wrong is the one mistake in the grammar that raises no error: the query is valid, it costs nothing, and it returns zero, which reads as an answer about the data rather than about the query.

Field
seniority
Values
AssociateDirectorEntry levelExecutiveInternshipMid-Senior levelNot Applicable
Field
employment_type
Values
ContractFull-timeInternshipOtherPart-timeTemporaryVolunteer

Not every category field is a fixed set

functions and industries look like fixed sets and are not: each runs to several hundred values. Ask them with match.

Response shape

Successful responses use the public envelope with code, msg, and data.

Search returns IDs, and a next cursor while more pages remain:

{
  "code": 200,
  "msg": "ok",
  "data": {
    "job_ids": ["Jb71xKcAoP", "Jb0mQ4ZtRe"],
    "next": "WzE3MjQwMDAwMDAsIkpiNzF4S2NBb1AiXQ"
  }
}

Detail returns the records, and names the IDs it could not resolve:

{
  "code": 200,
  "msg": "ok",
  "data": {
    "total": 2,
    "found": 1,
    "not_found": ["Jb0mQ4ZtRe"],
    "results": [{
      "id": "Jb71xKcAoP",
      "title": "Machine Learning Platform Engineer",
      "company_name": "Example Corp",
      "location": "San Francisco, CA",
      "seniority": "Mid-Senior level",
      "posted": "2026-08-02"
    }]
  }
}

The record itself is a page of its own: every field it can carry, what each one holds, and a whole one to read.

Access surfaces

Surface
REST
How to use this capability
POST /v1/jobs/query; POST /entity/v1/jobs/detail-by-id
Surface
Metix AI skill
How to use this capability
metix-job-search
Surface
MCP
How to use this capability
Use the MCP setup page to expose the same REST capability. Capability-specific tool names remain pending schema verification.
Surface
Natural-language prompt
How to use this capability
Find US machine-learning platform openings posted in the last 30 days, fetch the postings, and rank the hiring companies by how many they have open.

Common next step: look up the companies doing the hiring.