> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enrichloops.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Enrich a person

> Enqueues an enrichment for a person, identified by LinkedIn profile
URL or by work email address. Provide exactly one of `linkedinUrl` or
`email`.

Responds `200` if the task settled inside the request (a person we
already hold), or `202` if it was queued. Both responses share the same
shape. The body never carries the result, only the task. Poll `GET
/v1/tasks/{id}` or supply `loop_id` to get the result delivered to a
Loop's webhook.

**The two inputs take different routes.** A profile URL is an address
we can fetch. If we do not already hold that person, the task queues,
pulls their profile, and returns it.

An email address is resolved live (reverse email lookup). We identify
the company behind the domain, work out which names the local part
could describe, search that company's staff on LinkedIn, pull each
plausible profile, and confirm the person holds a current position
there under a name the address fits. Only a confirmed person is
returned. The result carries an `email_match` object: `method` is
`"exact"` for an address already on record, `"resolved"` for one
confirmed against a profile pulled for this request, `"inferred"` for
one recognized among people we already held.

**When we are not sure, we say so and do not charge.** If the address
fits more than one current employee and nothing separates them, the
task fails with `ambiguous`. `error` carries `candidates` (how many)
and `hintsAccepted`. Retry the same `email` with one of `fullName`,
`title` or `location` and we break the tie against the profiles we
already gathered. A miss is `not_found`. Both are refunded. An address
we cannot confidently resolve never costs you a credit.

Personal addresses (gmail, outlook, ...) and role addresses (info@,
sales@) are refused as `not_found` without any lookup.

An email lookup costs 3 credits. A URL lookup costs 1. Resolving an
address can take several LinkedIn retrievals.

**Freshness.** A stored profile older than 30 days is not served as-is.
The request is queued (`202`) and the profile is re-fetched before the
task settles. This matters more for people than for companies. A
person who changed jobs is not just out of date, and the new role is
usually the reason you asked. If the refresh cannot complete, the task
still succeeds and returns the stored record. Compare
`last_enriched_at` to tell the two apart. Email lookups follow the same
rule: an address that resolves to a person we already hold is
refreshed the same way.




## OpenAPI

````yaml /openapi.json post /v1/people/enrich
openapi: 3.1.0
info:
  title: EnrichLoops API
  version: 0.1.0
  description: |
    Send us a domain or a LinkedIn URL. We send back the company. Send us a
    LinkedIn profile URL or a work email. We send back the person. Results
    come to you directly, or to a webhook you register (a **Loop**).

    ## How it works

    1. You send an input: a domain, a LinkedIn URL, or a work email.
    2. We check what we already hold. If we have a fresh record, the task
       settles inside the same request.
    3. Otherwise we go and get it, store the result, and queue it for you.
    4. You collect the result by polling the task, or by having it delivered
       to your Loop's webhook.

    Every enrichment endpoint (`POST /v1/companies/enrich`, `POST
    /v1/people/enrich`, `POST /v1/loops/{id}/run`) responds with a task only.
    Never the enrichment data inline, even when the task settles instantly.
    `200` means it already settled and `202` means it was queued. Either way,
    fetch the result from `GET /v1/tasks/{id}` or receive it on your Loop's
    webhook.
  contact:
    name: EnrichLoops support
servers:
  - url: https://api.enrichloops.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Companies
    description: Enqueue a company enrichment.
  - name: People
    description: >-
      Enqueue a person enrichment, by LinkedIn profile URL or by work email
      address.
  - name: Tasks
    description: Poll or list the async tasks behind an enrichment.
  - name: Loops
    description: Webhook delivery. Send records to a Loop you created in the dashboard.
