ATS API: How to Add Natural-Language Search to the ATS You Already Run

Keep the ATS you run and bolt a natural-language search layer on top with an ATS API. Translate plain English into structured people filters across your records and fresh data.

Published

Jun 19, 2026

Written by

Manmohit Grewal

Reviewed by

Nithish

Read time

7

minutes

ATS API: How to Add Natural-Language Search to the ATS You Already Run

You do not need to replace your ATS to search it in plain English. You need a thin layer on top that takes a sentence like "software engineers who worked at a Series B company in New York with seven years of experience," turns it into a structured query, runs it against the records you already hold, and pulls fresh people data through an API when your own database comes up short. That layer is what most teams are actually asking for when they search for an ATS API, and you can build it without touching the system your recruiters live in every day.

We know that because the four recruiting teams we spoke with kept describing the same thing in different words. An ATS-and-CRM vendor wanted its users to query their own candidate database in natural language. A boutique executive-search firm wanted to ask for people the way they talk. A founder building his own recruiting platform wanted one clean data layer behind a search box. None of them wanted to throw out what they had. This guide shows how to add the search layer with a people search API, on both a low-code path and a direct-code path, with real endpoints you can copy. You can follow along on Crustdata's free tier, which includes 100 credits.

Why bolt search on instead of switching ATS?

Three of the four teams we spoke with were already running a system they had no intention of leaving. The question was never which ATS to buy. It was how to make the one they had answer a plain-English question.

An ATS-and-CRM product for recruiting agencies put it most plainly. Their users keep tens of thousands of candidates in their own database, and the team described the core problem as scale: "going through the entire database of, you know, sometimes millions of candidates is going to be very difficult for our users." The records existed. The way to reach them did not.

The fix they wanted was conversational. As they described it, "the natural way to do it is that you query natural language," so a recruiter could just ask for "candidates who have worked in a Series B company in New York, who have seven years plus experience, software engineers." Their users had been asking for exactly this. The team told us "even our users have been asking this," and they were building it as a layer over the database their customers already maintained, with no reason to migrate anyone off the system they ran.

The boutique search firm arrived at the same place from the opposite direction. They had stopped relying on a sourcing UI with weak filtering, which one of the founders described as "a garbage pail of everything" with little real filtering to lean on. So they wired a plain-English search into the front of their workflow instead, and the rest of their stack stayed put.

The pattern holds across the calls. You keep the system of record, and you change the way you reach into it.

What does a natural-language ATS search layer actually do?

A natural-language search layer has two jobs, and it is worth being precise about which part is the model and which part is the API.

The model reads the sentence. When a recruiter types "senior backend engineers who left a fintech in the last six months," something has to convert that into machine-readable criteria: a title pattern, a seniority level, an industry, a job-change window. That translation is the work of a language model, whether it is a coding agent or a model you call yourself. The ATS-and-CRM team had already built this side. They described "passing it through an LLM" that they had "given all the descriptions about the filters," so the model could map English onto the fields their system understood.

The API runs the query. Once the sentence is structured criteria, you need somewhere to run it. That is the people search API. It takes structured filters and returns matching profiles, fast enough to sit behind a search box and scale to many requests. The translation and the search are separate steps, and keeping them separate is what lets you reuse one model across your own records and a fresh-data provider.

This distinction matters because there is no single endpoint that swallows a raw English sentence and hands back people. The model does the language work and the API does the data work, and your search layer is the small piece of code that connects the two.

How do you query your own records first, then fresh data?

Two of the four teams described the same two-step shape, and it is the part that keeps your existing ATS at the center.

You search what you own before you pay for anything new. The ATS-and-CRM team was explicit about the order. As they described it, "currently we would pull the candidate list from the user's database itself" and show the recruiter how many of their own candidates fit the criteria. Only when the internal database comes up short does the second step kick in. They described the fallback as sourcing on demand: if a user "doesn't find it" in their own records, the system goes out and "we'll source it for them" against a fresh people-data source. They had even named the project internally and slotted it into a roadmap, which tells you how real the demand was.

