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

# Upload your own contacts

> Bring contacts from your CRM into a Fuse list, and manage list membership by id.

Searching is not the only way to fill a list. If you already have contacts — in a CRM, a spreadsheet, or another tool — you can push them into Fuse directly. The API upload takes the same path a CSV upload takes in the app: contacts are created as your workspace's own records, and enrichment fills in missing fields asynchronously after the fact.

This guide covers the full membership lifecycle: create a list, upload new contact data, add contacts that already exist in your workspace, and remove them again.

<Info>
  List endpoints require a key with the `lists` scope; resolving a contact id requires `contacts`. See [scopes](/concepts/scopes).
</Info>

<Steps>
  <Step title="Create a list">
    Lists are the container everything else hangs off — [exports](/guides/export-a-list), [custom columns](/guides/custom-columns), and [campaigns](/guides/campaigns) all start from a list.

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/lists \
      -u "af_YOUR_KEY:" \
      -H "Content-Type: application/json" \
      -d '{ "name": "CRM import - July" }'
    ```

    ```json theme={null}
    {
      "list": {
        "id": "68a1f20b9c41d20014b3e901",
        "name": "CRM import - July",
        "entityType": "contactList",
        "contactsCount": 0,
        "companiesCount": 0,
        "createdAt": "2026-07-27T10:00:00.000Z"
      }
    }
    ```

    `entityType` defaults to `"contactList"`, which is what you want here. List names are unique within your workspace: creating a list with a name that already exists returns `409 LIST_NAME_TAKEN` in the standard [error envelope](/concepts/errors), so a safe pattern for repeated imports is to date-stamp the name.
  </Step>

  <Step title="Upload contact data">
    Send batches of raw contact records to the list. Each request accepts up to **500 contacts**; loop over your dataset in batches for larger imports.

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/lists/68a1f20b9c41d20014b3e901/contacts \
      -u "af_YOUR_KEY:" \
      -H "Content-Type: application/json" \
      -d '{
        "contacts": [
          {
            "firstName": "Ana",
            "lastName": "Silva",
            "email": "ana.silva@acme.com",
            "jobTitle": "VP of Finance",
            "companyName": "Acme",
            "companyDomain": "acme.com"
          },
          {
            "linkedinUrl": "https://www.linkedin.com/in/jordan-lee",
            "phone": "+14155550142"
          }
        ]
      }'
    ```

    ```json theme={null}
    {
      "addedCount": 2,
      "contactIds": [
        "68a1f4109c41d20014b3ea01",
        "68a1f4109c41d20014b3ea02"
      ]
    }
    ```

    Each contact accepts the following fields. All are optional individually, but every contact must include at least one of `email`, `linkedinUrl`, `firstName`, or `lastName` — a record with none of these has nothing to identify or enrich.

    | Field           | Max length |
    | --------------- | ---------- |
    | `firstName`     | 200        |
    | `lastName`      | 200        |
    | `email`         | 320        |
    | `phone`         | 50         |
    | `linkedinUrl`   | 500        |
    | `jobTitle`      | 300        |
    | `companyName`   | 300        |
    | `companyDomain` | 300        |

    You do not need to send complete records. Enrichment runs asynchronously after the upload and fills gaps where it can, so a bare `linkedinUrl` or an email address is a workable starting point. The returned `contactIds` are the ids of the newly created contacts — keep them if you plan to manage membership later.

    <Warning>
      Re-uploading the same people creates duplicates. This endpoint always creates new contact records; it does not match against contacts already in your workspace. For people who already exist in Fuse, [resolve their ids](#resolve-a-contact-id-from-a-linkedin-url) and use the `/members` endpoint instead.
    </Warning>
  </Step>

  <Step title="Add existing contacts by id">
    When the contacts already exist in your workspace — from an earlier upload, a [saved search](/guides/build-a-list-from-search), or another list — add them to a list by id rather than re-uploading their data:

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/lists/68a1f20b9c41d20014b3e901/members \
      -u "af_YOUR_KEY:" \
      -H "Content-Type: application/json" \
      -d '{
        "contactIds": [
          "68a1f4109c41d20014b3ea01",
          "68a1f4109c41d20014b3ea02",
          "68a1f2c89c41d20014b3e955"
        ]
      }'
    ```

    ```json theme={null}
    {
      "submittedCount": 3,
      "addedCount": 2
    }
    ```

    Each request accepts up to **5,000** contact ids. Adding is idempotent: a contact that is already in the list is skipped, not duplicated. That is why `addedCount` can be lower than `submittedCount` — the difference is the number of contacts that were already members. You can safely retry a failed request without worrying about double-adding.
  </Step>

  <Step title="Remove contacts from a list">
    Removal uses the same endpoint with `DELETE`:

    ```bash theme={null}
    curl -X DELETE https://api.tryfuse.ai/api/v1/business/lists/68a1f20b9c41d20014b3e901/members \
      -u "af_YOUR_KEY:" \
      -H "Content-Type: application/json" \
      -d '{ "contactIds": ["68a1f4109c41d20014b3ea01", "68a1f4109c41d20014b3ea02"] }'
    ```

    ```json theme={null}
    { "submittedCount": 2 }
    ```

    This removes the contacts from the list; it does not delete the contact records from your workspace.
  </Step>
</Steps>

## Resolve a contact id from a LinkedIn URL

The `/members` endpoints work on contact ids, but CRMs rarely store Fuse ids. If your system keys people by LinkedIn URL, resolve the id first:

```bash theme={null}
curl -X POST https://api.tryfuse.ai/api/v1/business/contacts/resolve \
  -u "af_YOUR_KEY:" \
  -H "Content-Type: application/json" \
  -d '{ "linkedinUrl": "https://www.linkedin.com/in/jordan-lee" }'
```

```json theme={null}
{ "contactId": "68a1f2c89c41d20014b3e955" }
```

URLs are normalized server-side, so protocol, trailing slashes, and casing don't matter — `linkedin.com/in/Jordan-Lee/` resolves the same as `https://www.linkedin.com/in/jordan-lee`. If no contact in your workspace matches, you get `404 CONTACT_NOT_FOUND`:

```json theme={null}
{
  "error": {
    "code": "CONTACT_NOT_FOUND",
    "message": "The requested contact was not found.",
    "param": null,
    "doc_url": "https://docs.fuseai.com/concepts/errors#contact_not_found",
    "request_id": "req_8fK2mQ4x"
  }
}
```

This gives you a clean sync pattern for recurring imports: for each person, try `resolve` first — on a hit, add the id via `/members`; on a `404`, include the record in your next batch upload. That keeps repeat syncs from piling up duplicate contacts.

## Where to next

<CardGroup cols={2}>
  <Card title="Custom columns" icon="table-columns" href="/guides/custom-columns">
    Attach your own fields — CRM ids, owner, deal stage — to list rows.
  </Card>

  <Card title="Export a list" icon="file-arrow-down" href="/guides/export-a-list">
    Pull enriched contacts back out as CSV once enrichment has filled the gaps.
  </Card>

  <Card title="Build a list from search" icon="magnifying-glass" href="/guides/build-a-list-from-search">
    Fill a list from prospect search instead of an upload.
  </Card>

  <Card title="Campaigns" icon="paper-plane" href="/guides/campaigns">
    Put an uploaded list to work in an outbound campaign.
  </Card>
</CardGroup>
