Documentation navigation
GET /v2/developers/enrich

Enrich developers you already hold

Access
Authenticated
search:read
Cost
Free
Rate limit
30 / minute per account
Quota
None

Profile 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

  • ids string

    Developer 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 chars
  • logins string

    GitHub 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 chars
  • depth string 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.

    coreenricheddeep
  • facets string

    À-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 chars
  • garden string

    Opt 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

  • 200 The developers we hold, enriched to the requested depth
    • results array of PublicDeveloper required

      The 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 items
      • id string required

        The stable GitHub numeric id for this developer. Immutable (unlike the login) — this is the handle to pass to every other endpoint.

        max 64 chars
      • login string required

        The GitHub handle as of the last snapshot. Display it, do not key on it: handles are renameable and reusable.

        max 200 chars
      • name string | null required

        Display name as the developer set it on GitHub. Null when they set none.

        max 200 chars
      • avatarUrl string | null required

        GitHub avatar image URL. Null when absent.

        max 2048 chars
      • hasEmail boolean required

        Whether 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.

      • hasLinkedin boolean required

        Whether 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.

      • hasLocation boolean required

        Whether 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`.

      • details PublicDetails required

        Every 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`.

        • githubProfile object

          The free GitHub profile: `githubProfile.core` (bio, org, languages, snapshot counts) and `githubProfile.repos`. Named for its source — everything here comes straight from GitHub.

          • core object

            bio, currentRole, organization, university, languages, joinedAt, followers, following, totalStars.

          • repos array of ProfileRepoWithTags

            Top 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`.

            • name string required

              The repository name alone, without the owner (`linux`).

              max 200 chars
            • fullName string required

              GitHub `owner/name`. This is the join key: an evidence repo and a profile repo describing the same repository carry the same `fullName`.

              max 200 chars
            • description string | null required

              The repository's own GitHub description, verbatim. Null when it has none.

              max 5000 chars
            • language string | null required

              GitHub's primary language for the repository. Null when GitHub reports none (an empty or docs-only repo); never inferred from the code.

              max 200 chars
            • stars integer required

              Stargazer count at the time this profile was snapshotted, not at request time.

              min 0, max 100000000
            • commits integer

              How 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 100000000
            • owned boolean

              True 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.

            • url string required

              The public github.com URL of the repository.

              max 2048 chars
            • subjects array of string

              Subject/domain tags for this repository. Present only when `tags.repos` was resolved and this repo carried tags.

            • technologies array of string

              Technology tags for this repository. Present only when `tags.repos` was resolved and this repo carried tags.

            • summary string

              A 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`.

            • themes array of string

              A few short themes the AI drew from this repository. Present alongside `summary` when `ai.repo_summaries` was resolved.

        • score object

          The cracked score (`score.cracked`).

          • cracked object

            crackedScore, tier.

        • identity object

          The resolved external identity (`enriched`): `identity.linkedin`, `identity.experience`, `identity.education`.

          • linkedin object

            The resolved LinkedIn identity scalars (linkedin, headline, title, company, seniority, expertise, location).

          • experience array of object

            Employment history, newest first. Present only when non-empty.

            • title string | null

              Job title.

              max 200 chars
            • company string | null

              Employer name.

              max 200 chars
            • startDate string | null

              Start date exactly as the source rendered it. Free text, not normalised.

              max 200 chars
            • endDate string | null

              End date, same unnormalised free-text form. Null on a current role.

              max 200 chars
            • current boolean | null

              The source's own "still there" flag.

          • education array of object

            Education 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.

            • school string | null

              Institution name.

              max 200 chars
            • degree string | null

              Degree awarded.

              max 200 chars
            • fieldOfStudy string | null

              Field of study.

              max 200 chars
            • startDate string | null

              Start date as the source rendered it.

              max 200 chars
            • endDate string | null

              End date as the source rendered it.

              max 200 chars
        • contact object

          Contact channels: `contact.socials` (enriched), `contact.location` (enriched), `contact.emails` (the emails route).

          • socials object

            github, linkedin social links.

          • emails array of string

            Every address we observe for this developer, ranked, the best one first. Present only when we hold at least one, resolved via the emails route.

          • bestEmail string | null

            The address to use: the first unflagged personal address, else the first unflagged one; null when every address is flagged. On v2 only.

          • emailDetails array of object

            Every address with its type and flags, ranked, the most reliable first; within a source, personal, then work, then other. On v2 only.

            • address string required
            • type string required

              personal: a free-mail or own-name domain; work: a company domain; other: academic or government.

              personalworkother
            • flags array of string required

              Why 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.

          • location object | null

            Where 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"`.

            • raw string | null required

              The location string exactly as the developer typed it on GitHub ("SF / remote").

              max 200 chars
            • city string | null required

              City resolved from `raw` by geocoding. Null when `raw` was empty or unresolvable.

              max 200 chars
            • state string | null

              The 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 chars
            • country string | null required

              Country 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 chars
            • source string

              Where 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
        • github object

          The live-GitHub block (`deep`): `github.gardenSummary` by default, or the full `github.garden` with `garden=full`.

          • garden object

            The full contribution heatmap: every day’s count for the trailing year. Only with `garden=full`.

          • gardenSummary GardenSummary

            The contribution garden as a compact read (the default `garden=summary`): totals, active days and weeks, the last 90 days, per-month counts.

            • from string required

              First day of the garden window, `YYYY-MM-DD`, inclusive.

            • to string required

              Last day of the garden window, `YYYY-MM-DD`, inclusive.

            • total integer required

              Public contributions across the window.

            • activeDays integer required

              Days in the window with at least one contribution.

            • activeWeeks integer required

              Distinct weeks in the window with at least one contribution. Steady contributors score close to 52.

            • last90Days integer required

              Contributions in the 90 days ending on `to`.

            • lastActiveDay string | null required

              The most recent day with a contribution, `YYYY-MM-DD`. Null when the window is empty.

            • byMonth object required

              Contributions per calendar month, `YYYY-MM` → count. Months with none are omitted.

            • fetchedAt string required

              When the underlying garden was read from GitHub, as an ISO 8601 instant.

            • followers integer | null required

              Current GitHub follower count, live. Null on older gardens.

            • totalStars integer | null required

              Stars on repositories they OWN, live. Null on older gardens.

          • status string

            Present 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.

            pendingfailedunavailable
          • retryable boolean

            With `status`: whether re-fetching this call can return the garden.

          • retryAfterMs number

            With `status: pending`: milliseconds to wait before re-fetching.

        • ai object

          À-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_summary object

            A 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.

            • status string 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.

              okpendingfailedunavailable
            • value object

              On `ok` only.

            • retryable boolean

              On `failed` and `unavailable`: whether re-fetching this call can return the value.

            • retryAfterMs number

              On `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_summaries object

            The 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.

            • status string 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.

              okpendingfailedunavailable
            • value object

              On `ok` only.

            • retryable boolean

              On `failed` and `unavailable`: whether re-fetching this call can return the value.

            • retryAfterMs number

              On `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_rationale object

            Whether 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`.

            • status string 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.

              okpendingfailedunavailable
            • value object

              On `ok` only.

            • retryable boolean

              On `failed` and `unavailable`: whether re-fetching this call can return the value.

            • retryAfterMs number

              On `pending` only: milliseconds to wait before re-fetching this call. The value is computed once, so a retry returns `ok` quickly once it lands.

  • 400 Neither `ids` nor `logins` named anyone
    • code string required

      Stable 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_allowed
    • message string required

      Human-readable explanation of the refusal.

    • status integer required

      The HTTP status code, repeated in the body.

    • remedy object

      A self-serve path forward, when one exists (a 402 points at how to restore access).

      • kind string required

        What 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_callcheckout
      • url string required

        Where to go to clear the condition: an API path, or a web app page when only a person can.

  • 401 Missing or invalid API key
    • code string required

      Stable 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_allowed
    • message string required

      Human-readable explanation of the refusal.

    • status integer required

      The HTTP status code, repeated in the body.

    • remedy object

      A self-serve path forward, when one exists (a 402 points at how to restore access).

      • kind string required

        What 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_callcheckout
      • url string required

        Where to go to clear the condition: an API path, or a web app page when only a person can.

  • 402 Your plan does not include this capability
    • code string required

      Stable 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_allowed
    • message string required

      Human-readable explanation of the refusal.

    • status integer required

      The HTTP status code, repeated in the body.

    • remedy object

      A self-serve path forward, when one exists (a 402 points at how to restore access).

      • kind string required

        What 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_callcheckout
      • url string required

        Where to go to clear the condition: an API path, or a web app page when only a person can.

  • 403 The key lacks search:read
    • code string required

      Stable 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_allowed
    • message string required

      Human-readable explanation of the refusal.

    • status integer required

      The HTTP status code, repeated in the body.

    • remedy object

      A self-serve path forward, when one exists (a 402 points at how to restore access).

      • kind string required

        What 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_callcheckout
      • url string required

        Where to go to clear the condition: an API path, or a web app page when only a person can.

  • 429 More than 30 requests a minute
    • code string required

      Stable 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_allowed
    • message string required

      Human-readable explanation of the refusal.

    • status integer required

      The HTTP status code, repeated in the body.

    • remedy object

      A self-serve path forward, when one exists (a 402 points at how to restore access).

      • kind string required

        What 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_callcheckout
      • url string required

        Where to go to clear the condition: an API path, or a web app page when only a person can.