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

# Enroll Contact List Members

> Enroll many contacts of a list into a sequence template in one call.

Enroll many contacts of a list into a sequence template in one call.

Unlike `POST /v1/sequences`, which enrolls a single contact and fails on any
conflict, this reports per-outcome counts and applies `conflict_strategy` to
contacts already enrolled or recently contacted, so a large enrollment does
not stop at the first one that cannot start.

Each contact's eligibility is checked before it is enrolled, including an
email verification when its address was never verified. When the checks take
longer than about 20 seconds, the response returns what is known so far:
`queued_count` contacts are still being checked and are enrolled in the
background by the run `enrollment_run_id`. Follow them with
`GET /v1/sequences?contact_id=<uuid>`.

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


## OpenAPI

````yaml POST /v1/contact-lists/{contact_list_id}/enroll
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/contact-lists/{contact_list_id}/enroll:
    post:
      tags:
        - contacts
      summary: Enroll Contact List Members
      description: >-
        Enroll many contacts of a list into a sequence template in one call.


        Unlike `POST /v1/sequences`, which enrolls a single contact and fails on
        any

        conflict, this reports per-outcome counts and applies
        `conflict_strategy` to

        contacts already enrolled or recently contacted, so a large enrollment
        does

        not stop at the first one that cannot start.


        Requires the `sequences:write` scope on the API key.
      operationId: >-
        enroll_contact_list_members_v1_contact_lists__contact_list_id__enroll_post
      parameters:
        - name: contact_list_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
            title: Contact List Id
          description: ID of the contact list.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollContactListPublicInput'
        description: Enroll Contact List Members payload
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollContactListPublicOutput'
components:
  schemas:
    EnrollContactListPublicInput:
      properties:
        sequence_template_id:
          type: string
          format: uuid
          title: Sequence Template Id
          description: Sequence template to enroll the contacts into.
        contact_ids:
          items:
            type: string
            format: uuid
          type: array
          minItems: 1
          title: Contact Ids
          description: >-
            Contacts to enroll. Any id that is not a contact of this workspace
            is skipped.
        conflict_strategy:
          $ref: '#/components/schemas/EnrollmentConflictStrategy'
          description: What to do with contacts already enrolled or recently contacted.
          default: SKIP
        reenroll_cooldown_days:
          anyOf:
            - type: integer
              minimum: 0
            - type: 'null'
          title: Reenroll Cooldown Days
          description: >-
            How many days to wait before a contact can be enrolled again. Omit
            to use the workspace default.
      additionalProperties: false
      type: object
      required:
        - sequence_template_id
        - contact_ids
      title: EnrollContactListPublicInput
    EnrollContactListPublicOutput:
      properties:
        enrolled_count:
          type: integer
          title: Enrolled Count
          description: Contacts enrolled.
        skipped_count:
          type: integer
          title: Skipped Count
          description: >-
            Contacts left alone, because the id is unknown here or the conflict
            strategy said to skip them.
        replaced_count:
          type: integer
          title: Replaced Count
          description: Contacts whose existing sequence was replaced.
        enrolled_contact_ids:
          items:
            type: string
            format: uuid
          type: array
          title: Enrolled Contact Ids
          description: Contacts that were enrolled.
        created_sequence_ids:
          items:
            type: string
            format: uuid
          type: array
          title: Created Sequence Ids
          description: Sequence runs started by this call.
        enrollment_run_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Enrollment Run Id
          description: >-
            The background run that checks each contact's eligibility (email
            verification included) and enrolls it.
        queued_count:
          type: integer
          title: Queued Count
          description: >-
            Contacts still being checked when the call returned. They are
            enrolled in the background; list their sequences with `GET
            /v1/sequences?contact_id=` to follow them.
          default: 0
      type: object
      required:
        - enrolled_count
        - skipped_count
        - replaced_count
        - enrolled_contact_ids
        - created_sequence_ids
      title: EnrollContactListPublicOutput
    EnrollmentConflictStrategy:
      type: string
      enum:
        - SKIP
        - ADD_IF_ALL_COMPLETED
        - ADD_ANYWAY
        - REPLACE
      title: EnrollmentConflictStrategy
  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.