# Import contacts

> Upsert up to 500 contacts by email.

Source: https://sndr.sh/docs/cranl/contacts/import-contacts

`POST https://api.sndr.sh/api/cranl/v1/contacts/bulk`

Upsert up to 500 contacts by email. For existing contacts, attributes are merged and first_name, last_name and status are updated only when the row sets them, so an unsubscribed contact stays unsubscribed. New contacts default to subscribed.

Scope: `send`

**Body** (JSON)

- `contacts` (object[], required): Contacts as {email, first_name?, last_name?, status?, attributes?}, 1–500 per request.
- `source` (string): Source label for newly created contacts. Defaults to manual.
- `group_ids` (string[]): Groups (grp_...) to add every contact in the batch to.

**Request**

```bash
curl -X POST 'https://api.sndr.sh/api/cranl/v1/contacts/bulk' \
  -H 'Authorization: Bearer sndr_your_access_token' \
  -H 'Content-Type: application/json' \
  -d '{
    "contacts": [
      { "email": "alice@example.com", "first_name": "Alice", "attributes": { "tier": "gold" } },
      { "email": "bob@example.com",   "first_name": "Bob" }
    ],
    "source":    "import",
    "group_ids": ["grp_4d8a0c5e-1f7"]
  }'
```

**Response** (200)

```json
{
  "inserted": 1,
  "updated":  1,
  "skipped":  0
}
```

**Errors**

- `unauthenticated` (401): Missing or invalid Bearer token.
- `token_expired` (401): Access token expired — exchange the refresh token for a new pair.
- `token_revoked` (401): Token was explicitly revoked.
- `invalid_request` (400): Body is not valid JSON, has an unknown field, or a field has the wrong type.
- `chunk_too_large` (400): More than 500 contacts in one request. Split the import.
- `insufficient_scope` (403): Token is missing the send scope.
- `validation_failed` (422): contacts is empty, or a row has a missing or invalid email or an unknown status (fields names each row, e.g. row.<index>.email). Nothing is saved.
