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

# Enroll contacts

> Enroll existing leads by `lead_ids`, raw numbers via `phone_numbers`, or both in
one request. Raw numbers are normalized to E.164 and matched against your existing
leads; a lead is created only when there is no match. Required scope
`campaigns:manage`.

Enrollment is per-row and idempotent: unparseable numbers, numbers on the
do-not-call list, and unknown lead IDs are reported in the response instead of
failing the batch, and re-enrolling an existing contact is a no-op. At most 1,000
contacts per request — send several requests for larger lists.

You can enroll into a `running` campaign; new contacts join the pacing queue. A
`completed`, `stopped` or `archived` campaign rejects enrollment.


Adds contacts to a campaign. Enroll leads you already have with `lead_ids`, raw phone numbers with `phone_numbers`, or both in one request. Numbers are matched to existing leads where possible and created as new leads otherwise.

The response is a **report, not a failure**: bad rows are described rather than aborting the batch, so a list with three unparseable numbers and two people on the do-not-call list still enrolls everybody else and tells you exactly who was skipped and why.

Enrollment works on a `draft` campaign and on a live one — adding contacts to a `running` campaign is supported and they join the queue at the normal pace.

Required scope: `campaigns:manage`.

## Path parameters

| Parameter | Type | Notes                                                |
| --------- | ---- | ---------------------------------------------------- |
| `id`      | uuid | Campaign ID. Must belong to the API key's workspace. |

## Body

At least one of `lead_ids` / `phone_numbers` must be present and non-empty. Their combined length may not exceed **1000 per request** — send several requests for a longer list.

| Field                   | Type    | Notes                                                                                                                                            |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lead_ids`              | uuid\[] | IDs of existing leads (`GET /leads`). IDs that do not exist in your workspace, or are deleted, come back in `not_found`.                         |
| `phone_numbers`         | array   | Each entry is either a bare phone string, or an object (below).                                                                                  |
| `phone_numbers[].phone` | string  | **Required** in object form. Any common format — `+972501234567`, `972501234567`, `0501234567` — normalized to E.164 before anything is written. |
| `phone_numbers[].name`  | string  | Optional. Used when a new lead is created.                                                                                                       |
| `phone_numbers[].email` | string  | Optional. Used when a new lead is created.                                                                                                       |
| `phone_numbers[].notes` | string  | Optional, truncated to 2000 characters. Becomes the lead's long-term context, which the agent can draw on during the call.                       |

Numbers are canonicalized **before** any lookup or insert, so the same person supplied as `0501234567` in one row and `+972501234567` in another resolves to one lead and one enrollment rather than two.

`name`, `email` and `notes` apply to **newly created** leads. When a number matches a lead you already have, the existing lead is enrolled as-is and its details are left untouched — update it through `PATCH /leads/{id}` if you need to change them.

## What gets skipped

| Reason                                   | Reported as                      | Behaviour                                                                                                                                                                                                        |
| ---------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unparseable phone number                 | `invalid_phone`                  | Skipped. Includes the offending input, and the `lead_id` when the bad number came from an existing lead.                                                                                                         |
| Lead ID not in this workspace            | `not_found`                      | Skipped.                                                                                                                                                                                                         |
| Number on the do-not-call list           | `on_do_not_call`                 | Skipped, and reported up front rather than surfacing later as blocked calls. Scope-limited entries are honoured against this campaign's agent.                                                                   |
| Already enrolled in this campaign        | `already_enrolled`               | No duplicate row, no error. Re-sending the same list is safe.                                                                                                                                                    |
| Already live in a **different** campaign | `409 ALREADY_IN_ACTIVE_CAMPAIGN` | A number can only be dialed by one active campaign at a time — otherwise the same person receives two campaigns' worth of attempts. Stop or finish the other campaign, or exclude the contact there, then retry. |

A verbal opt-out during a call ("stop calling me") adds that number to the do-not-call list automatically, workspace-wide, so it will be skipped by every future enrollment as well.

## Response fields

| Field              | Type      | Meaning                                                                               |
| ------------------ | --------- | ------------------------------------------------------------------------------------- |
| `campaign_id`      | uuid      | Echo of the path parameter.                                                           |
| `enrolled`         | integer   | Contacts newly added to this campaign.                                                |
| `already_enrolled` | integer   | Contacts that were already on this campaign.                                          |
| `leads_created`    | integer   | New leads created from `phone_numbers`.                                               |
| `leads_matched`    | integer   | `phone_numbers` entries that matched a lead you already had.                          |
| `invalid_phone`    | array     | Rows whose number could not be parsed.                                                |
| `on_do_not_call`   | string\[] | E.164 numbers skipped because they are suppressed.                                    |
| `not_found`        | string\[] | `lead_ids` that do not resolve in this workspace.                                     |
| `total_leads`      | integer   | Total contacts on the campaign after this request. Omitted when nothing was enrolled. |
| `company_id`       | uuid      | Workspace this response belongs to.                                                   |

## Example request

```bash theme={null}
curl -X POST "https://api.goyappr.com/campaigns/CAMPAIGN_ID/leads" \
  -H "Authorization: Bearer $YAPPR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lead_ids": [
      "a1c3e5f7-0000-4b21-9c30-000000000021",
      "a1c3e5f7-0000-4b21-9c30-000000000022"
    ],
    "phone_numbers": [
      { "phone": "0501234567", "name": "Dana Levi", "email": "dana@example.com", "notes": "Renewal due in August. Prefers mornings." },
      { "phone": "+972521112222", "name": "Yossi Mor" },
      "0539998888"
    ]
  }'
