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

# Build an audience

> Create a reusable list of numbers and template variables from inline members or a CSV file, then keep it up to date.

An audience is a list of phone numbers, each with its own template variables,
that belongs to your team. Any campaign in any project of that team can use it.
Polymorfa stores the members, counts them, and copies them into a campaign when
the campaign launches.

Audiences are a team-level resource. Use a team API key: `campaigns:read` to
read, `campaigns:manage` to change. Project tokens are refused on every
audience route.

## Create an audience from inline members

Send up to 1,000 members in the request body.

```bash theme={null}
curl -X POST "https://api.polymorfa.com/platform/audiences" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "September launch",
    "members": [
      { "phone": "+14155550100", "variables": { "first_name": "Ada", "plan": "pro" } },
      { "phone": "+442071838750", "variables": { "first_name": "Alan" } }
    ]
  }'
```

The response returns the audience and what the import did with your input:

```json theme={null}
{
  "data": {
    "id": "3b2f0c4e-6c1a-4a9b-9a53-6b7c9d0e1f22",
    "name": "September launch",
    "source": "manual",
    "recipientCount": 2,
    "duplicateCount": 0,
    "invalidCount": 0,
    "invalidRows": []
  }
}
```

`recipientCount` is the number of members Polymorfa stored. A count you send in
the request is ignored.

## Import a CSV file

Importing a file takes three requests: ask for an upload URL, upload the file,
then create the audience from the stored file.

Ask for an upload URL. It is single use and expires shortly after it is issued.

```bash theme={null}
curl -X POST "https://api.polymorfa.com/platform/audiences/uploads" \
  -H "Authorization: Bearer $POLYMORFA_KEY"
```

Upload the file to that URL. The response returns `storageId`.

```bash theme={null}
curl -X POST "$UPLOAD_URL" \
  -H "Content-Type: text/csv" \
  --data-binary @contacts.csv
```

Create the audience from the stored file, passing `storageId` as `fileId` and
naming the columns.

```bash theme={null}
curl -X POST "https://api.polymorfa.com/platform/audiences" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "September launch",
    "fileId": "'"$STORAGE_ID"'",
    "mapping": {
      "phone": "Mobile",
      "variables": { "first_name": "First name", "plan": "Plan" }
    }
  }'
```

`mapping.phone` names the column that holds the number. `mapping.variables`
maps each template variable name to the column that supplies it. Column names
match exactly first, then without regard to letter case.

Polymorfa reads the file on the server. The file must be UTF-8 and have a header
row. The separator is detected from the header line: comma, semicolon or tab,
and comma when the line is ambiguous. Quoted fields, doubled quotes inside a
quoted field, line breaks inside a quoted field, and LF, CRLF or CR line endings
are all read correctly. Blank lines are ignored.

Send either `members` or `fileId` with `mapping`. A request carrying both is
refused.

## Number and variable rules

| Rule                      | Value                                                                                                                                                              |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Number format             | International. Spaces, dots, hyphens and parentheses are ignored; a leading `00` is read as `+`; digits without `+` are read as already including the country code |
| Number length             | 8 to 15 digits including the country code, and a possible length for that country                                                                                  |
| Variables per member      | At most 50                                                                                                                                                         |
| Variable name             | Starts with a letter, digit or `_`, then up to 63 more characters from letters, digits, `_`, `.` and `-`                                                           |
| Variable value            | A string, number or boolean, stored as a string of at most 1,024 characters                                                                                        |
| Members per request       | At most 1,000                                                                                                                                                      |
| Data rows per file import | At most 100,000                                                                                                                                                    |

A national number with no country code cannot be resolved and is reported as
invalid. A file with more than 100,000 data rows is refused with `413`: split it
into smaller files and import each one.

## Read what the import rejected

Every create and append reports the rows it did not store, so you can fix and
resend only those:

| Field            | Meaning                                                                                        |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `recipientCount` | Members in the audience after the request                                                      |
| `duplicateCount` | Valid entries skipped because the number repeated in the request or is already in the audience |
| `invalidCount`   | Entries that failed validation                                                                 |
| `invalidRows`    | Up to 20 samples, each with `row` and `reason`                                                 |

`reason` is `missing_phone`, `invalid_phone`, `invalid_variables` or
`invalid_entry`. For inline members, `row` is the 1-based position in the array.
For a file, `row` is the spreadsheet row number, where the header is row 1.

## Keep an audience up to date

```bash theme={null}
# Add up to 1,000 members
curl -X POST "https://api.polymorfa.com/platform/audiences/$LIST_ID/members" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "members": [{ "phone": "+14155550101", "variables": { "first_name": "Grace" } }] }'

# Page through members in the order they were added
curl "https://api.polymorfa.com/platform/audiences/$LIST_ID/members?limit=100" \
  -H "Authorization: Bearer $POLYMORFA_KEY"

# Remove one member
curl -X DELETE "https://api.polymorfa.com/platform/audiences/$LIST_ID/members/%2B14155550101" \
  -H "Authorization: Bearer $POLYMORFA_KEY"
```

Member listing is cursor-paginated: `limit` is 1 to 100 and defaults to 25, and
`page.nextCursor` carries the next page until `page.hasMore` is `false`.
Adding a number that is already a member skips it and reports it in
`duplicateCount`. Removing a member changes the audience only. A campaign that
already launched keeps the recipients it copied.

Duplicating a campaign linked to an audience creates a draft with that same
audience. The copy takes the audience's members when it launches, including
membership changes since the original launch. A campaign without an audience
copies its recipients into the new draft.

## Use the audience in a campaign

Set `recipientListId` on the campaign. Its members are copied into the campaign
when the campaign launches, numbers on the team's opt-out list are skipped, and
`recipientCount` is set from the rows that were actually created. See
[Send a campaign](/guides/engage-at-scale/send-a-campaign) and
[Opt-outs and STOP replies](/guides/engage-at-scale/opt-outs).
