# 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`, `full_name`, `unsubscribed`, `subscription_status` and `properties` to column names. `full_name` is split into first and last name. A `subscription_status` cell is `subscribed`, `unsubscribed` or `never_subscribed`, and a row is never stored as more subscribed than it says. With no `column_map`, a `subscription_status` column is read anyway. 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
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("contacts.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.

### `POST /contacts/verifications/estimate`

What checking an import's or a segment's contacts would check and cost.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `import_id` | string | The import to verify: the contacts it created or updated. |
| `segment_id` | string | Or the segment to verify. Send exactly one of the two. |

Estimate verifying a list:

```sh
curl -X POST "https://api.rasket.com/contacts/verifications/estimate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "import_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts/verifications/estimate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    import_id: "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
  }),
});

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

```python
import os

import requests

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

print(response.json())
```

#### Response `200`

```json
{
  "object": "contact_verification_estimate",
  "segment_id": null,
  "import_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "addresses": 1220,
  "known": 910,
  "to_check": 310,
  "per_1000_cents": 200,
  "estimated_cents": 62
}
```

- Addresses Rasket already knows are free and never checked again. `to_check` is the most that need a mailbox check; some are answered by the free checks first.
- `estimated_cents` is the most it can cost, at your plan's rate ($2 per 1,000 checks).

### `POST /contacts/verifications`

Check an import's or a segment's contacts in the background.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `import_id` | string | The import to verify. |
| `segment_id` | string | Or the segment to verify. Send exactly one of the two. |

Verify a list:

```sh
curl -X POST "https://api.rasket.com/contacts/verifications" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "import_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts/verifications", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    import_id: "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
  }),
});

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

```python
import os

import requests

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

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

#### Response `201`

```json
{
  "object": "contact_verification",
  "id": "0195f3a0-7c1e-7b2a-9d4f-3e8a1c2b5d60",
  "status": "walking",
  "segment_id": null,
  "import_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "addresses": 1220,
  "estimated_checks": 310,
  "per_1000_cents": 200,
  "estimated_cents": 62,
  "walked": 1220,
  "queued": 268,
  "counts": null,
  "removed": 0,
  "tagged": 0,
  "error": null,
  "created_at": "2026-09-08T22:22:17.595Z",
  "finished_at": null
}
```

- Needs the address-verification add-on and a `full_access` key of an admin. One list at a time per project, up to 100,000 addresses.
- Each address that needs a mailbox check is billed on your next invoice. An `Idempotency-Key` returns the verification it started.

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

Where it got to, and the counts by verdict once it is done.

#### Path parameters

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

Retrieve a list verification:

```sh
curl -X GET "https://api.rasket.com/contacts/verifications/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/verifications/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/verifications/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_verification",
  "id": "0195f3a0-7c1e-7b2a-9d4f-3e8a1c2b5d60",
  "status": "done",
  "segment_id": null,
  "import_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "addresses": 1220,
  "estimated_checks": 310,
  "per_1000_cents": 200,
  "estimated_cents": 62,
  "walked": 1220,
  "queued": 268,
  "counts": {
    "valid": 1104,
    "invalid": 41,
    "risky": 52,
    "unknown": 23
  },
  "removed": 0,
  "tagged": 0,
  "error": null,
  "created_at": "2026-09-08T22:22:17.595Z",
  "finished_at": "2026-09-08T23:10:44.000Z"
}
```

- `counts` is `{ valid, invalid, risky, unknown }` once `status` is `done`. Most lists finish within a day; large ones can take several.

### `DELETE /contacts/verifications/{id}/invalid-contacts`

Delete the contacts whose address doesn't exist, up to 500 a request.

#### Path parameters

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

Delete a list's invalid contacts:

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

```ts
const response = await fetch("https://api.rasket.com/contacts/verifications/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/invalid-contacts", {
  method: "DELETE",
  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.delete(
    "https://api.rasket.com/contacts/verifications/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/invalid-contacts",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact_verification_removal",
  "id": "0195f3a0-7c1e-7b2a-9d4f-3e8a1c2b5d60",
  "removed": 41,
  "remaining": 0
}
```

- The contacts are deleted from your audience, not just from the list. Send it again while `remaining` is above zero.

### `POST /contacts/verifications/{id}/tag-risky`

Add the risky contacts to a segment named "Risky addresses".

#### Path parameters

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

Tag a list's risky contacts:

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

```ts
const response = await fetch("https://api.rasket.com/contacts/verifications/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/tag-risky", {
  method: "POST",
  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.post(
    "https://api.rasket.com/contacts/verifications/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/tag-risky",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact_verification_tag",
  "id": "0195f3a0-7c1e-7b2a-9d4f-3e8a1c2b5d60",
  "tagged": 52,
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf"
}
```

- Risky: role addresses such as info@, disposable domains, and domains that accept every address.
