Search API
Search API — User Guide
POST /api/v6/profiles/search/ finds candidates in the AmazingHiring database by a JSON query: skills, positions, companies, locations and the same filters the search page offers. Results carry no contacts; contacts are obtained with a separate GET /api/v6/profiles/{id}/ request. Slugs of the entities for a query come from GET /api/v6/suggestions/.
This document covers everything needed to start from scratch: access, request structure, every field and filter, where to get the values, the response format, limits and errors.
1. Access
Token
There is one token per company. A company administrator issues it on the Company → API management page (/company/api-management/) in the AmazingHiring UI. The company must have API access enabled for that — an AmazingHiring manager does it. The search endpoint POST /profiles/search/ is enabled separately, by the same manager; its current state is shown on the same API management page. Without it search answers 402.
Pass the token in the Authorization header with the literal Token:
Authorization: Token a0b1c2d3e4f5...
The token acts on behalf of the company and grants access to personal data. Keep it like a password. Regenerating the token disables every integration of the company.
Base URL and documentation
| What | Where |
|---|---|
| Production | https://search.amazinghiring.com |
| Interactive documentation (Swagger) | https://search.amazinghiring.com/api/v6/docs/ |
| OpenAPI schema (JSON/YAML) | https://search.amazinghiring.com/api/v6/schema/ |
The schema is generated from the code and is always up to date. Every closed list of values (enum) is in it — see section 5.
Swagger lets you call the endpoints right from the page: press Authorize, choose TokenAuth, paste Token a0b1c2d3e4f5 — then Try it out on any operation. Requests go to the host the documentation is opened on.
Constraints
- Server-to-server only: CORS is not configured, calling from a browser is not possible.
- Requests are synchronous; a search may take several seconds.
2. Quick start
Python developers in and around Berlin with a known email.
First the slug of the location — suggestions by the beginning of a name:
curl "https://search.amazinghiring.com/api/v6/suggestions/?type=location&q=berlin" \
-H "Authorization: Token a0b1c2d3e4f5"
{"results": [{"value": "id-berlin__berlin__germany", "name": "Berlin, Germany"}, ...]}
value of the first match goes into the search request as is:
curl -X POST https://search.amazinghiring.com/api/v6/profiles/search/ \
-H "Authorization: Token a0b1c2d3e4f5" \
-H "Content-Type: application/json" \
-d '{
"skills": {"all": ["python"]},
"locations": {"any": [{"value": "id-berlin__berlin__germany", "radius_km": 40}]},
"filters": {"contacts": {"any": ["email"]}},
"per_page": 20
}'
Response:
{
"count": 1342,
"page": 1,
"per_page": 20,
"results": [
{
"id": 6904,
"name": "Guido van Rossum",
"title": "Engineer at Dropbox",
"locations": [{"id": "berlin__berlin__germany", "name": "Berlin, Germany"}],
"positions": [...],
"contacts_opened": false,
...
}
]
}
Next — GET /api/v6/profiles/6904/ to obtain the contacts (see section 7).
3. Request structure
{
"skills": {"all": [...], "any": [...], "none": [...], "preferred": [...]},
"positions": {"any": [...], "none": [...]},
"last_positions": {"any": [...], "none": [...]},
"companies": {"any": [...], "none": [...]},
"last_companies": {"any": [...], "none": [...]},
"locations": {"any": [...], "none": [...]},
"educations": {"any": [...], "none": [...]},
"names": {"any": [...], "none": [...]},
"text": {"any": [...], "none": [...]},
"filters": { ... },
"page": 1,
"per_page": 50
}
Three parts:
- Terms (
skills,positions, …text) — what to search for. At least one positive term (all,anyorpreferred) in any group is required. A request with filters only returns400. - Filters (
filters) — how to narrow down. All optional. - Pagination (
page,per_page).
The request is strict: an unknown key at any level is a 400 error with the text Unknown field.. A typo never silently turns into an empty result.
4. Terms
4.1. What a term is
A term is a string or an object {"value": "...", ...}. The two forms are equivalent; the object is needed when there is a qualifier (min_years, radius_km).
"python"
{"value": "python"}
{"value": "id-python", "min_years": 3}
The value is one of two kinds:
| Kind | Example | How it is matched |
|---|---|---|
Slug of a known entity — starts with id- | id-python, id-google, id-berlin__berlin__germany | Exact match of a database entity: every synonym and spelling. More reliable. |
| Free text | python, backend engineer, machine learning | Full-text search. Several words are matched as a phrase. |
Where to get slugs — section 5.2.
Constraints on the value:
- 1 to 100 characters;
- no comma;
- does not start with
-.
Each list (all, any, …) holds at most 50 terms.
4.2. Operator logic
| Key | Meaning | Available in |
|---|---|---|
all | Every term must match (AND) | skills only |
any | At least one term must match (OR) | every group |
none | No term may match (NOT) | every group |
preferred | Does not filter, only ranks matches higher | skills only |
Different groups are joined with AND: skills.all = [python] + locations.any = [id-germany] — Python and Germany.
4.3. Term groups
| Group | What is searched | Notes |
|---|---|---|
skills | Skills | all supports min_years |
positions | Positions over the whole career | |
last_positions | Current position | |
companies | Companies over the whole career | |
last_companies | Current company | |
locations | Place of residence: country, region or city | any supports radius_km |
educations | Schools and universities | |
names | First and last name | |
text | Free text over the whole profile |
4.4. Term qualifiers
min_years — least years of experience with the skill. Only in skills.all. Allowed values: 2, 3, 4, 5.
"skills": {"all": [{"value": "id-go", "min_years": 3}]}
radius_km — also search within a radius around the city. Only in locations.any, cities only (ignored for a country or region). Allowed values: 20, 40, 80, 160.
"locations": {"any": [{"value": "id-munich__bavaria__germany", "radius_km": 80}]}
Any other min_years / radius_km value is rejected with 400.
5. Filters
The filters object. Every field is optional.
5.1. Full list
Ranges
Format: {"min": N, "max": N, "include_unknown": bool}. Either bound is optional, both inclusive. min > max is an error. include_unknown: true adds profiles whose value is unknown to the results.
| Field | Unit |
|---|---|
age | years |
experience_years | total experience, years |
months_on_last_position | months in the current position |
graduation_year | calendar year of graduation |
"experience_years": {"min": 5},
"age": {"min": 25, "max": 45, "include_unknown": true}
Closed lists (enum) — {"any": [...], "none": [...]}
any — at least one of the values, none — none of them.
| Field | Values | Note |
|---|---|---|
contacts | email, phone, messengers, any, no | any — has some contact, no — has no contacts at all |
seniority | junior, middle, senior, team_lead, head_of, c_level, any, no | Level of the current position. any — the level is known, no — unknown |
education_levels | bachelor, master, specialist, doctorate, csdegree | Highest degree. csdegree — a Computer Science degree of any level |
company_size | 1-10, 11-200, 201-500, 501-1000, 1001-5000, 5001-10'000, 10'000+ | Plus the scope field, see below |
gender | female | Depends on the GDPR settings of the company |
diversity | asian, black, hispanic, indian, middleEast, white | Depends on the GDPR settings of the company |
company_size additionally takes scope — which companies to consider:
scope | Meaning |
|---|---|
current | current company (default) |
previous | previous companies |
any | any |
"company_size": {"any": ["201-500", "501-1000"], "scope": "any"}
Open lists — {"any": [...], "none": [...]}
Values are identifiers from the AmazingHiring database (section 5.2).
| Field | What | Example value |
|---|---|---|
industries | Industries of the companies | software engineering |
companies | Companies over the whole career, by slug | id-google |
last_companies | Current company, by slug | id-microsoft |
educations | Schools and universities, by slug | id-stanford-university |
How filters.companies differs from the companies term: the term is a field of the search bar (a slug or free text), the filter is the checkboxes of the filter panel in the UI and takes only identifiers from it. If you know the slug, use the term; the filter exists to reproduce a query built through the filter panel.
Profile sources — sites
{"all": [...], "none": [...]} — domains the profile was collected from. all — the profile exists on every listed site (AND), none — on none of them.
"sites": {"all": ["github.com", "stackoverflow.com"], "none": ["linkedin.com"]}
Languages — languages
A list of objects. Every listed language is required (AND between languages); levels within a language are OR.
"languages": [
{"name": "English", "levels": ["full", "native"]},
{"name": "German"}
]
| Field | Values |
|---|---|
name | Name of the language in English: English, German, Spanish… |
levels | elem (elementary), limited (limited working), prof (professional working), full (full professional), native, unknown. Optional — without levels any level matches |
Flags — true / false
| Field | true means |
|---|---|
remote | Open to remote work |
freelancer | Freelancer |
frequent_job_changer | Changes jobs more often than usual |
exclude_multiple_locations | Exclude profiles whose sources disagree on the location |
exclude_linkedin | Exclude profiles found on LinkedIn only |
false — explicitly require the opposite. To not filter by a flag, simply do not pass it.
5.2. Where to get the values
Closed lists
The values are listed above and fixed in the OpenAPI schema GET /api/v6/schema/ — the components.schemas section:
| Schema | Field |
|---|---|
ContactKindEnum | filters.contacts |
SeniorityEnum | filters.seniority |
EducationLevelEnum | filters.education_levels |
CompanySizeEnum, CompanySizeScopeEnum | filters.company_size |
GenderEnum | filters.gender |
DiversityEnum | filters.diversity |
LanguageLevelEnum | filters.languages[].levels |
Allowed min_years and radius_km are in the SkillTermField / LocationTermField schemas (also enum). The schema is the source of truth: if a list grows, it changes there.
Slugs (id-...)
A slug is id- + the identifier of an entity in the AmazingHiring database. Ways to get one:
-
Suggestions —
GET /api/v6/suggestions/(section 5.3). The primary way: pass the beginning of a name and the kind of entity, get the list of known entities with a ready-to-usevalueterm. -
From search results. The identifiers in the response are the same entities without the prefix:
results[].locations[].id→"berlin__berlin__germany"→ termid-berlin__berlin__germany;results[].positions[].company.id→"yandex"→ termid-yandex.
-
From the address bar of the search page. Build a query in the AmazingHiring UI through its suggestions — the URL of the results page carries
q=parameters with the same slugs (location[0]:id-united-states,skillAll[0]:id-python). -
Free text. If the slug is unknown, pass the name as text:
"python","google","berlin". It works, but catches only the literal spelling.
The format of location slugs is city__region__country; for a country just id-germany, for a region id-bavaria__germany. Slugs of companies and schools are Latin letters joined with hyphens: id-google, id-stanford-university.
An unknown slug raises no error — it simply matches nothing, and the results are empty or incomplete. Check count.
Industries, sites, languages
industries— lowercase English names as in the filter panel of the UI:software engineering,fintech. There is no public list (/suggestions/has no industries); take them from the URL of the search page (f=industry[0]:...).sites— domain names:github.com,stackoverflow.com,linkedin.com,habr.com.languages[].name— the English name of the language, capitalized.
5.3. Suggestions — GET /suggestions/
Autocomplete of the entities the search understands. Pass the kind and the beginning of a name — get the known entities, best matches first.
curl "https://search.amazinghiring.com/api/v6/suggestions/?type=location&q=berl" \
-H "Authorization: Token a0b1c2d3e4f5"
{
"results": [
{"value": "id-berlin__berlin__germany", "name": "Berlin, Germany"},
{"value": "id-berlin__coos-county__new-hampshire__united-states", "name": "Berlin, New Hampshire, USA"}
]
}
| Parameter | Required | Values |
|---|---|---|
type | yes | skill, position, company, location, education |
q | yes | Beginning of a name, 1–100 characters, no , |
type | What it completes | Where value goes |
|---|---|---|
skill | Skills and technologies | skills.all/any/none/preferred |
position | Job titles | positions, last_positions |
company | Companies | companies, last_companies, filters.companies, filters.last_companies |
location | Cities, regions, countries | locations |
education | Schools and universities | educations, filters.educations |
value— a ready-to-use term: put it into a search request as is, without processing. For locations, companies and schools it is anid-...slug; for skills and positions it is the canonical name, which the search matches to the entity rather than to the spelling.name— a human-readable name to show to a user.- An empty
resultsmeans nothing matched. The language of the suggestions follows the language of the token's user. - Suggestions do not spend the daily search limit. Cache the results on your side: the dictionary changes rarely.
6. Response
{
"count": 1342,
"page": 1,
"per_page": 50,
"results": [ { ...profile... } ]
}
| Field | Meaning |
|---|---|
count | Total number of matching profiles |
page, per_page | As in the request (or the defaults) |
results | Profiles of the current page |
Profile fields in the results
The same structure as GET /api/v6/profiles/{id}/, minus contacts and comments, plus contacts_opened.
| Field | Type | Description |
|---|---|---|
id | int | Profile identifier — for GET /profiles/{id}/ |
name | string | Name |
title | string | null | Profile headline (usually "position at company") |
general_info | string | null | Short summary |
age | int | null | Age |
birthday | string | YYYY-MM-DD, an empty string when the date is unknown |
avatars | string[] | null | Photo URLs |
languages | object | {"English": "native", ...} |
locations | object[] | {id, name} — the id works as a slug with the id- prefix |
positions | object[] | {position, description, company: {id, name, site}, start, end, skills}; current ones first; dates are YYYY-MM |
educations | object[] | {name, faculty, specialization, degree, start, end} |
courses_or_certificates | object[] | {name, organization_name, start, end} |
skills | object[] | Programming languages only: {name, sources[], additional_skills[]} |
all_skills_grouped | object[] | All skills by group: {id, name, skills[]} |
links | object[] | {value, personal_site} — links to the sources |
resumes | object[] | Resumes uploaded by your company |
contacts_opened | bool | Contacts are already opened by your company — GET /profiles/{id}/ is free |
Links are masked. By default links[].value is not the direct URL of the source but a proxy link like https://search.amazinghiring.com/api/profiles/{id}/links/{hash}/. It redirects to the original when opened. This is a company setting; to receive direct URLs, contact your AmazingHiring manager.
Profiles that requested deletion of their data (opt-out) are not returned — so a page may hold fewer than per_page items while the next pages are not empty.
7. Obtaining contacts
Contacts are the paid part. Search does not return them.
curl https://search.amazinghiring.com/api/v6/profiles/6904/ \
-H "Authorization: Token a0b1c2d3e4f5"
The response is the full profile with the contacts field:
"contacts": [
{"type": "email", "value": "[email protected]"},
{"type": "phone", "value": "+1..."},
{"type": "skype", "value": "..."}
]
Billing rules:
- If the search results show
contacts_opened: true, the contacts are already opened by your company and the request is free. - If
false, 1 credit of the company's contacts quota is spent. Repeated requests for the same profile are free. - Separately, a daily limit of profiles opened through the API applies (400 per company per day by default).
- Limit exhausted →
402(see section 9).
The GET /profiles/{id}/ request is "open the contacts". Do not call it "just to look": the charge happens on GET.
Whether opening is worth it can be checked with the contacts filter in the search itself: {"contacts": {"any": ["email"]}} returns only profiles with a known email.
8. Pagination and the search limit
| Parameter | Default | Range |
|---|---|---|
page | 1 | ≥ 1 |
per_page | 50 | 1–100 |
To walk the whole result set, increase page while page * per_page < count.
Every received page spends the company's daily limit of search results (15,000 profiles a day by default). The number of profiles actually returned is charged. When exhausted — 429; the limit resets a day after the last request.
In practice:
- Use
per_page: 100— the limit is spent on profiles, not on requests; large pages are simply faster. - Repeating a byte-for-byte identical request (same terms, filters,
page,per_page) does not spend the limit again — the history entry is overwritten. Any difference is a new request. - Narrow the query with filters down to the
countyou need instead of exporting everything.
The limit is counted per the token's user. The company token is a separate service user, so manual searches by employees in the UI do not interfere with the integration and vice versa.
GET /suggestions/ does not count towards the limit of results.
9. Errors
| Code | Cause | What to do |
|---|---|---|
400 | Invalid request: unknown field, invalid enum value, min > max, no positive term, per_page > 100 | Fix the request. The body names the offending field |
401 | No Authorization header, wrong token, or the company has no API access | Check the token |
403 | No active license, or a GDPR restriction (see below) | Contact the company administrator |
402 | Contacts quota or the daily limit of profiles through the API is exhausted (GET /profiles/{id}/), or the Search API is not enabled for the company (POST /profiles/search/, GET /suggestions/, code 1012) | Wait for the reset / raise the quota; for 1012 contact your AmazingHiring manager |
429 | The daily limit of search results is exhausted | Retry in a day |
502 | The search backend is unavailable or returned an error | Retry with exponential backoff (1 s, 2 s, 4 s…) |
504 | The search backend did not answer in time | Retry with a delay; simplify the query |
Validation error format (400)
Standard: the key is the path to the field, the value is a list of messages.
{"skill": ["Unknown field."]}
{"skills": {"all": {"0": ["\"min_years\" must be one of [2, 3, 4, 5]."]}}}
{"non_field_errors": ["At least one term to search for is required."]}
{"filters": {"seniority": {"any": {"0": ["\"lead\" is not a valid choice."]}}}}
Limit and access error format
{"status": {"code": 1010, "message": "Request was throttled."}}
status.code | HTTP | Meaning |
|---|---|---|
1010 | 429 | The daily limit of search results is exhausted |
1011 | 402 | Contacts / profiles quota is exhausted |
1012 | 402 | The Search API is not enabled for the company |
100 | 403 | GDPR: access to the profile is restricted |
102 | 403 | GDPR: the age filter is forbidden for your company |
103 | 403 | GDPR: the gender filter is forbidden |
104 | 403 | GDPR: the diversity filter is forbidden |
The age, gender and diversity filters are not available to every company — the GDPR settings of the account govern that. If you get 102–104, drop the filter or discuss the settings with your manager.
10. Examples
Senior Go backend engineer, not from Google, speaks English
{
"skills": {
"all": [{"value": "id-go", "min_years": 3}, "kubernetes"],
"any": ["postgresql", "mysql"],
"none": ["php"],
"preferred": ["grpc"]
},
"last_positions": {"any": ["backend engineer", "backend developer"], "none": ["intern"]},
"companies": {"none": ["id-google"]},
"filters": {
"seniority": {"any": ["senior", "team_lead"]},
"experience_years": {"min": 5},
"months_on_last_position": {"min": 12},
"languages": [{"name": "English", "levels": ["full", "native"]}],
"remote": true
},
"per_page": 100
}
Data scientist from top universities, with a phone, active on GitHub
{
"positions": {"any": ["data scientist", "ml engineer"]},
"educations": {"any": ["id-mit", "id-stanford-university"]},
"filters": {
"contacts": {"any": ["phone"]},
"education_levels": {"any": ["master", "doctorate"]},
"sites": {"all": ["github.com"]}
}
}
Everyone currently working at a given company
{
"last_companies": {"any": ["id-yandex"]},
"per_page": 100
}
Estimate the volume before exporting
A request with per_page: 1 spends one profile of the limit, while count returns the full size of the result set.
{
"skills": {"all": ["id-rust"]},
"locations": {"any": ["id-germany"]},
"per_page": 1
}
11. Integration recommendations
- Retry only on
502/504and network errors, with exponential backoff and a cap of 3–5 attempts. Do not retry4xx. - Log the body of
400errors — it points at the exact field. - Slug > text. Where a slug is known, use it: a text match depends on the spelling in the source. Take slugs from
GET /suggestions/and cache them — the dictionary changes rarely. - Check
countafter changing slugs: a typo inid-...gives no error, it gives empty results. - Watch the schema
GET /api/v6/schema/on updates: new filters and values appear there first.