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

# Add list members

> Upsert one or more people into a custom list by LinkedIn slug or URL.

Adds memberships to a custom list. Slugs are looked up in the people graph; an unknown slug is reported in `meta.missing` and does not create a membership. URLs are accepted even when the graph lookup does not find a person, so an arbitrary non-empty URL can create a membership with nullable person metadata.

The operation processes `slugs` first and `urls` second. It is not transactional: a later lookup or database failure can leave earlier items written. Each item is an upsert keyed by the exact trimmed `person_linkedin_url` within the list.

<ParamField path="id" type="string" required>
  List UUID or case-insensitive name. Default lists reject writes with `403`; add members only to a custom list.
</ParamField>

<ParamField body="slugs" type="string[]">
  Optional LinkedIn public identifiers. If present, 1–100 non-validated strings. Unknown slugs are returned in `meta.missing`.
</ParamField>

<ParamField body="urls" type="string[]">
  Optional URL strings, 1–100 items. The Gateway trims each value, extracts a slug from `/in/<slug>` when present, and otherwise stores the trimmed string as the membership URL. Empty trimmed values are skipped.
</ParamField>

<ParamField body="stage" type="string">
  Optional enum: `to-contact` (default), `contacted`, `responded`, or `passed`.
</ParamField>

<ParamField body="notes" type="string">
  Optional notes, up to 2,000 characters. Defaults to `""` and is not nullable in the stored/API membership shape.
</ParamField>

At least one of `slugs` or `urls` must contain a processable value. The body allows both arrays; the per-array maximum is 100, so a request can contain up to 200 entries. Only documented fields are supported; the framework strips unknown top-level fields.

## Partial success and repeat calls

If the exact URL already belongs to the list, the existing membership is updated rather than duplicated. The supplied `stage` and `notes` **always overwrite** the existing values, including the defaults when those fields are omitted. Repeating a request can therefore reset a member's stage to `to-contact` and notes to `""`. Enrichment fields are updated only when the new lookup supplies non-null values.

## Response schema

A successful request returns `201`:

| Field          | Type      | Nullability and meaning                                                          |
| -------------- | --------- | -------------------------------------------------------------------------------- |
| `data`         | object\[] | One membership result per successfully processed slug or URL, including upserts. |
| `meta.added`   | integer   | Number of returned membership objects. Despite the name, this includes updates.  |
| `meta.missing` | string\[] | Unknown slugs only. URL misses are not included.                                 |

Each `data` item uses the membership shape from [Get list](/api-reference/lists/get): UUID `id`/`list_id`, required `person_linkedin_url`, nullable person enrichment fields, stage enum, string `notes`, and ISO `added_at`/`updated_at` timestamps.

<ResponseExample>
  ```json 201 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "data": [
      {
        "id": "8c5f6a6b-5cb7-4e3f-a1fd-9aabf2d2c6e0",
        "list_id": "550e8400-e29b-41d4-a716-446655440000",
        "person_linkedin_url": "https://www.linkedin.com/in/derek-morrow",
        "person_slug": "derek-morrow",
        "person_full_name": "Derek Morrow",
        "person_headline": null,
        "person_picture_url": null,
        "stage": "to-contact",
        "notes": "",
        "added_at": "2026-09-07T10:00:00.000Z",
        "updated_at": "2026-09-07T10:00:00.000Z"
      }
    ],
    "meta": {
      "added": 1,
      "missing": []
    }
  }
  ```
</ResponseExample>

<RequestExample>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --fail-with-body --max-time 30 -X POST "https://api.freshtalent.ai/v1/lists/hot/members" \
    -H "Authorization: Bearer $FRESHTALENT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"slugs":["derek-morrow"],"stage":"to-contact","notes":"Review after demo"}'
  ```

  ```js JavaScript (native fetch) theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch("https://api.freshtalent.ai/v1/lists/hot/members", {
    method: "POST",
    signal: AbortSignal.timeout(30_000),
    headers: {
      Authorization: `Bearer ${process.env.FRESHTALENT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ slugs: ["derek-morrow"], stage: "to-contact" }),
  });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
  const body = await response.json();
  console.log(`processed ${body.meta.added}; missing ${body.meta.missing.join(", ")}`);
  ```

  ```python Python (stdlib) theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import json
  import os
  from urllib.request import Request, urlopen

  payload = json.dumps({"urls": ["https://www.linkedin.com/in/derek-morrow"], "stage": "contacted"}).encode()
  request = Request(
      "https://api.freshtalent.ai/v1/lists/hot/members",
      data=payload,
      method="POST",
      headers={
          "Authorization": f"Bearer {os.environ['FRESHTALENT_API_KEY']}",
          "Content-Type": "application/json",
      },
  )
  with urlopen(request, timeout=30) as response:
      body = json.load(response)
  print(body["meta"], body["data"])
  ```
</RequestExample>

## Errors, reconciliation, and retry safety

* `400`: invalid body, invalid stage, no processable `slugs`/`urls`, or a missing platform-key `X-FreshTalent-Org-Id` UUID. Fix the request/header. A body containing an unknown slug can still return `201` with that slug in `meta.missing`.
* `401` (`unauthorized`): provide a valid API key.
* `402` (`payment_required`): restore entitlement.
* `403` (`forbidden`): the addressed list is a Gateway-managed default list; use a custom list.
* `404` (`not_found`): the list UUID/name is not visible to the organization.
* `429` or `5xx`: the outcome can be unknown and the request can be partially applied. Do not blindly replay a request that includes stage/notes. First `GET /lists/{id}` and reconcile by exact `person_linkedin_url`; retry only missing items with the intended stage and notes.

There is no request id or `next` link. The batch is bounded by the request arrays, not paginated.

## Next steps

[Get list](/api-reference/lists/get) to reconcile memberships, or [Update list member](/api-reference/lists/update-member) for a stage/notes-only change.