paths:
  /v1/people/enrich:
    post:
      tags:
        - People
      summary: Enrich a person
      description: |
        Enqueues an enrichment for a person, identified by LinkedIn profile
        URL or by work email address. Provide exactly one of `linkedinUrl` or
        `email`.

        Responds `200` if the task settled inside the request (a person we
        already hold), or `202` if it was queued. Both responses share the same
        shape. The body never carries the result, only the task. Poll `GET
        /v1/tasks/{id}` or supply `loop_id` to get the result delivered to a
        Loop's webhook.

        **The two inputs take different routes.** A profile URL is an address
        we can fetch. If we do not already hold that person, the task queues,
        pulls their profile, and returns it.

        An email address is resolved live (reverse email lookup). We identify
        the company behind the domain, work out which names the local part
        could describe, search that company's staff on LinkedIn, pull each
        plausible profile, and confirm the person holds a current position
        there under a name the address fits. Only a confirmed person is
        returned. The result carries an `email_match` object: `method` is
        `"exact"` for an address already on record, `"resolved"` for one
        confirmed against a profile pulled for this request, `"inferred"` for
        one recognized among people we already held.

        **When we are not sure, we say so and do not charge.** If the address
        fits more than one current employee and nothing separates them, the
        task fails with `ambiguous`. `error` carries `candidates` (how many)
        and `hintsAccepted`. Retry the same `email` with one of `fullName`,
        `title` or `location` and we break the tie against the profiles we
        already gathered. A miss is `not_found`. Both are refunded. An address
        we cannot confidently resolve never costs you a credit.

        Personal addresses (gmail, outlook, ...) and role addresses (info@,
        sales@) are refused as `not_found` without any lookup.

        An email lookup costs 3 credits. A URL lookup costs 1. Resolving an
        address can take several LinkedIn retrievals.

        **Freshness.** A stored profile older than 30 days is not served as-is.
        The request is queued (`202`) and the profile is re-fetched before the
        task settles. This matters more for people than for companies. A
        person who changed jobs is not just out of date, and the new role is
        usually the reason you asked. If the refresh cannot complete, the task
        still succeeds and returns the stored record. Compare
        `last_enriched_at` to tell the two apart. Email lookups follow the same
        rule: an address that resolves to a person we already hold is
        refreshed the same way.
      operationId: enrichPerson
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            Reusing the same key returns the original task instead of
            creating and charging a new one. Use it to retry a request whose
            response you never received.

            A key is bound to the body it was first sent with. Reusing it with
            a different body is rejected with `422`. Maximum 255 characters.
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                linkedinUrl:
                  type: string
                  description: >-
                    A LinkedIn profile URL, e.g.
                    `"https://www.linkedin.com/in/jane-doe"`.
                  example: https://www.linkedin.com/in/jane-doe
                email:
                  type: string
                  description: |
                    A work email address, as an alternative to `linkedinUrl`.
                    Resolved live against LinkedIn. See the endpoint
                    description for what happens on a miss or a tie.
                  example: jane.doe@acme.com
                fullName:
                  type: string
                  maxLength: 200
                  description: |
                    Optional hint for `email` lookups only. Read solely to
                    break a tie after an `ambiguous` result. Never used as a
                    search filter. Rejected alongside `linkedinUrl`.
                  example: Jane Marie Doe
                title:
                  type: string
                  maxLength: 200
                  description: >-
                    Optional tie-break hint for `email` lookups. The person's
                    job title.
                  example: Head of Finance
                location:
                  type: string
                  maxLength: 200
                  description: >-
                    Optional tie-break hint for `email` lookups. A city or
                    country.
                  example: Amsterdam
                loop_id:
                  type: string
                  format: uuid
                  description: |
                    Deliver the result to this Loop's webhook once it's
                    ready, in addition to polling this task directly.
              additionalProperties: false
      responses:
        '200':
          description: >-
            The task settled inside the request (a record we already hold, or an
            idempotent replay).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskEnqueueResponse'
        '202':
          description: The task was queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskEnqueueResponse'
        '400':
          description: >-
            Malformed request body (e.g. neither `linkedinUrl` nor `email`, or
            both, provided).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '422':
          $ref: '#/components/responses/IdempotencyKeyReused'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    TaskEnqueueResponse:
      type: object
      description: |
        Never carries `data`. Carries `error` only if the task already failed
        inside the request. Fetch the result from `GET /v1/tasks/{id}` or a
        Loop webhook.
      properties:
        task:
          $ref: '#/components/schemas/Task'
        error:
          $ref: '#/components/schemas/TaskError'
      required:
        - task
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    Task:
      type: object
      description: |
        `input` and `target` are dropped in future revisions to a single
        field; today `target` is the normalized domain or LinkedIn URL the
        task resolved to.
      properties:
        id:
          type: string
          format: uuid
        object_type:
          type: string
          enum:
            - company
            - person
        operation:
          type: string
          enum:
            - enrich
        status:
          $ref: '#/components/schemas/TaskStatus'
        input:
          type: object
          additionalProperties: true
        target:
          type: string
          description: >-
            The normalized input we resolved. The domain, LinkedIn URL or email
            you sent.
        credits_charged:
          type: integer
        loop_id:
          type:
            - string
            - 'null'
          format: uuid
        idempotency_key:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        queued_at:
          type:
            - string
            - 'null'
          format: date-time
        started_at:
          type:
            - string
            - 'null'
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: >-
            If the task is still running past this time, it will be marked
            `failed`.
      required:
        - id
        - object_type
        - operation
        - status
        - target
        - credits_charged
        - created_at
    TaskError:
      type: object
      properties:
        code:
          $ref: '#/components/schemas/TaskErrorCode'
        message:
          type: string
        candidates:
          type: integer
          description: |
            Present when `code` is `ambiguous`: how many current employees
            the address fit. Names are never included.
        hintsAccepted:
          type: array
          items:
            type: string
          description: |
            Present when `code` is `ambiguous`: the request fields that
            would break the tie on a retry (`fullName`, `title`, `location`).
      required:
        - code
        - message
    TaskStatus:
      type: string
      enum:
        - queued
        - running
        - succeeded
        - failed
        - canceled
    TaskErrorCode:
      type: string
      description: >-
        The same codes appear whether you poll the task or receive it on a
        webhook. Safe to branch on.
      enum:
        - not_found
        - ambiguous
        - provider_unavailable
        - timed_out
        - invalid_input
        - internal_error
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        text/plain:
          schema:
            type: string
            example: Unauthorized
    PaymentRequired:
      description: Insufficient or expired credits.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              code:
                type: string
                enum:
                  - credits_expired
                  - insufficient_credits
              creditsRemaining:
                type: number
            required:
              - error
    IdempotencyKeyReused:
      description: |
        This `Idempotency-Key` was already used with a different request
        body. A key identifies one request, so replaying it against different
        input would return a task for the wrong target. Nothing was charged
        and no task was created. Retry with a new key.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              code:
                type: string
                enum:
                  - idempotency_key_reused
            required:
              - error
    TooManyRequests:
      description: |
        Rate limited. `Retry-After` gives the number of seconds to wait.

        Two limits apply: one per client address and one per API key. A
        `code: "queue_full"` body means neither was exceeded. Instead the
        organization's queued-task backlog is full, and the fix is to let
        in-flight tasks drain rather than to slow the request rate.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              code:
                type: string
                enum:
                  - queue_full
              queuedTasks:
                type: integer
            required:
              - error
    InternalError:
      description: Something went wrong on our end.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              requestId:
                type: string
            required:
              - error
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Pass your API key as a bearer token: `Authorization: Bearer
        YOUR_API_KEY`. Generate and manage keys from your dashboard under
        **Settings → API Keys**.

````