The founder building his own platform ran the same logic for a different reason. He cached results into his own backend so a repeated search would return people he had already paid to enrich, rather than billing him twice for the same person. The shape is identical. Check your own database, and only reach for fresh data when you have to.

So the search layer has a clear order of operations:

  • Translate the request once: the model converts the recruiter's sentence into structured filters that both your database and the API understand.

  • Run it against your own records first: most established teams already hold the candidates they need, and querying your own store costs nothing per search.

  • Fall back to fresh people data on a miss: when your database returns too few matches, send the same structured filters to the people search API and merge the new profiles in.

  • Write the new people back into your ATS: the candidates you source become records you own, so the next identical search finds them at home.

That last step is where this guide hands off. Filling, governing, and refreshing those records inside your own database is its own job, and we cover it in detail in our guide to integrating enrichment with your ATS and candidate database.

How do you build the search layer with an MCP path?

A Claude Code agent with Crustdata's MCP server configured is the fastest way to build the search layer, because the agent handles the language translation for you and calls the data tools directly. The boutique search firm worked exactly this way. They described going to Claude and asking, in plain English, "using Crustdata, can you find me 25 people that are in home healthcare AI software in the West Coast," and getting back a structured list. The recruiter writes a sentence, and the agent turns it into people.

The relationship between the pieces is worth stating once so it is clear. Claude Code is the agent runtime, MCP is the protocol it uses to call tools, and the Crustdata MCP server is a tool that wraps the Crustdata API. To the recruiter, all of that is hidden behind a chat box.

To set it up, you install the Crustdata MCP server, point your agent at it with your API key, and let the agent map the request onto the search tool. A prompt like the one below is enough to drive a real search:

Using the Crustdata MCP server, find people who match this request:
"software engineers who worked at a Series B company in New York,
seven plus years of experience, not currently at a big-tech employer."

Return the name, present role, employer, and location for the top 25.
Using the Crustdata MCP server, find people who match this request:
"software engineers who worked at a Series B company in New York,
seven plus years of experience, not currently at a big-tech employer."

Return the name, present role, employer, and location for the top 25.
Using the Crustdata MCP server, find people who match this request:
"software engineers who worked at a Series B company in New York,
seven plus years of experience, not currently at a big-tech employer."

Return the name, present role, employer, and location for the top 25.

The agent translates that sentence into the structured filters the search tool expects, runs the query, and returns the people. For a low-code team this is the whole search layer, and it is the cleanest way to prototype the prompt and the filter logic before you commit any engineering time. Once the request patterns settle, you can save them as reusable prompts so every recruiter on the team runs the same well-formed search.

How do you build it with a direct REST path?

For teams that want full control over the merge logic and the fallback, you call the people search API yourself, following the same pattern we lay out in our people search API workflow for recruiting agencies. The model still translates the sentence, but you own the code that runs the query, checks your own database, and decides when to reach for fresh data.

The endpoint takes structured filters rather than raw English, which is why the translation step comes first in your own code. The sentence above maps onto this request:

curl --request POST \
  --url https://api.crustdata.com/person/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "experience.employment_details.title", "type": "(.)", "value": "Software Engineer" },
        { "field": "basic_profile.location.full_location", "type": "(.)", "value": "New York" }
      ]
    },
    "limit": 25
  }'
curl --request POST \
  --url https://api.crustdata.com/person/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "experience.employment_details.title", "type": "(.)", "value": "Software Engineer" },
        { "field": "basic_profile.location.full_location", "type": "(.)", "value": "New York" }
      ]
    },
    "limit": 25
  }'
curl --request POST \
  --url https://api.crustdata.com/person/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "experience.employment_details.title", "type": "(.)", "value": "Software Engineer" },
        { "field": "basic_profile.location.full_location", "type": "(.)", "value": "New York" }
      ]
    },
    "limit": 25
  }'

