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

# Campaign audience

> Manage the contact lists that define who a campaign calls.

A contact list holds the phone numbers a campaign calls. You build a list by uploading a CSV file: create the list to get a presigned upload URL, upload the file, wait for validation, then confirm the upload to import the contacts.

## Base URL

```
https://api.persistence.dev
```

<Note>
  These endpoints are served under the public `/v1` prefix.
</Note>

## Authentication

Every request requires a workspace API key in the `X-API-Key` header.

```bash theme={null}
curl https://api.persistence.dev/v1/calls/contact-lists \
  --header "X-API-Key: $PERSISTENCE_API_KEY"
```

## Endpoints

<CardGroup cols={1}>
  <Card title="List contact lists" icon="list" href="/api-reference/campaign-audience/list-contact-lists">
    `GET /v1/calls/contact-lists` — page through contact lists.
  </Card>

  <Card title="Create contact list" icon="plus" href="/api-reference/campaign-audience/create-contact-list">
    `POST /v1/calls/contact-lists` — create a list and get a presigned upload URL.
  </Card>

  <Card title="Get contact list" icon="file-text" href="/api-reference/campaign-audience/get-contact-list">
    `GET /v1/calls/contact-lists/{id}` — retrieve a list and its upload status.
  </Card>

  <Card title="Update contact list" icon="pencil" href="/api-reference/campaign-audience/update-contact-list">
    `PATCH /v1/calls/contact-lists/{id}` — change a list name or type.
  </Card>

  <Card title="Delete contact list" icon="trash-2" href="/api-reference/campaign-audience/delete-contact-list">
    `DELETE /v1/calls/contact-lists/{id}` — delete a list.
  </Card>

  <Card title="List contact list items" icon="phone" href="/api-reference/campaign-audience/list-contact-list-items">
    `GET /v1/calls/contact-lists/{id}/items` — page through the phone numbers in a list.
  </Card>

  <Card title="Create upload URL" icon="link" href="/api-reference/campaign-audience/create-upload-url">
    `POST /v1/calls/contact-lists/{id}/get-presign-url` — get a presigned URL for a new file.
  </Card>

  <Card title="Confirm upload" icon="check" href="/api-reference/campaign-audience/confirm-upload">
    `POST /v1/calls/contact-lists/{id}/uploads/{uploadID}/confirm` — import a validated upload.
  </Card>

  <Card title="Stream upload status" icon="activity" href="/api-reference/campaign-audience/stream-upload-status">
    `GET /v1/calls/contact-lists/{id}/uploads/{uploadID}/events` — follow upload progress over SSE.
  </Card>
</CardGroup>

## Upload a contact list

<Steps>
  <Step title="Create the list">
    Send `POST /v1/calls/contact-lists` with `name`, `type`, and the `fileName` of the CSV you intend to upload. The response returns the new list along with `uploadUrl`, `uploadHeaders`, and `s3Key`, plus an `uploadDetails` object whose `id` is the `uploadID` used by the remaining steps.

    ```bash theme={null}
    curl --request POST https://api.persistence.dev/v1/calls/contact-lists \
      --header "X-API-Key: $PERSISTENCE_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "name": "Spring outreach",
        "type": "contacts",
        "fileName": "contacts.csv"
      }'
    ```
  </Step>

  <Step title="Upload the file">
    Send the CSV file directly to `uploadUrl`, including every header returned in `uploadHeaders`. This request goes to the storage provider, so do not send your API key with it.

    ```bash theme={null}
    curl --request PUT "$UPLOAD_URL" \
      --header "Content-Type: text/csv" \
      --upload-file contacts.csv
    ```
  </Step>

  <Step title="Wait for validation">
    After the file lands, the upload moves through the statuses below. Follow the progress with `GET /v1/calls/contact-lists/{id}/uploads/{uploadID}/events`, or poll `GET /v1/calls/contact-lists/{id}` and read `uploadDetails.status`.
  </Step>

  <Step title="Confirm the upload">
    Once the upload reaches `validated`, call `POST /v1/calls/contact-lists/{id}/uploads/{uploadID}/confirm` to import the rows into the list. Confirming before validation completes returns a `409`.

    ```bash theme={null}
    curl --request POST \
      "https://api.persistence.dev/v1/calls/contact-lists/$LIST_ID/uploads/$UPLOAD_ID/confirm" \
      --header "X-API-Key: $PERSISTENCE_API_KEY"
    ```
  </Step>
