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

# Enrich Contact

> Find a contact's missing details from what you already know about them.

Find a contact's missing details from what you already know about them.

The person described in the body is saved as a contact in your workspace:
an existing contact is found by LinkedIn URL, then email, then name and
company domain, and is never overwritten. Its missing email is then asked
from Topo's lead database and an enrichment provider, which spends
enrichment credits, and saved on the contact. Use
`POST /v1/contacts/get-and-enrich` to start from a contact you already
store.

A lookup that is still running after about 20 seconds returns
`completed: false` with an `enrichment_id`: collect the answer with
`GET /v1/contacts/enrichments/{enrichment_id}` rather than enriching again.

Requires the `contacts:write` scope on the API key.


## OpenAPI

````yaml POST /v1/contacts/enrich
openapi: 3.1.0
info:
  title: Topo Public API
  description: >-
    Public REST API for Topo. All requests require a bearer API key issued from
    Settings → Developers. See the **Guides** in the left sidebar for
    authentication and rate-limit details.
  version: 1.0.0
servers:
  - url: https://api.topo.io
    description: Production
security:
  - BearerApiKey: []
tags:
  - name: Authentication
    description: >-
      Every request to the Topo Public API is authenticated with a bearer API
      key.


      ```http

      Authorization: Bearer topo_live_4f8a9c2b...

      ```


      Requests without a valid bearer token receive `401 Unauthorized`. After
      repeated failures with the same token, further attempts are
      short-circuited with `429 Too Many Requests` (`data.window_seconds: 60`)
      so a stale or malformed key cannot be retried in a tight loop.


      ### Issuing keys


      API keys are issued from the dashboard under **Settings → Developers → API
      keys**. Each key is bound to a single organization — the workspace it was
      created in — and cannot be used to read or write data from any other
      organization.


      The raw key is shown **once** at creation and never displayed again. If
      the raw key is lost, revoke it and issue a new one.


      ### Scopes


      Keys carry a granular scope list of `resource:action` pairs. A request to
      an endpoint whose required scope is not on the key returns `403
      Forbidden`.


      v1 ships with the following scopes:


      - `account_exclusions:read`, `account_exclusions:write`

      - `account_lists:read`, `account_lists:write`

      - `accounts:read`, `accounts:write`

      - `activities:read`

      - `contact_exclusions:read`, `contact_exclusions:write`

      - `contact_lists:read`, `contact_lists:write`

      - `contacts:read`, `contacts:write`

      - `crm:write`

      - `events:read`, `events:write`

      - `messages:read`

      - `sequences:read`, `sequences:write`

      - `signals:read`, `signals:write`

      - `tasks:read`, `tasks:write`

      - `users:read`

      - `webhooks:read`, `webhooks:write`


      Each endpoint's required scope is listed in its description.


      ### Inspecting the current key


      `GET /v1/me` returns the principal behind the bearer token: the key id,
      its name, the organization id and name, the id of the user who issued the
      key (`null` when it was not issued by a person), the scope list, the key
      prefix, the last-used timestamp, and the creation date. Use it to confirm
      a key is wired up correctly before debugging endpoint-level errors.
  - name: Rate limits
    description: >-
      Requests are rate-limited per organization across three concurrent
      windows. The budget is shared by every API key issued for the
      organization.


      | Window | Default limit |

      |---|---|

      | Per second | 10 requests |

      | Per minute | 60 requests |

      | Per day | 10,000 requests |


      A request is accepted only if it fits inside **all three** windows.
      Exceeding any one of them returns `429`.


      ### Response headers


      Every response carries the state of all three windows. Each window is
      suffixed with its name — `second`, `minute`, or `day`:


      ```http

      X-RateLimit-Limit-second: 5

      X-RateLimit-Remaining-second: 2

      X-RateLimit-Reset-second: 1729435920

      X-RateLimit-Limit-minute: 60

      X-RateLimit-Remaining-minute: 13

      X-RateLimit-Reset-minute: 1729435932

      X-RateLimit-Limit-day: 1000

      X-RateLimit-Remaining-day: 842

      X-RateLimit-Reset-day: 1729519200

      ```


      | Header | Meaning |

      |---|---|

      | `X-RateLimit-Limit-{window}` | Capacity of that window |

      | `X-RateLimit-Remaining-{window}` | Requests left in that window before
      the next `429` |

      | `X-RateLimit-Reset-{window}` | Unix timestamp (seconds) at which that
      window is fully refilled |


      Each window is a token bucket that refills continuously, so
      `X-RateLimit-Remaining-{window}` recovers gradually under steady traffic —
      it does not snap back at a fixed boundary.


      ### When the limit is exceeded


      Requests over the budget return `429` with the standard error envelope and
      a `Retry-After` header. `data.window_seconds` identifies the tier that
      tripped (`1`, `60`, or `86400`):


      ```http

      Retry-After: 3

      ```


      ```json

      {
        "status_code": 429,
        "type": "RateLimitIssue",
        "message": "Rate limit exceeded",
        "data": {
          "limit": 60,
          "reset_at": 1729435932,
          "window_seconds": 60
        },
        "request_id": "req_01HX7K..."
      }

      ```


      `Retry-After` is the number of seconds until the next request is allowed
      and is the recommended signal for backoff. `X-RateLimit-Reset` marks when
      the window is fully refilled, which is later than `Retry-After` whenever
      the bucket has more than one slot.
  - name: Task lifecycle
    description: >-
      A task represents one action a rep is expected to take — reply to a
      message, review a new lead, make a call, or complete a standalone to-do.
      This guide covers the states a task moves through, and how the wording
      used in the Topo app maps onto the values returned by this API.


      ### Statuses


      | `status` | Meaning |

      |---|---|

      | `PENDING` | Waiting for the rep. This is where a task starts. |

      | `IN_PROGRESS` | A call task the rep has started but not yet logged. |

      | `COMPLETED` | The rep acted on the task — a reply was sent, a lead
      approved or refused, a call logged. |

      | `SKIPPED` | The rep dismissed the task without acting on it. Shown as
      **Archived** in the app. |


      `PENDING` and `IN_PROGRESS` are the actionable states. `COMPLETED` and
      `SKIPPED` are terminal, but both can be reversed with `POST
      /v1/tasks/{task_id}/reopen`.


      ### App wording vs. API wording


      The Topo app calls the dismiss action **Archive**. On this API the same
      operation is called *skip*. They are the same thing — only the label
      differs:


      | In the Topo app | On this API |

      |---|---|

      | The **Archive** button | `POST /v1/tasks/{task_id}/skip` |

      | The **Archived** badge | `status: "SKIPPED"` |

      | "Archived 2 hours ago" | `skipped_at` |

      | — | webhook event `task.skipped` |


      These names are part of the frozen `/v1` contract and will not change
      within this major version. If you are mapping API data back to what a user
      sees in the app, treat `SKIPPED` and `skipped_at` as "archived".


      ### Ending a task


      | Action | Endpoint | Result |

      |---|---|---|

      | Act on it | `POST /v1/tasks/{task_id}/execute` | `COMPLETED` +
      `completed_at`, and the task's own side effects (message sent, lead
      enrolled or refused, call logged) |

      | Mark done without acting | `POST /v1/tasks/{task_id}/complete` |
      `COMPLETED` + `completed_at` |

      | Dismiss it (archive) | `POST /v1/tasks/{task_id}/skip` | `SKIPPED` +
      `skipped_at` |


      `execute` is the only one of the three that changes anything beyond the
      task itself. `complete` and `skip` record an outcome and nothing more.


      Both terminal states emit a webhook if you are subscribed:
      `task.completed` and `task.skipped` respectively.


      ### Reopening


      `POST /v1/tasks/{task_id}/reopen` returns a `COMPLETED` or `SKIPPED` task
      to `PENDING`. It clears `completed_at` and `skipped_at`, and sets
      `reopened_at`. A task that is already actionable cannot be reopened.


      `reopen_reason` distinguishes an automatic reopen from a manual one.
      `REMINDER_RULE` means Topo brought the task back on its own because a sent
      message went unanswered; `null` means a person reopened it.


      ### Snoozing


      Deferring a task ("Snooze") does **not** change its status. The task stays
      `PENDING` and drops out of the active queue until the chosen time passes,
      then reappears on its own.


      Use `POST /v1/tasks/{task_id}/snooze` with `{ "snoozed_until": "<ISO-8601
      timestamp>" }` to defer a task. The `snoozed_until` timestamp is also
      returned on task objects in `GET /v1/tasks` and `GET /v1/tasks/{task_id}`.
  - name: me
    description: >-
      Identity of the API key in use: the workspace it belongs to and its
      scopes.
  - name: search
    description: >-
      Find records by name across kinds and load any match back. A search result
      carries a typed id (`<type>:<uuid>`) that `GET /v1/fetch` resolves into
      the full record, so a name is enough to reach a lead, an account, a
      sequence template or a task without knowing which endpoint owns it.
  - name: users
    description: List users in the authenticated organization.
  - name: contacts
    description: Manage contact lists, list entries, and contact-level exclusions.
  - name: accounts
    description: Manage account lists, list entries, and account-level exclusions.
  - name: sequences
    description: Manage outbound sequences and inspect their execution state.
  - name: sequence-templates
    description: Manage reusable sequence templates that seed new sequences.
  - name: activities
    description: >-
      Read the immutable event log produced by outbound sequences (sent, opened,
      replied, meeting booked, etc.).
  - name: events
    description: >-
      Ingest first-party events (product usage, website activity, form
      submissions) and read them back to verify ingestion and resolution. Every
      well-formed event is accepted (HTTP 202) and stored. Subject correlation
      to contacts and accounts is asynchronous and best-effort. Replays with the
      same `external_event_id` or identical event content are deduplicated.
  - name: signals
    description: >-
      Create and run buying signals: a signal watches one catalog preset
      (hiring, funding, technology adoption, news, LinkedIn activity, or your
      own first-party events), checks it daily, and imports the qualified people
      or companies into a destination list it creates. Read the imports feed to
      see what each signal brought in.
  - name: webhooks
    description: Manage webhook subscriptions to receive outbound event notifications.
  - name: tasks
    description: List and inspect tasks in the workspace.
  - name: messages
    description: Read email and LinkedIn messages exchanged in outreach sequences.
