/v2/developers/enrichEnrich developers you already hold
search:readProfile and contribution detail for developer ids or logins you already hold. Contribution gardens are opt-in: depth=deep adds the garden only with garden (true, summary or full); without it deep is the enriched tier, and no garden is read or billed.
Query parameters
idsstringDeveloper ids, comma separated, at most 25. A developer's id is GitHub's public NUMERIC user id, stringified — deterministic, rename-proof, and exactly what `developer.developerId` carries on every response from this API, so ids from search paste straight in. Only have usernames? Use `logins` instead (mixing both is fine). Repeating an id changes nothing and costs nothing: ids are deduplicated before we fetch or bill.
max 1625 charsloginsstringGitHub usernames, comma separated, at most 25 — the friendly alternative to `ids`, freely mixable with it. Logins resolve to the same developers their numeric ids name; a login that resolves to nobody is omitted from the results and costs nothing, the same contract an unknown id has. Prefer `ids` when you carry results between calls: a login can be renamed and then belongs to someone else.
max 1625 charsdepthstring default: "core"Same ladder as search: `core` profile + repos + cracked score; `enriched` adds LinkedIn identity and socials; `deep` adds the contribution garden — the heavy block, so ask for it only when you need it. AI summaries and per-repo tags are NOT a depth tier: resolve them à-la-carte (their own call), each computed once.
coreenricheddeepfacetsstringÀ-la-carte facet keys to resolve ON TOP OF the depth tier, comma-separated, at most 5 — e.g. `tags.repos` (folds onto `details.githubProfile.repos[]`). The menu: `identity.linkedin`, `contact.socials`, `contact.emails`, `tags.repos`, `ai.person_summary`, `ai.repo_summaries`, `ai.match_rationale` (or a depth tier name); any other key is a 400. A facet already inside the chosen tier is a no-op.
max 605 charsgardenstringOpt in to each developer’s contribution garden. Off unless sent: a garden is a GitHub read and a large block for an agent to carry. `true` or `summary` returns the compact `gardenSummary` (totals, active days and weeks, the last 90 days, per-month counts); `full` returns the whole daily `garden`, for drawing a heatmap.
truefalsefullsummary
Responses
200The developers we hold, enriched to the requested depthresultsarray of PublicDeveloper requiredThe developers we hold data for, in the same shape a search result carries. Ids we hold nothing for are omitted rather than returned empty — and are not billed.
max 250 itemsidstring requiredThe stable GitHub numeric id for this developer. Immutable (unlike the login) — this is the handle to pass to every other endpoint.
max 64 charsloginstring requiredThe GitHub handle as of the last snapshot. Display it, do not key on it: handles are renameable and reusable.
max 200 charsnamestring | null requiredDisplay name as the developer set it on GitHub. Null when they set none.
max 200 charsavatarUrlstring | null requiredGitHub avatar image URL. Null when absent.
max 2048 charshasEmailboolean requiredWhether we hold at least one email address for this developer. It says we have an address on file, not that the address works — we do not check deliverability. The addresses themselves are never returned by a search or a base profile read: this boolean is the free signal, and the paid `contact.emails` facet is the only way to get them. Use `hasEmail` on a search request to filter to developers we hold an address for, without paying.
hasLinkedinboolean requiredWhether we hold a LinkedIn match for this developer — i.e. whether `enriched` will return `identity.*` and `contact.socials` for them. Available on the same base row as `hasEmail`: read both to decide what depth is worth requesting.
hasLocationboolean requiredWhether we hold any resolved location for this developer (from LinkedIn or a geocoded GitHub string). A free presence flag alongside `hasEmail`/`hasLinkedin`. The resolved `location` OBJECT rides `details.contact.location` at `enriched` (like the LinkedIn URL), not on the spine. Filter on it with `requireLocation=true`.
detailsPublicDetails requiredEvery detail about this developer beyond the identity spine, as a KEYED object grouped by section. A section and its members appear only when resolved; absent details are omitted entirely. The set grows with depth: `core` fills `githubProfile.*` and `score.cracked`; `enriched` adds `identity.*`, `contact.socials` and `contact.location`; `deep` adds `github.garden`. À-la-carte: `tags.repos` folds onto `githubProfile.repos[]`, the emails route fills `contact.emails`.
githubProfileobjectThe free GitHub profile: `githubProfile.core` (bio, org, languages, snapshot counts) and `githubProfile.repos`. Named for its source — everything here comes straight from GitHub.
coreobjectbio, currentRole, organization, university, languages, joinedAt, followers, following, totalStars.
reposarray of ProfileRepoWithTagsTop repositories (default 4), with `owned`/`commits` and, when resolved, `subjects`/`technologies` (`tags.repos`) and a plain-English `summary`/`themes` (`ai.repo_summaries`) folded on. The full set of blurbs is also on `details.ai.repo_summaries.value`.
namestring requiredThe repository name alone, without the owner (`linux`).
max 200 charsfullNamestring requiredGitHub `owner/name`. This is the join key: an evidence repo and a profile repo describing the same repository carry the same `fullName`.
max 200 charsdescriptionstring | null requiredThe repository's own GitHub description, verbatim. Null when it has none.
max 5000 charslanguagestring | null requiredGitHub's primary language for the repository. Null when GitHub reports none (an empty or docs-only repo); never inferred from the code.
max 200 charsstarsinteger requiredStargazer count at the time this profile was snapshotted, not at request time.
min 0, max 100000000commitsintegerHow many commits THIS developer authored to this repository over the trailing year. This is the difference between a project they wrote and a project they landed one drive-by fix in — a 385k-star repository with 4 of their commits is not their work. Absent when no commit signal was captured; 0 means none in the trailing year, which for older dormant work is not the same as none ever.
min 0, max 100000000ownedbooleanTrue when the repository sits under the developer's own account, false when it is someone else's (an organisation's or another person's) and they are a contributor to it. This is the difference between "they built this" and "they committed to this", and it is the ONLY honest basis for crediting a repository's stars to a person. Absent when no ownership signal was captured; treat absent as unknown, never as false.
urlstring requiredThe public github.com URL of the repository.
max 2048 charssubjectsarray of stringSubject/domain tags for this repository. Present only when `tags.repos` was resolved and this repo carried tags.
technologiesarray of stringTechnology tags for this repository. Present only when `tags.repos` was resolved and this repo carried tags.
summarystringA one-line, plain-English blurb on what this repository is, written for a non-technical reader. Present only when `ai.repo_summaries` was resolved and the model could describe this repo. Folded onto the repos shown here; `details.ai.repo_summaries.value.repoSummaries` lists every repository that was summarized, including ones past `repoLimit`.
themesarray of stringA few short themes the AI drew from this repository. Present alongside `summary` when `ai.repo_summaries` was resolved.
scoreobjectThe cracked score (`score.cracked`).
crackedobjectcrackedScore, tier.
identityobjectThe resolved external identity (`enriched`): `identity.linkedin`, `identity.experience`, `identity.education`.
linkedinobjectThe resolved LinkedIn identity scalars (linkedin, headline, title, company, seniority, expertise, location).
experiencearray of objectEmployment history, newest first. Present only when non-empty.
titlestring | nullJob title.
max 200 charscompanystring | nullEmployer name.
max 200 charsstartDatestring | nullStart date exactly as the source rendered it. Free text, not normalised.
max 200 charsendDatestring | nullEnd date, same unnormalised free-text form. Null on a current role.
max 200 charscurrentboolean | nullThe source's own "still there" flag.
educationarray of objectEducation history, in the order the source listed it. Present only when non-empty. Entries are not ordered by level and can include secondary schools, so read every entry when confirming a university or a graduation year.
schoolstring | nullInstitution name.
max 200 charsdegreestring | nullDegree awarded.
max 200 charsfieldOfStudystring | nullField of study.
max 200 charsstartDatestring | nullStart date as the source rendered it.
max 200 charsendDatestring | nullEnd date as the source rendered it.
max 200 chars
contactobjectContact channels: `contact.socials` (enriched), `contact.location` (enriched), `contact.emails` (the emails route).
socialsobjectgithub, linkedin social links.
emailsarray of stringEvery address we observe for this developer, ranked, the best one first. Present only when we hold at least one, resolved via the emails route.
bestEmailstring | nullThe address to use: the first unflagged personal address, else the first unflagged one; null when every address is flagged. On v2 only.
emailDetailsarray of objectEvery address with its type and flags, ranked, the most reliable first; within a source, personal, then work, then other. On v2 only.
addressstring requiredtypestring requiredpersonal: a free-mail or own-name domain; work: a company domain; other: academic or government.
personalworkotherflagsarray of string requiredWhy it may not reach this person. `name_mismatch`: it names neither their current name nor their login (a prior name, or someone else). `low_link_confidence`: it came through a LinkedIn link we are not sure of. `ambiguous_link`: they map to several LinkedIn profiles. Flagged addresses are kept, never dropped.
locationobject | nullWhere this developer is, resolved once and LinkedIn-preferred: `source` says whether the clean city/country came from their LinkedIn (`linkedin`) or from geocoding their GitHub location string (`github`). Present at `enriched`+ whenever `hasLocation` is true — the spine carries only the `hasLocation` flag. Note the GitHub geocode is rough (it often drops a country string into `city`); trust `source: "linkedin"` over `"github"`.
rawstring | null requiredThe location string exactly as the developer typed it on GitHub ("SF / remote").
max 200 charscitystring | null requiredCity resolved from `raw` by geocoding. Null when `raw` was empty or unresolvable.
max 200 charsstatestring | nullThe country subdivision (US state, province, region) as a full name, e.g. "Ohio". Present only from the clean LinkedIn overlay, for the developers we hold a match for. Absent otherwise: the GitHub geocoder does not reliably resolve it (so we do not surface its guess), and it is legitimately absent for city-states and metro-only profiles.
max 200 charscountrystring | null requiredCountry resolved from `raw`. Null when `raw` was empty or unresolvable. It can also be null while `city` is set, so read `city` and `raw` beside it before treating a location as unknown.
max 200 charssourcestringWhere the resolved city/country came from: `linkedin` when our LinkedIn overlay supplied it (clean, self-reported on LinkedIn), `github` when it is geocoded from the GitHub `raw` string. Most developers have no LinkedIn match, so `github` is the common case and its city/country are often sparse. Absent on unresolved payloads.
linkedingithub
githubobjectThe live-GitHub block (`deep`): `github.gardenSummary` by default, or the full `github.garden` with `garden=full`.
gardenobjectThe full contribution heatmap: every day’s count for the trailing year. Only with `garden=full`.
gardenSummaryGardenSummaryThe contribution garden as a compact read (the default `garden=summary`): totals, active days and weeks, the last 90 days, per-month counts.
fromstring requiredFirst day of the garden window, `YYYY-MM-DD`, inclusive.
tostring requiredLast day of the garden window, `YYYY-MM-DD`, inclusive.
totalinteger requiredPublic contributions across the window.
activeDaysinteger requiredDays in the window with at least one contribution.
activeWeeksinteger requiredDistinct weeks in the window with at least one contribution. Steady contributors score close to 52.
last90Daysinteger requiredContributions in the 90 days ending on `to`.
lastActiveDaystring | null requiredThe most recent day with a contribution, `YYYY-MM-DD`. Null when the window is empty.
byMonthobject requiredContributions per calendar month, `YYYY-MM` → count. Months with none are omitted.
fetchedAtstring requiredWhen the underlying garden was read from GitHub, as an ISO 8601 instant.
followersinteger | null requiredCurrent GitHub follower count, live. Null on older gardens.
totalStarsinteger | null requiredStars on repositories they OWN, live. Null on older gardens.
statusstringPresent only when the garden did not come back, in place of it: `pending` (being read, re-fetch after `retryAfterMs`), `failed` (the GitHub read errored; a retry can succeed), `unavailable` (no garden exists for this developer). None of the three is charged.
pendingfailedunavailableretryablebooleanWith `status`: whether re-fetching this call can return the garden.
retryAfterMsnumberWith `status: pending`: milliseconds to wait before re-fetching.
aiobjectÀ-la-carte AI prose. `person_summary`/`repo_summaries` come from the `/developers/summaries` route or `facets=ai.person_summary,ai.repo_summaries` on enrich, at every depth (never on the v2 search or similar). `match_rationale` comes from `facets=ai.match_rationale` on SEARCH, judged against your `q`. Compute is async, so each member carries a `status`: `ok` (the `value` is here), `pending` (with `retryAfterMs`: wait that long, then re-fetch this call once; the value is computed once, so a retry returns quickly), `failed` (with `retryable: true`: the write errored, and a re-fetch requests it again) or `unavailable` (with `retryable: true`: there is nothing to summarize yet, and a later read may return it). A member you asked for is never silently left out. A pending `person_summary`/`repo_summaries` can also be collected from `/developers/summaries` with the same ids: there, reading a developer is free while a summary your account has paid for is still being written, and for the one read that returns it finished. Read status before value.
person_summaryobjectA short pitch for this developer. On `ok`, `value` is `{ personSummary, headline, body? }`: `personSummary` is the whole pitch as one string of two lines, a headline (who they are across their career) over one or two plain sentences on the kind of work they do and their most notable projects. `headline` is the first line, ready to set as a title. `body` is the rest, absent when the pitch is one line.
statusstring required`ok`: `value` is here. `pending`: being written, re-fetch after `retryAfterMs`. `failed`: the write errored and a re-fetch requests it again. `unavailable`: there is nothing to summarize yet, and a later read may return it.
okpendingfailedunavailablevalueobjectOn `ok` only.
retryablebooleanOn `failed` and `unavailable`: whether re-fetching this call can return the value.
retryAfterMsnumberOn `pending` only: milliseconds to wait before re-fetching this call. The value is computed once, so a retry returns `ok` quickly once it lands.
repo_summariesobjectThe per-repo AI blurbs. On `ok`, `value` is `{ repoSummaries: [{ fullName, blurb, themes? }] }`, one entry for every repository that was summarized (not only the ones `repoLimit` shows). The same blurbs also fold onto `githubProfile.repos[].summary`/`.themes` for the repos shown.
statusstring required`ok`: `value` is here. `pending`: being written, re-fetch after `retryAfterMs`. `failed`: the write errored and a re-fetch requests it again. `unavailable`: there is nothing to summarize yet, and a later read may return it.
okpendingfailedunavailablevalueobjectOn `ok` only.
retryablebooleanOn `failed` and `unavailable`: whether re-fetching this call can return the value.
retryAfterMsnumberOn `pending` only: milliseconds to wait before re-fetching this call. The value is computed once, so a retry returns `ok` quickly once it lands.
match_rationaleobjectWhether and why this developer matches the search query, judged against it. SEARCH ONLY (there is no query to judge against on an id lookup). `value` is `{ matched, reasons? }`: a real match is `{ matched: true, reasons: ["Currently Staff Engineer at Stripe on payments", "Maintains a Go ISO-20022 library", "8 years shipping Go"] }` — a list of short, positive, recruiter-facing bullets (repos, languages, LinkedIn role/company, organisation, location, tenure). A weak/non-match is `{ matched: false }` (no reasons — an honest verdict, not a manufactured one). Check `matched` before reading `reasons`.
statusstring required`ok`: `value` is here. `pending`: being written, re-fetch after `retryAfterMs`. `failed`: the write errored and a re-fetch requests it again. `unavailable`: there is nothing to summarize yet, and a later read may return it.
okpendingfailedunavailablevalueobjectOn `ok` only.
retryablebooleanOn `failed` and `unavailable`: whether re-fetching this call can return the value.
retryAfterMsnumberOn `pending` only: milliseconds to wait before re-fetching this call. The value is computed once, so a retry returns `ok` quickly once it lands.
400Neither `ids` nor `logins` named anyonecodestring requiredStable machine-readable error code. Branch on this, never on the numeric status.
bad_requestunauthorizedsignature_requiredpayment_requiredforbiddennot_foundconflictgonepayload_too_largeunprocessable_entitytoo_many_requestsinternal_errornot_implementedbilling_unavailablenot_contactablemailbox_link_unavailablemailbox_requiredmail_engine_unavailablewebhook_publisher_unavailabledatabase_unavailableclient_errorbuild_failedself_serve_checkout_disabledwork_email_requiredtrial_unavailablecheckout_pendingchange_email_not_allowedmessagestring requiredHuman-readable explanation of the refusal.
statusinteger requiredThe HTTP status code, repeated in the body.
remedyobjectA self-serve path forward, when one exists (a 402 points at how to restore access).
kindstring requiredWhat kind of remedy this is, so a client can route it: whether the caller can clear the condition through the API, or a person must act in the web app.
topupconnect_mailboxbook_callcheckouturlstring requiredWhere to go to clear the condition: an API path, or a web app page when only a person can.
401Missing or invalid API keycodestring requiredStable machine-readable error code. Branch on this, never on the numeric status.
bad_requestunauthorizedsignature_requiredpayment_requiredforbiddennot_foundconflictgonepayload_too_largeunprocessable_entitytoo_many_requestsinternal_errornot_implementedbilling_unavailablenot_contactablemailbox_link_unavailablemailbox_requiredmail_engine_unavailablewebhook_publisher_unavailabledatabase_unavailableclient_errorbuild_failedself_serve_checkout_disabledwork_email_requiredtrial_unavailablecheckout_pendingchange_email_not_allowedmessagestring requiredHuman-readable explanation of the refusal.
statusinteger requiredThe HTTP status code, repeated in the body.
remedyobjectA self-serve path forward, when one exists (a 402 points at how to restore access).
kindstring requiredWhat kind of remedy this is, so a client can route it: whether the caller can clear the condition through the API, or a person must act in the web app.
topupconnect_mailboxbook_callcheckouturlstring requiredWhere to go to clear the condition: an API path, or a web app page when only a person can.
402Your plan does not include this capabilitycodestring requiredStable machine-readable error code. Branch on this, never on the numeric status.
bad_requestunauthorizedsignature_requiredpayment_requiredforbiddennot_foundconflictgonepayload_too_largeunprocessable_entitytoo_many_requestsinternal_errornot_implementedbilling_unavailablenot_contactablemailbox_link_unavailablemailbox_requiredmail_engine_unavailablewebhook_publisher_unavailabledatabase_unavailableclient_errorbuild_failedself_serve_checkout_disabledwork_email_requiredtrial_unavailablecheckout_pendingchange_email_not_allowedmessagestring requiredHuman-readable explanation of the refusal.
statusinteger requiredThe HTTP status code, repeated in the body.
remedyobjectA self-serve path forward, when one exists (a 402 points at how to restore access).
kindstring requiredWhat kind of remedy this is, so a client can route it: whether the caller can clear the condition through the API, or a person must act in the web app.
topupconnect_mailboxbook_callcheckouturlstring requiredWhere to go to clear the condition: an API path, or a web app page when only a person can.
403The key lacks search:readcodestring requiredStable machine-readable error code. Branch on this, never on the numeric status.
bad_requestunauthorizedsignature_requiredpayment_requiredforbiddennot_foundconflictgonepayload_too_largeunprocessable_entitytoo_many_requestsinternal_errornot_implementedbilling_unavailablenot_contactablemailbox_link_unavailablemailbox_requiredmail_engine_unavailablewebhook_publisher_unavailabledatabase_unavailableclient_errorbuild_failedself_serve_checkout_disabledwork_email_requiredtrial_unavailablecheckout_pendingchange_email_not_allowedmessagestring requiredHuman-readable explanation of the refusal.
statusinteger requiredThe HTTP status code, repeated in the body.
remedyobjectA self-serve path forward, when one exists (a 402 points at how to restore access).
kindstring requiredWhat kind of remedy this is, so a client can route it: whether the caller can clear the condition through the API, or a person must act in the web app.
topupconnect_mailboxbook_callcheckouturlstring requiredWhere to go to clear the condition: an API path, or a web app page when only a person can.
429More than 30 requests a minutecodestring requiredStable machine-readable error code. Branch on this, never on the numeric status.
bad_requestunauthorizedsignature_requiredpayment_requiredforbiddennot_foundconflictgonepayload_too_largeunprocessable_entitytoo_many_requestsinternal_errornot_implementedbilling_unavailablenot_contactablemailbox_link_unavailablemailbox_requiredmail_engine_unavailablewebhook_publisher_unavailabledatabase_unavailableclient_errorbuild_failedself_serve_checkout_disabledwork_email_requiredtrial_unavailablecheckout_pendingchange_email_not_allowedmessagestring requiredHuman-readable explanation of the refusal.
statusinteger requiredThe HTTP status code, repeated in the body.
remedyobjectA self-serve path forward, when one exists (a 402 points at how to restore access).
kindstring requiredWhat kind of remedy this is, so a client can route it: whether the caller can clear the condition through the API, or a person must act in the web app.
topupconnect_mailboxbook_callcheckouturlstring requiredWhere to go to clear the condition: an API path, or a web app page when only a person can.