---
title: "Search developers by location, role, skills, and languages"
method: POST
path: "/v1/search"
---

# Search developers by location, role, skills, and languages

`POST /v1/search`

## Request

- Base URL: `https://api.githunt.ai`
- URL: `https://api.githunt.ai/v1/search`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-Api-Key`

## Request body

- SearchRequest: At least one of `location`, `locations`, or `regions` is required.
  - `location` string
  - `role` string
  - `skills` string[]
  - `languages` string[]
  - `minExperienceYears` integer
  - `isHireable` boolean
  - `strictSkills` boolean
  - `includeUnlocated` boolean: When true, also search candidates indexed with no stated location. Their location is unknown, so they are returned as screening leads and can never be counted as geographically verified; `meta.coverage.unlocated_included` marks a response that contains them.
  - `maxResults` integer
  - `offset` integer: Number of top-ranked candidates to skip, for paging. Use with maxResults to fetch the next page. Consecutive pages never overlap.
  - `excludeLogins` string[]: GitHub logins to leave out of results, e.g. candidates already seen in a prior call. Use for dedup across searches. Case-insensitive; invalid entries are dropped rather than rejected.
  - `excludeLocations` string[]: Countries or cities to exclude, matched the same way as the location parameter (aliases and native names included).
  - `excludeUtcOffsets` object: Exclude candidates whose stated location resolves entirely within this UTC offset band, in hours. A location that only partially overlaps the band, or can't be resolved, is kept (not excluded) - a candidate whose commit timezone falls inside the band but whose location is unresolvable comes back flagged with `location_resolution.screening_reason`, never dropped.
    - `min` number, required
    - `max` number, required
  - `locations` string[]: Additional cities/countries to search in one call, freeform like `location`, merged with it (max 10 combined). Live GitHub crawl only covers the first location; further locations are served from the candidate pool only. At least one of `location`, `locations`, or `regions` is required.
  - `regions` string[]: Named region shortcuts that expand to their member countries, merged with `location`/`locations` (max 10 combined locations after expansion; extras are dropped). Case-insensitive. A request whose region names are all unknown and that carries no `location` is rejected with `invalid_request`.
  - `organizations` string[]: GitHub org logins to check candidates' employer affinity against. Boosts matching candidates' rank unless `requireOrgAffinity` is true.
  - `sectors` string[]: Industry sectors to check candidates' employer affinity against, via GitHunt's employer graph (public GitHub orgs mapped to companies and sectors).
  - `requireOrgAffinity` boolean: When true, only return candidates matching one of the given `organizations`/`sectors` (hard filter) instead of just boosting their rank.
  - `domains` string[]: Domain vocabulary packs to score candidates' repos/bio against for subject-matter evidence (e.g. core payments-processing terminology, not just the word "payments").
  - `skillRecency` SkillRecencyEntry[]: Filter on public skill activity. Entries are ANDed: a candidate must satisfy every entry to be returned. Only candidates whose signals positively support every entry are returned, and only they occupy a slot in the per-location selection the search scores, so a candidate with evidence is never crowded out by higher-activity candidates without it. Candidates whose signals cannot answer the question are `unknown` and are left out of the results (and queued for enrichment), which is not a negative finding - see `meta.skill_recency`. `unknown` covers a signal that is missing, failed, expired (signals are refreshed on a 30-day cycle, and one older than that no longer answers a strict filter) or partial (the years the answer depends on could not be fetched). Malformed entries are an uncharged `invalid_request`. When `skillRecency` is set, the search is served from the candidate pool alone: a live GitHub row carries no derived signals and could only ever be `unknown`.
    - `skill` string, required: Language or skill name. Common aliases resolve to the canonical GitHub Linguist name (ts, js, py, rb, kt, rs, sh, cpp, c#, golang).
    - `minActiveYears` integer: Minimum number of distinct calendar years with public activity in this skill (a year counts at five or more commits). This is a public-activity measure, never elapsed or commercial tenure.
    - `usedWithinYears` integer: Require dated activity inside the rolling window of this many years ending at the request's UTC timestamp. Calendar-year subtraction in UTC, clamping Feb 29 to Feb 28 in a non-leap target year; the cutoff day itself counts as inside the window.
  - `excludeList` string: Name of a seen list (see /v1/lists) whose logins are merged into `excludeLogins` for this search, so a rerun of the same brief never returns candidates the account already recorded. Only permanent `seen` entries and claims that have not expired are excluded: an interrupted run's candidates are rediscoverable as soon as the claim lapses. The server-side list is bounded at 5000 eligible logins rather than the 500-entry client cap; `meta.exclude_list.truncated` reports when that bound was hit. An unknown list is a `not_found` before any quota is charged, so create the list first with an empty `POST /v1/lists/{name}/logins`.

## Response `200`

Search results

- SearchResponse
  - `success` true
  - `data` object
    - `matchedCount` integer: Results in this page.
    - `totalCount` integer
    - `offset` integer: Echoes the applied offset.
    - `locationSuggestions` string[]: Similar supported locations ("City, Country"), present only when the search returned zero results because the location matched no known city or country.
    - `results` SearchResult[]
      - `login` string
      - `score` number
      - `profile_score` number
      - `tech_stack_score` number
      - `activity_score` number
      - `github_experience` number
      - `matching_keywords` string: Comma-separated list of matched keywords
      - `avatar_url` string
      - `email` string, nullable
      - `name` string, nullable
      - `company` string, nullable
      - `location` string, nullable
      - `followers` integer
      - `bio` string, nullable
      - `profile_url` string
      - `blog` string
      - `twitter_username` string
      - `hireable` 'Yes' | 'No'
      - `created_at` string, date-time
      - `updated_at` string, date-time
      - `last_active_date` string, nullable: Date of the candidate's most recent GitHub activity. Usually a date (YYYY-MM-DD); may be a full ISO date-time when derived from fallback sources.
      - `commits_per_month` number, nullable: Average commits per month over recent history
      - `commit_frequency_label` string, nullable: Human label for commit activity (e.g. Very Active, Active, Unknown)
      - `semantic_commit_percentage` number, nullable: Percentage of analyzed commits with semantic/descriptive messages
      - `commit_message_quality_label` string, nullable: Human label for commit message quality (e.g. Excellent, Good, Insufficient Data)
      - `top_repositories` object[]: The candidate's top repositories, as shown on the website candidate card
        - `name` string
        - `stars` integer
        - `language` string, nullable
      - `top_oss_contributions` object[]: Notable open-source projects the candidate contributed to
        - `repository` string: Full repository name (owner/name)
        - `tier_label` string: Project prominence tier (e.g. Elite, Popular, Notable)
        - `commits` integer
        - `language` string, nullable
      - `evidence` object[]: Where each matched skill/keyword was found on the candidate.
        - `keyword` string
        - `source` 'repo_name' | 'repo_description' | 'repo_language' | 'bio' | 'company' | 'skill_fallback'
        - `repo` string: Bare repository name (no owner), present when the match came from a repository. Use `top_repositories` or the evidence report for owner-qualified names.
        - `snippet` string: Short surrounding text for bio/company matches
      - `org_affinity` OrgAffinity: Present when `organizations` or `sectors` were given in the request.
        - `score` number
        - `matches` object[]
          - `org` string
          - `company` string
          - `sector` string
      - `domain_evidence` DomainEvidence: Present when `domains` were given, keyed by pack name (e.g. "payments").
      - `inferred_utc_offset` InferredUtcOffset, nullable: UTC offset inferred from the candidate's own commit author timestamps, used when GitHub's `location` field is empty or unresolvable. Null when no signal is available.
        - `offset` number
        - `confidence` 'none' | 'low' | 'medium' | 'high'
        - `sample_size` integer: Number of commits the inference was based on
        - `share` number: Fraction of sampled commits at the dominant offset
        - `latest_commit` string, date-time
      - `location_resolution` LocationResolution: How the candidate's stated base resolved at the run timestamp. Attached to every result whenever the request carried an exclusion or a `skillRecency` filter. Discovery stays permissive: `ambiguous` and `unresolved` candidates are still returned, and belong in a screening queue rather than a geographically eligible shortlist. Commit timezone is never `resolved` - an author-side commit offset can support discovery but cannot establish a country or a current base, and never excludes a candidate on its own - see `screening_reason`.
        - `status` 'resolved' | 'ambiguous' | 'unresolved'
        - `country` string, nullable: Null unless the stated location settles a single country.
        - `utc_offset` object, nullable: Offset band in hours at the run timestamp, daylight saving applied. `min` equals `max` when the location pins a single zone.
          - `min` number
          - `max` number
        - `basis` 'stated_location' | 'commit_timezone', nullable
        - `screening_reason` 'inferred_offset_in_excluded_band', nullable: Why this candidate needs a screening question before being counted as geographically eligible. `inferred_offset_in_excluded_band` means the stated location did not resolve and the candidate's commit timezone falls inside the `excludeUtcOffsets` band the request asked to exclude. Such candidates are deliberately returned rather than excluded: a commit offset cannot establish a country or a current base, so it is a question to ask, not an answer. Null when no screening flag applies.
        - `observed_at` string, date-time: The run's frozen evaluation instant.
      - `rank_breakdown` object: Present when the request named languages, skills, sectors, organizations or domains. Components in [0,1] of the evidence ranking formula rank = base * (1 + 0.6*recency + 0.4*org + 0.5*domain); unknown evidence scores 0 and never lowers a row.
        - `base` number
        - `recency` number
        - `org` number
        - `domain` number
        - `final` number
      - `score_boosts` object: Ranking boosts applied on top of the base score (present only when non-zero). `score` stays clamped to 0-100. When the request names languages, skills, sectors, organizations or domains, ordering is not this boosted value: results are ranked by the base score scaled by how recently the requested skills were used and how strongly the candidate matches the requested employers and domains, so a result may sit above one with a higher `score`.
        - `org_affinity` number
        - `domain_evidence` number
  - `meta` object
    - `requestId` string
    - `dataSource` string
    - `quota` Quota
      - `used` integer
      - `limit` integer: -1 means unlimited
      - `month` string
      - `cost` integer: Credits this request debited (search: 1, get user: 1, evidence report: 3 when a report is returned and 0 while pending, analyze profile: 3, list operations: 0). `used` already reflects this charge.
      - `rollover` integer: Unused credits banked from the previous month and available on top of `limit` this month (`remaining` = limit + rollover - used). Only present on the Team plan, capped at one extra month's allowance (so at most 2x the allowance is usable in a month).
    - `coverage` object: What the search actually read, so an empty page is never mistaken for an exhaustive absence.
      - `locations_searched` integer
      - `locations_dropped` integer: Locations left unsearched because region expansion exceeded the per-request location cap.
      - `partial` boolean: True when retrieval was bounded (a capped partition, a per-location read cap, a timed-out location read, or a failed pool read).
      - `candidates_fetched` integer: Pool candidates that passed the request's exclusions across all searched locations, before ranking.
      - `candidates_scored` integer: Candidates handed to the ranker. Scaled with the number of locations under a per-request ceiling and the time budget, so a region search does not split one location's budget across its countries.
      - `unlocated_included` boolean: Present only when the request set `includeUnlocated`. Results may include candidates with no stated location, which are screening leads, not verified geography.
    - `skill_recency` object: Present only when the request carried `skillRecency`. Counts come from every candidate seen in the queried partitions, not from the returned page.
      - `evaluated` integer
      - `supported` integer
      - `unknown` integer: Candidates whose signals could not answer the filter, because they are missing, failed, expired (older than the 30-day signal refresh window) or partial (the years the answer depends on could not be fetched). Not a negative finding; they are excluded from the scored selection, and queued for enrichment so a later search can judge them on evidence.
      - `contradicted` integer: Candidates the signals positively rule out. A bound that only an unfetched year could have met is `unknown`, not `contradicted`.
      - `partial` boolean: True when retrieval was bounded (a capped partition or a failed pool read), so an empty page is not evidence of absence.
    - `exclude_list` object: Present only when the request carried `excludeList`.
      - `name` string
      - `logins` integer: Logins merged into the exclusion from the list.
      - `truncated` boolean: True when the list exceeded the 5000-login read bound.

## Other responses

- `400`: Invalid request
- `401`: Missing, invalid, or revoked API key
- `429`: Monthly or daily quota exceeded
- `500`: Unexpected server error

---

[API](https://skmtc.dev/githunt/apis/githunt-api.md) · [All operations](https://skmtc.dev/githunt/apis/githunt-api/llms.txt) · [OpenAPI document](https://skmtc.dev/githunt/apis/githunt-api/revisions/6fc325607da0?raw)
