# Imports

Bring a CSV of contacts in. The file is uploaded in one request and processed in the background; the import records what happened to every row.

## The column map

`column_map` says which CSV column holds which field, and which columns become typed properties. A property entry is a column name, or `{ column, type }` with `type` one of `string`, `number` or `boolean` — the default is `string`.

A column map:

```text
{
  "email": "Email",
  "first_name": "First name",
  "last_name": "Last name",
  "unsubscribed": "Opted out",
  "properties": {
    "plan": "Plan",
    "seats": {
      "column": "Seats",
      "type": "number"
    }
  }
}
```

## What happens to a row

- A new address is `created`. An address that already exists is `updated` with the row's values when `on_conflict` is `upsert`, and `skipped` when it is `skip`, the default.
- A row with no usable address, a value that does not match its property's type, or a row past your plan's contact limit is `failed`. The first thousand failures are kept with their row numbers.
- Rows are processed in batches, so an import that is `in_progress` already has some of its contacts. No `contact.created` webhook is sent per row.
- The file is deleted seven days after the import completes. The counts stay.

## Endpoints

### `POST /contacts/imports`

Upload a file of up to 50 MB; the rows are processed in the background.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `file` (required) | file | The CSV, up to 50 MB, as a multipart part. |
| `column_map` | string | A JSON object mapping `email`, `first_name`, `last_name`, `unsubscribed` and `properties` to column names. A property entry is a column name, or `{ column, type }` with `type` one of `string`, `number` or `boolean`. |
| `on_conflict` | string | `upsert` updates a contact whose address already exists; `skip` (the default) leaves it alone. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `segments` | string | A JSON array of `{ id }` segments every imported contact is added to. |
| `topics` | string | A JSON array of `{ id, subscription }` topic choices applied to every imported contact. |

Import contacts from a CSV:

```sh
curl -X POST "https://api.rasket.com/contacts/imports" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -F "file=@contacts.csv" \
  -F "column_map={\"email\":\"Email\",\"first_name\":\"First name\",\"properties\":{\"plan\":\"Plan\"}}" \
  -F "on_conflict=upsert" \
  -F "segments=[{\"id\":\"78261eea-8f8b-4381-83c6-79fa7120f1cf\"}]"
```

```ts
const form = new FormData();
form.append("file", new Blob([csv]), "contacts.csv");
form.append("column_map", "{\"email\":\"Email\",\"first_name\":\"First name\",\"properties\":{\"plan\":\"Plan\"}}");
form.append("on_conflict", "upsert");
form.append("segments", "[{\"id\":\"78261eea-8f8b-4381-83c6-79fa7120f1cf\"}]");

const response = await fetch("https://api.rasket.com/contacts/imports", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
  body: form,
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/contacts/imports",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    files={"file": open("contacts.csv", "rb")},
    data={
        "column_map": "{\"email\":\"Email\",\"first_name\":\"First name\",\"properties\":{\"plan\":\"Plan\"}}",
        "on_conflict": "upsert",
        "segments": "[{\"id\":\"78261eea-8f8b-4381-83c6-79fa7120f1cf\"}]",
    },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "contact_import",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- The response is immediate; poll the import for its counts.
- No `contact.created` or `contact.updated` webhook is sent per row. One audit entry records the import.
- Rows past your plan's contact limit are counted as `failed`.

### `GET /contacts/imports`

Every import, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `queued`, `in_progress`, `completed` or `failed`. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List imports:

```sh
curl -X GET "https://api.rasket.com/contacts/imports?status=completed" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/imports?status=completed", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/contacts/imports?status=completed",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "contact_import",
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
      "status": "completed",
      "created_at": "2026-09-08T22:22:17.595Z",
      "completed_at": "2026-09-08T22:24:01.330Z",
      "counts": {
        "total": 1240,
        "created": 1180,
        "updated": 40,
        "skipped": 15,
        "failed": 5
      }
    }
  ]
}
```

### `GET /contacts/imports/{id}`

One import, with its status and counts.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The import's ID. |

Retrieve an import:

```sh
curl -X GET "https://api.rasket.com/contacts/imports/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/imports/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/contacts/imports/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "contact_import",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "status": "completed",
  "created_at": "2026-09-08T22:22:17.595Z",
  "completed_at": "2026-09-08T22:24:01.330Z",
  "counts": {
    "total": 1240,
    "created": 1180,
    "updated": 40,
    "skipped": 15,
    "failed": 5
  }
}
```

- `counts` is `{ total, created, updated, skipped, failed }`. `completed_at` is `null` until the import finishes.
- The uploaded file is deleted seven days after the import completes; the counts stay.