paths:
  /v1/contacts/enrich:
    post:
      tags:
        - contacts
      summary: Enrich Contact
      description: >-
        Find a contact's missing details from what you already know about them.


        The person described in the body is saved as a contact in your
        workspace:

        an existing contact is found by LinkedIn URL, then email, then name and

        company domain, and is never overwritten. Its missing email is then
        asked

        from Topo's lead database and an enrichment provider, which spends

        enrichment credits, and saved on the contact. Use

        `POST /v1/contacts/get-and-enrich` to start from a contact you already

        store.


        A lookup that is still running after about 20 seconds returns

        `completed: false` with an `enrichment_id`: collect the answer with

        `GET /v1/contacts/enrichments/{enrichment_id}` rather than enriching
        again.


        Requires the `contacts:write` scope on the API key.
      operationId: enrich_contact_v1_contacts_enrich_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrichContactPublicInput'
        required: true
        description: Enrich Contact payload
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrichContactPublicOutput'
components:
  schemas:
    EnrichContactPublicInput:
      properties:
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
          description: Email, if known.
        linkedin_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Linkedin Url
          description: LinkedIn profile URL, if known.
        first_name:
          anyOf:
            - type: string
            - type: 'null'
          title: First Name
          description: First name, if known.
        last_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Name
          description: Last name, if known.
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Name
          description: Employer name, if known.
        company_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Domain
          description: Employer's primary domain, if known.
      additionalProperties: false
      type: object
      title: EnrichContactPublicInput
    EnrichContactPublicOutput:
      properties:
        completed:
          type: boolean
          title: Completed
          description: >-
            Whether enrichment finished. `false` while the lookup is still
            running: collect the answer with `enrichment_id`.
        enrichment_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Enrichment Id
          description: >-
            Set when `completed` is `false`: pass it to `GET
            /v1/contacts/enrichments/{enrichment_id}` to collect the answer
            instead of enriching again.
        contact_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Contact Id
          description: The workspace contact the answer was saved on.
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
          description: Email.
        first_name:
          anyOf:
            - type: string
            - type: 'null'
          title: First Name
          description: First name.
        last_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Name
          description: Last name.
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          title: Job Title
          description: Job title.
        linkedin_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Linkedin Url
          description: LinkedIn profile URL.
        phone_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Phone Number
          description: Phone number.
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Name
          description: Employer name.
        company_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Domain
          description: Employer's primary domain.
      type: object
      required:
        - completed
      title: EnrichContactPublicOutput
  securitySchemes:
    BearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: topo_live_<64hex>
      description: Bearer API key issued from Settings → Developers.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.