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

WhatWhere
Productionhttps://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:

  1. Terms (skills, positions, … text) — what to search for. At least one positive term (all, any or preferred) in any group is required. A request with filters only returns 400.
  2. Filters (filters) — how to narrow down. All optional.
  3. 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:

KindExampleHow it is matched
Slug of a known entity — starts with id-id-python, id-google, id-berlin__berlin__germanyExact match of a database entity: every synonym and spelling. More reliable.
Free textpython, backend engineer, machine learningFull-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

KeyMeaningAvailable in
allEvery term must match (AND)skills only
anyAt least one term must match (OR)every group
noneNo term may match (NOT)every group
preferredDoes not filter, only ranks matches higherskills only

Different groups are joined with AND: skills.all = [python] + locations.any = [id-germany] — Python and Germany.

4.3. Term groups

GroupWhat is searchedNotes
skillsSkillsall supports min_years
positionsPositions over the whole career
last_positionsCurrent position
companiesCompanies over the whole career
last_companiesCurrent company
locationsPlace of residence: country, region or cityany supports radius_km
educationsSchools and universities
namesFirst and last name
textFree 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.

FieldUnit
ageyears
experience_yearstotal experience, years
months_on_last_positionmonths in the current position
graduation_yearcalendar 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.

FieldValuesNote
contactsemail, phone, messengers, any, noany — has some contact, no — has no contacts at all
seniorityjunior, middle, senior, team_lead, head_of, c_level, any, noLevel of the current position. any — the level is known, no — unknown
education_levelsbachelor, master, specialist, doctorate, csdegreeHighest degree. csdegree — a Computer Science degree of any level
company_size1-10, 11-200, 201-500, 501-1000, 1001-5000, 5001-10'000, 10'000+Plus the scope field, see below
genderfemaleDepends on the GDPR settings of the company
diversityasian, black, hispanic, indian, middleEast, whiteDepends on the GDPR settings of the company

company_size additionally takes scope — which companies to consider:

scopeMeaning
currentcurrent company (default)
previousprevious companies
anyany
"company_size": {"any": ["201-500", "501-1000"], "scope": "any"}

Open lists — {"any": [...], "none": [...]}

Values are identifiers from the AmazingHiring database (section 5.2).

FieldWhatExample value
industriesIndustries of the companiessoftware engineering
companiesCompanies over the whole career, by slugid-google
last_companiesCurrent company, by slugid-microsoft
educationsSchools and universities, by slugid-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"}
]
FieldValues
nameName of the language in English: English, German, Spanish
levelselem (elementary), limited (limited working), prof (professional working), full (full professional), native, unknown. Optional — without levels any level matches

Flags — true / false

Fieldtrue means
remoteOpen to remote work
freelancerFreelancer
frequent_job_changerChanges jobs more often than usual
exclude_multiple_locationsExclude profiles whose sources disagree on the location
exclude_linkedinExclude 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:

SchemaField
ContactKindEnumfilters.contacts
SeniorityEnumfilters.seniority
EducationLevelEnumfilters.education_levels
CompanySizeEnum, CompanySizeScopeEnumfilters.company_size
GenderEnumfilters.gender
DiversityEnumfilters.diversity
LanguageLevelEnumfilters.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:

  1. 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-use value term.

  2. From search results. The identifiers in the response are the same entities without the prefix:

    • results[].locations[].id"berlin__berlin__germany" → term id-berlin__berlin__germany;
    • results[].positions[].company.id"yandex" → term id-yandex.
  3. 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).

  4. 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"}
  ]
}
ParameterRequiredValues
typeyesskill, position, company, location, education
qyesBeginning of a name, 1–100 characters, no ,
typeWhat it completesWhere value goes
skillSkills and technologiesskills.all/any/none/preferred
positionJob titlespositions, last_positions
companyCompaniescompanies, last_companies, filters.companies, filters.last_companies
locationCities, regions, countrieslocations
educationSchools and universitieseducations, 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 an id-... 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 results means 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... } ]
}
FieldMeaning
countTotal number of matching profiles
page, per_pageAs in the request (or the defaults)
resultsProfiles 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.

FieldTypeDescription
idintProfile identifier — for GET /profiles/{id}/
namestringName
titlestring | nullProfile headline (usually "position at company")
general_infostring | nullShort summary
ageint | nullAge
birthdaystringYYYY-MM-DD, an empty string when the date is unknown
avatarsstring[] | nullPhoto URLs
languagesobject{"English": "native", ...}
locationsobject[]{id, name} — the id works as a slug with the id- prefix
positionsobject[]{position, description, company: {id, name, site}, start, end, skills}; current ones first; dates are YYYY-MM
educationsobject[]{name, faculty, specialization, degree, start, end}
courses_or_certificatesobject[]{name, organization_name, start, end}
skillsobject[]Programming languages only: {name, sources[], additional_skills[]}
all_skills_groupedobject[]All skills by group: {id, name, skills[]}
linksobject[]{value, personal_site} — links to the sources
resumesobject[]Resumes uploaded by your company
contacts_openedboolContacts 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

ParameterDefaultRange
page1≥ 1
per_page501–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 count you 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

CodeCauseWhat to do
400Invalid request: unknown field, invalid enum value, min > max, no positive term, per_page > 100Fix the request. The body names the offending field
401No Authorization header, wrong token, or the company has no API accessCheck the token
403No active license, or a GDPR restriction (see below)Contact the company administrator
402Contacts 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
429The daily limit of search results is exhaustedRetry in a day
502The search backend is unavailable or returned an errorRetry with exponential backoff (1 s, 2 s, 4 s…)
504The search backend did not answer in timeRetry 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.codeHTTPMeaning
1010429The daily limit of search results is exhausted
1011402Contacts / profiles quota is exhausted
1012402The Search API is not enabled for the company
100403GDPR: access to the profile is restricted
102403GDPR: the age filter is forbidden for your company
103403GDPR: the gender filter is forbidden
104403GDPR: 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 102104, 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/504 and network errors, with exponential backoff and a cap of 3–5 attempts. Do not retry 4xx.
  • Log the body of 400 errors — 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 count after changing slugs: a typo in id-... gives no error, it gives empty results.
  • Watch the schema GET /api/v6/schema/ on updates: new filters and values appear there first.