```

## Example response

```json theme={null}
{
  "campaign_id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
  "enrolled": 4,
  "already_enrolled": 1,
  "leads_created": 2,
  "leads_matched": 1,
  "invalid_phone": [],
  "on_do_not_call": ["+972539998888"],
  "not_found": [],
  "total_leads": 412,
  "company_id": "fe493f11-0000-0000-0000-000000000001"
}
```

A request where nothing survived the filters still returns 200 with the report, and without `total_leads`:

```json theme={null}
{
  "campaign_id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
  "enrolled": 0,
  "already_enrolled": 0,
  "leads_created": 0,
  "leads_matched": 0,
  "invalid_phone": [{ "phone": "not-a-number" }],
  "on_do_not_call": [],
  "not_found": ["a1c3e5f7-0000-4b21-9c30-0000000000ff"],
  "company_id": "fe493f11-0000-0000-0000-000000000001"
}
```

Always inspect the report rather than assuming 200 means everything landed.

## Errors

| HTTP | Code                         | When                                                                                                                                      |
| ---- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | —                            | Neither `lead_ids` nor `phone_numbers` supplied (or both empty).                                                                          |
| 400  | —                            | More than 1000 contacts in one request.                                                                                                   |
| 400  | —                            | Campaign is `completed`, `stopped` or `archived` — those cannot take new contacts.                                                        |
| 400  | —                            | Request body is not valid JSON.                                                                                                           |
| 401  | `INSUFFICIENT_SCOPE`         | API key lacks `campaigns:manage`.                                                                                                         |
| 404  | —                            | No campaign with that ID in this workspace, or it has been archived.                                                                      |
| 409  | `ALREADY_IN_ACTIVE_CAMPAIGN` | One or more numbers are live in another active campaign. The body carries the same report fields, so you can see how far the request got. |


## OpenAPI

````yaml POST /campaigns/{id}/leads
openapi: 3.1.0
info:
  title: Yappr API
  description: >
    Create and manage AI voice agents, purchase phone numbers, configure tools,
    and initiate calls — all via REST.
  version: 1.0.0
  contact:
    url: https://goyappr.com
servers:
  - url: https://api.goyappr.com
    description: Production
security:
  - apiKey: []
paths:
  /campaigns/{id}/leads:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Campaigns
      summary: Enroll contacts
      description: >
        Enroll existing leads by `lead_ids`, raw numbers via `phone_numbers`, or
        both in

        one request. Raw numbers are normalized to E.164 and matched against
        your existing

        leads; a lead is created only when there is no match. Required scope

        `campaigns:manage`.


        Enrollment is per-row and idempotent: unparseable numbers, numbers on
        the

        do-not-call list, and unknown lead IDs are reported in the response
        instead of

        failing the batch, and re-enrolling an existing contact is a no-op. At
        most 1,000

        contacts per request — send several requests for larger lists.


        You can enroll into a `running` campaign; new contacts join the pacing
        queue. A

        `completed`, `stopped` or `archived` campaign rejects enrollment.
      operationId: enrollCampaignContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: >-
                Supply `lead_ids`, `phone_numbers`, or both — at least one must
                be non-empty.
              properties:
                lead_ids:
                  type: array
                  description: IDs of leads that already exist in this workspace.
                  items:
                    type: string
                    format: uuid
                phone_numbers:
                  type: array
                  description: >
                    Numbers to enroll. Each entry is either a bare phone string
                    or an object. Any format is accepted and normalized to
                    E.164.
                  items:
                    oneOf:
                      - type: string
                        example: '+972501234567'
                      - type: object
                        required:
                          - phone
                        properties:
                          phone:
                            type: string
                            example: '0501234567'
                          name:
                            type: string
                            nullable: true
                          email:
                            type: string
                            format: email
                            nullable: true
                          notes:
                            type: string
                            nullable: true
                            description: >-
                              Becomes the lead's AI memory
                              (`long_term_context`), capped at 2,000 characters.
      responses:
        '200':
          description: >
            Enrollment report. Returned even when nothing was enrolled — read
            the counters and the rejection arrays to see why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollmentReport'
        '400':
          description: >
            Invalid JSON body, neither `lead_ids` nor `phone_numbers` supplied,
            more than 1,000 contacts in one request, or the campaign is
            `completed` / `stopped` / `archived`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >
            `ALREADY_IN_ACTIVE_CAMPAIGN` — one or more numbers are already
            enrolled in another active campaign. A number can only be dialed by
            one campaign at a time. The response also carries the partial
            enrollment report.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/CampaignError'
                  - $ref: '#/components/schemas/EnrollmentReport'
components:
  schemas:
    EnrollmentReport:
      type: object
      description: >
        Result of `POST /campaigns/{id}/leads`. Enrollment is per-row and

        best-effort: a bad phone number or a number on the do-not-call list is

        reported here rather than failing the whole batch, and re-enrolling an

        existing contact is idempotent.


        Numbers are canonicalized to E.164 before anything is written, so the
        same

        person cannot be enrolled twice under two formats.
      properties:
        campaign_id:
          type: string
          format: uuid
        enrolled:
          type: integer
          description: Contacts newly enrolled by this request.
        already_enrolled:
          type: integer
          description: Contacts that were already enrolled — no-ops, not errors.
        leads_created:
          type: integer
          description: >-
            New leads created from `phone_numbers` entries with no existing
            match.
        leads_matched:
          type: integer
          description: '`phone_numbers` entries matched to a lead you already had.'
        invalid_phone:
          type: array
          description: >-
            Rows rejected before enrollment because the number could not be
            parsed, or the lead could not be created.
          items:
            type: object
            properties:
              lead_id:
                type: string
                format: uuid
                description: Present when the row came from `lead_ids`.
              phone:
                type: string
              error:
                type: string
                description: Present when lead creation itself failed.
        on_do_not_call:
          type: array
          description: >
            Numbers excluded because they are on the workspace do-not-call list.
            A verbal opt-out on a call adds the number to that list
            automatically, workspace-wide, so previously-contacted people can
            appear here.
          items:
            type: string
            example: '+972501234567'
        not_found:
          type: array
          description: IDs from `lead_ids` that are not leads in this workspace.
          items:
            type: string
            format: uuid
        total_leads:
          type: integer
          description: >-
            Total contacts enrolled in the campaign after this request. Omitted
            when nothing was enrolled.
        company_id:
          type: string
          format: uuid
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
    CampaignError:
      type: object
      required:
        - error
        - message
      description: >
        Coded error envelope used by the campaign conflict and preflight
        responses. It

        differs from the generic `Error` shape: `error` carries the
        machine-readable code

        and `message` the human-readable explanation.


        - `ALREADY_IN_ACTIVE_CAMPAIGN` (409) — one or more numbers are already
        enrolled in
          another active campaign. A number can only be dialed by one campaign at a time.
        - `CAMPAIGN_NOT_READY` (422) — launch preflight failed; `message` names
        the single
          thing to fix.
      properties:
        error:
          type: string
          enum:
            - ALREADY_IN_ACTIVE_CAMPAIGN
            - CAMPAIGN_NOT_READY
        message:
          type: string
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: >-
        Your Yappr API key (e.g. `ypr_live_...`). Generate one in the dashboard
        under **Settings → API Keys**.

````