The response comes back as a profiles array with identity, employment history, education, and location on each person, plus a total_count for how many match across the full database and a next_cursor for paging. Search is billed at 0.03 credits per result returned, so a 25-person search costs well under a single credit. You take those profiles, dedupe them against the records you already hold, and write the new ones into your ATS.

In code, the two-step flow from the previous section looks like this:

import requests

HEADERS = {
    "authorization": "Bearer YOUR_API_KEY",
    "content-type": "application/json",
    "x-api-version": "2025-11-01",
}

def search_candidates(structured_filters, own_db, limit=25):
    # Step 1: run the structured filters against your own ATS records first.
    local = own_db.query(structured_filters, limit=limit)
    if len(local) >= limit:
        return local

    # Step 2: short on matches, so pull fresh people data via the API.
    resp = requests.post(
        "https://api.crustdata.com/person/search",
        headers=HEADERS,
        json={"filters": structured_filters, "limit": limit - len(local)},
    )
    fresh = resp.json().get("profiles", [])

    # Merge, dedupe on the stable person id, and return one ranked list.
    return own_db.merge(local, fresh, key="crustdata_person_id")
import requests

HEADERS = {
    "authorization": "Bearer YOUR_API_KEY",
    "content-type": "application/json",
    "x-api-version": "2025-11-01",
}

def search_candidates(structured_filters, own_db, limit=25):
    # Step 1: run the structured filters against your own ATS records first.
    local = own_db.query(structured_filters, limit=limit)
    if len(local) >= limit:
        return local

    # Step 2: short on matches, so pull fresh people data via the API.
    resp = requests.post(
        "https://api.crustdata.com/person/search",
        headers=HEADERS,
        json={"filters": structured_filters, "limit": limit - len(local)},
    )
    fresh = resp.json().get("profiles", [])

    # Merge, dedupe on the stable person id, and return one ranked list.
    return own_db.merge(local, fresh, key="crustdata_person_id")
import requests

HEADERS = {
    "authorization": "Bearer YOUR_API_KEY",
    "content-type": "application/json",
    "x-api-version": "2025-11-01",
}

def search_candidates(structured_filters, own_db, limit=25):
    # Step 1: run the structured filters against your own ATS records first.
    local = own_db.query(structured_filters, limit=limit)
    if len(local) >= limit:
        return local

    # Step 2: short on matches, so pull fresh people data via the API.
    resp = requests.post(
        "https://api.crustdata.com/person/search",
        headers=HEADERS,
        json={"filters": structured_filters, "limit": limit - len(local)},
    )
    fresh = resp.json().get("profiles", [])

    # Merge, dedupe on the stable person id, and return one ranked list.
    return own_db.merge(local, fresh, key="crustdata_person_id")

The structured_filters here are whatever your model produced from the recruiter's sentence, so the same translation feeds both your database query and the API call. The data on both paths is identical, which means the only real decision is how much of the orchestration you want to write yourself.

How do you make your records answer the question?

A natural-language search is only as good as the fields it can filter on, and two of the four teams ran into the same gap. Their records did not hold the attributes their users were asking about.

The ATS-and-CRM team described it precisely. For each candidate's employer they stored "the company names for now, and then the time period from which the candidate has worked in that company." But users were asking for "someone who has worked in a Series B," which means the system needs the company's funding stage, whether it is public or private, and where it is based. None of that lived on the record yet. The plain-English query was ready before the data was.

The founder building his own platform ran into the same gap on the people side, where he needed skills, full work history, and location enriched onto each candidate before his matching logic could use them. In his words, enrichment was "the number one expense in the entire platform," and it was also the part that decided whether his search returned anything useful.

So before a natural-language layer can answer a rich question, the records behind it have to carry the fields that question filters on. You enrich a candidate's employers with funding and location data, and you enrich the candidate with skills and a full work history, so the same sentence that asks for "a Series B engineer in New York" has real columns to match against. The mechanics of filling and refreshing those records from an enrichment API, including how to avoid paying to enrich the same person twice, are covered in our guide to candidate enrichment APIs.