</Steps>

To replace or extend the contents of an existing list, start at step 2 by requesting a fresh URL from `POST /v1/calls/contact-lists/{id}/get-presign-url`.

## Contact list fields

| Field           | Type    | Description                                   |
| --------------- | ------- | --------------------------------------------- |
| `id`            | string  | Contact list ID.                              |
| `name`          | string  | Contact list name.                            |
| `type`          | string  | List type: `contacts`, `dnd`, or `batch`.     |
| `status`        | string  | List status: `draft`, `ready`, or `archived`. |
| `totalEntries`  | integer | Number of entries in the list.                |
| `uploadDetails` | object  | The most recent upload for the list.          |
| `createdAt`     | string  | Creation timestamp.                           |
| `updatedAt`     | string  | Last update timestamp.                        |

Use `contacts` for the audience a campaign calls and `dnd` for numbers a campaign must skip.

## Upload fields

`uploadDetails` describes the state of a file upload.

| Field              | Type   | Description                                       |
| ------------------ | ------ | ------------------------------------------------- |
| `id`               | string | Upload ID, used as the `uploadID` path parameter. |
| `status`           | string | Current upload status.                            |
| `s3Key`            | string | Storage key for the uploaded file.                |
| `metadata`         | object | Row counts for the upload.                        |
| `errors`           | array  | Errors recorded during validation.                |
| `errorReportS3Key` | string | Storage key for the error report.                 |
| `errorReportUrl`   | string | Download URL for the error report.                |
| `uploadedAt`       | string | Time the file was uploaded.                       |
| `validatedAt`      | string | Time validation finished.                         |
| `processedAt`      | string | Time the import finished.                         |

`metadata` reports `totalRows`, `validRows`, `invalidRows`, `duplicateRows`, and `importedRows`.

### Upload statuses

| Status              | Meaning                                          |
| ------------------- | ------------------------------------------------ |
| `uploaded`          | The file has been received.                      |
| `validating`        | The file is being checked.                       |
| `validated`         | The file passed validation and can be confirmed. |
| `validation_failed` | The file failed validation.                      |
| `processing`        | The confirmed upload is being imported.          |
| `processed`         | The import finished.                             |
| `failed`            | The upload could not be processed.               |

When validation fails, `errorReportUrl` points to a report describing the rejected rows.

## Track upload progress

`GET /v1/calls/contact-lists/{id}/uploads/{uploadID}/events` returns a server-sent event stream. Each event carries an `eventId`, an `eventType`, the current upload in `data`, and `errorReportUrl` when a report is available.

```bash theme={null}
curl --no-buffer \
  "https://api.persistence.dev/v1/calls/contact-lists/$LIST_ID/uploads/$UPLOAD_ID/events" \
  --header "X-API-Key: $PERSISTENCE_API_KEY" \
  --header "Accept: text/event-stream"
```

<Note>
  The interactive playground on this page sends a normal request and shows a single event body. Use a client that supports server-sent events to consume the stream.
</Note>

## Pagination and filtering

`GET /v1/calls/contact-lists` accepts these query parameters.

| Parameter    | Type    | Description                               |
| ------------ | ------- | ----------------------------------------- |
| `page`       | integer | Page number.                              |
| `perPage`    | integer | Items per page.                           |
| `type`       | string  | Filter by list type, `contacts` or `dnd`. |
| `status`     | string  | Filter by list status.                    |
| `searchText` | string  | Filter lists by text.                     |

`GET /v1/calls/contact-lists/{id}/items` accepts `page` and `perPage`. Both endpoints return a paged object containing `items`, `page`, `perPage`, `totalCount`, `column`, `order`, and `searchText`.

Each item in a list has a `phoneNumber` and a `metadata` object holding the other columns imported from the CSV file.

## Errors

Errors return a `code` and an `error` message.

```json theme={null}
{
  "code": "not_found",
  "error": "contact list not found"
}
```

| Status | Meaning                                                              |
| ------ | -------------------------------------------------------------------- |
| `400`  | The request body or a path parameter is invalid.                     |
| `401`  | The `X-API-Key` header is missing or invalid.                        |
| `403`  | The key is not permitted to perform the operation.                   |
| `404`  | The contact list or upload does not exist.                           |
| `409`  | The list already exists, or the upload is not ready to be confirmed. |