How do you keep search cheap when end users run it?

Two of the four teams worried about the same thing once search sits behind a box that real users can click. Once your end users run the searches, they control how often the search fires and what each run costs you.

The founder building his own platform named the risk directly. His fear was "one power user who's going to bankrupt me" by clicking the search button all day, and waking up to a large bill he never authorized. The boutique firm came at it from the budgeting side, telling us they prefer a fixed, predictable cost over a consumption model where a busy week produces a surprise.

The two-step flow is most of the answer. Because you query your own records first and only call the API on a miss, the searches that run against data you already hold cost nothing per query. Then the fresh-data calls that do hit the API are cheap on their own. At 0.03 credits per result, the founder we spoke with anchored on this himself, telling us he could not "beat three credits for up to 100 people." A search that returns a hundred people costs three credits, and a search that returns nobody costs nothing.

You can also cap the blast radius in code. Set a hard limit on every API search so a single request can never fan out into thousands of billed results, cache fresh profiles into your own database so a repeated search reuses paid work, and meter usage per end user if you are reselling the search. The pricing is predictable because you decide where the API gets called and how wide it is allowed to go.

Where this fits in your stack

A natural-language search layer is the thinnest possible change to a workflow your recruiters already trust. The ATS stays. The system of record stays. What changes is the front door, from a filter form or a Boolean string to a sentence, and a thin piece of code behind it that runs the same structured query against your own data and a fresh-data API.

This week, take one search your recruiters run constantly and write it as a sentence, then map that sentence to the structured filters your ATS understands. This quarter, wire the fallback so the same filters reach a people search API when your own database returns too few matches, and write the new people back as records you own.

You do not need a large team to do this. The boutique firm we spoke with runs it with a couple of people, and described the goal as getting "done what a team of 10 gets done." A product lead and one engineer can build a working version on top of an API, because the hard part was never the code. It was having data fresh enough and connected enough to trust behind the search box, and that is the part you can now buy.

Crustdata is the data layer for recruiting teams adding natural-language search to the system they already run. Come and see what a plain-English query returns across your own target roles. Book a demo, or start free with 100 credits at crustdata.com.

Frequently asked questions

What is an ATS API in the context of candidate search?

It is the interface that lets you add search and data capabilities to your applicant tracking system without rebuilding it. In practice, recruiting teams use a people search API alongside their ATS to translate a plain-English request into a structured query, run it against their own records, and pull fresh candidate data when their database comes up short.

Do I have to replace my ATS to add natural-language search?

No. The teams we spoke with kept the system they ran and added a thin layer on top. The layer takes a sentence, turns it into structured filters with a language model, and runs those filters against your existing records first, then a people search API on a miss. Your ATS stays the system of record.

How does natural-language search turn a sentence into a query?

A language model does the translation, while the API does the data work. The model reads the recruiter's sentence and maps it onto structured criteria like title, seniority, location, and industry. The people search API then takes those structured filters and returns matching profiles. Keeping the two steps separate lets you reuse one model across your own database and a fresh-data provider.

What does it cost to run searches at scale?

Searching your own records costs nothing per query, and the fresh-data calls are billed per result. With Crustdata's people search API at 0.03 credits per result returned, a search that surfaces a hundred people costs three credits, and one that surfaces nobody costs nothing. Setting a hard limit per request and caching fresh profiles keeps the bill predictable even when end users run the searches.

Can I build this without engineers?

You can prototype it with no orchestration code at all. A Claude Code agent with Crustdata's MCP server configured handles the language translation and calls the data tools directly, so a recruiter types a sentence and gets people back. When you want full control over the fallback and the merge, you move to the direct REST path and write that logic yourself.

Data

Delivery Methods

Use Cases

Solutions