# Contacts

The people you email: create, update in batches, search, read their history, export or erase them.

## List contacts

`GET /projects/{project_id}/contacts`

Newest first. Filter by one of search (matches email and name), tag or email at a time.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |
| `search` | query | string \| null | Optional |
| `tag` | query | string \| null | Optional |
| `email` | query | string \| null | Optional |
| `cursor` | query | string \| null | Optional |
| `limit` | query | integer | Optional, default 50 |

```sh Request
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/contacts?tag=trial&limit=50" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "contacts": [
    {
      "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
      "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
      "email": "maya@example.com",
      "first_name": "Maya",
      "last_name": "Patel",
      "timezone": "Europe/London",
      "tags": ["trial"],
      "attributes": {
        "plan": "trial",
        "company": "Northwind"
      },
      "is_unsubscribed": false,
      "is_blocked": false,
      "created_at": 1790000000000
    }
  ],
  "next_cursor": null
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | limit: Too big: expected number to be <=200 |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `project_not_found` | Project not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Create a contact

`POST /projects/{project_id}/contacts`

Fails with 409 contact_exists when the address is already in the project; to create or update in one call, use the batch route. A new contact joins every live campaign whose audience it matches.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |

### Body

Send JSON with every field.

| Field | Type | Notes |
| --- | --- | --- |
| `email` | string | Required |
| `first_name` | string \| null | Required |
| `last_name` | string \| null | Required |
| `timezone` | string \| null | Required |
| `tags` | string[] | Required |
| `attributes` | object of string | Required |

```sh Request
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/contacts" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "maya@example.com",
  "first_name": "Maya",
  "last_name": "Patel",
  "timezone": "Europe/London",
  "tags": ["trial"],
  "attributes": {
    "plan": "trial",
    "company": "Northwind"
  }
}'
```

### Response

```json 201
{
  "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
  "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
  "email": "maya@example.com",
  "first_name": "Maya",
  "last_name": "Patel",
  "timezone": "Europe/London",
  "tags": ["trial"],
  "attributes": {
    "plan": "trial",
    "company": "Northwind"
  },
  "is_unsubscribed": false,
  "is_blocked": false,
  "created_at": 1790000000000
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | Send a JSON request body. |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 402 | `plan_limit_reached` | Your plan allows 5,000 contacts. Upgrade the plan to add more. |
| 404 | `project_not_found` | Project not found. |
| 409 | `contact_exists` | maya@example.com is already a contact in this project. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Create or update contacts in a batch

`PUT /projects/{project_id}/contacts/batch`

Up to 1,000 contacts, all or nothing: one bad contact refuses the batch and the error names its index. For an existing contact a null field keeps what is stored, tags are added and attributes are set.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |

### Body

Send JSON with every field.

| Field | Type | Notes |
| --- | --- | --- |
| `contacts` | object[] | Required |
| `contacts[].email` | string | Required |
| `contacts[].first_name` | string \| null | Required |
| `contacts[].last_name` | string \| null | Required |
| `contacts[].timezone` | string \| null | Required |
| `contacts[].tags` | string[] | Required |
| `contacts[].attributes` | object of string | Required |

```sh Request
curl -X PUT "https://sendocado.com/api/v1/projects/$PROJECT_ID/contacts/batch" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "contacts": [
    {
      "email": "maya@example.com",
      "first_name": "Maya",
      "last_name": null,
      "timezone": null,
      "tags": ["customer"],
      "attributes": {
        "plan": "pro"
      }
    },
    {
      "email": "leo@example.com",
      "first_name": "Leo",
      "last_name": "Martin",
      "timezone": "Europe/Paris",
      "tags": ["trial"],
      "attributes": {}
    }
  ]
}'
```

### Response

`204` with no body.

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | Send a JSON request body. |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 402 | `plan_limit_reached` | Your plan allows 5,000 contacts. Upgrade the plan to add more. |
| 404 | `project_not_found` | Project not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Get a contact

`GET /contacts/{contact_id}`

is_unsubscribed and is_blocked are read from the address’s history, so they stay true after a contact is deleted and added again.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/contacts/$CONTACT_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
  "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
  "email": "maya@example.com",
  "first_name": "Maya",
  "last_name": "Patel",
  "timezone": "Europe/London",
  "tags": ["trial"],
  "attributes": {
    "plan": "trial",
    "company": "Northwind"
  },
  "is_unsubscribed": false,
  "is_blocked": false,
  "created_at": 1790000000000
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Update a contact

`PUT /contacts/{contact_id}`

The email never changes. Tags and attributes become exactly the sets you send. Unsubscribing and blocking have their own routes.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |

### Body

Send JSON with every field.

| Field | Type | Notes |
| --- | --- | --- |
| `first_name` | string \| null | Required |
| `last_name` | string \| null | Required |
| `timezone` | string \| null | Required |
| `tags` | string[] | Required |
| `attributes` | object of string | Required |

```sh Request
curl -X PUT "https://sendocado.com/api/v1/contacts/$CONTACT_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "first_name": "Maya",
  "last_name": "Patel",
  "timezone": "Europe/London",
  "tags": ["trial"],
  "attributes": {
    "plan": "trial",
    "company": "Northwind"
  }
}'
```

### Response

```json 200
{
  "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
  "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
  "email": "maya@example.com",
  "first_name": "Maya",
  "last_name": "Patel",
  "timezone": "Europe/London",
  "tags": ["trial"],
  "attributes": {
    "plan": "trial",
    "company": "Northwind"
  },
  "is_unsubscribed": false,
  "is_blocked": false,
  "created_at": 1790000000000
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | Send a JSON request body. |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Delete a contact

`DELETE /contacts/{contact_id}`

Its campaigns end. The address stays unsubscribed or blocked; to remove every trace of it, create a contact erasure.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |

```sh Request
curl -X DELETE "https://sendocado.com/api/v1/contacts/$CONTACT_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

`204` with no body.

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## List a contact’s emails and events

`GET /contacts/{contact_id}/activity`

Every email sent to the contact’s address, newest first, including ones sent before the contact existed. Each lists up to 15 events of each type.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |
| `cursor` | query | string \| null | Optional |
| `limit` | query | integer | Optional, default 50 |

```sh Request
curl "https://sendocado.com/api/v1/contacts/$CONTACT_ID/activity" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "activity": [
    {
      "send": {
        "id": "thjxjqi3ogz5kok16zv0mwufxbv932by",
        "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
        "contact_id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
        "to": "maya@example.com",
        "subject": "Welcome to Acme, Maya",
        "source": {
          "type": "campaign_step",
          "campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
          "step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
          "enrollment_id": "j3j4wj99ibag7i1mnbqns6puq80idw37"
        },
        "status": "delivered",
        "has_opened": true,
        "has_clicked": true,
        "has_replied": false,
        "accepted_at": 1790000120000,
        "failed_at": null,
        "ext_provider_message_id": "4ef9a1c2-7d3b-4f0e-9a51-2c8e6b1d0f73",
        "created_at": 1790000120000
      },
      "events": [
        {
          "type": "delivered",
          "url": null,
          "detail": null,
          "created_at": 1790000180000
        },
        {
          "type": "opened",
          "url": null,
          "detail": null,
          "created_at": 1790005700000
        },
        {
          "type": "clicked",
          "url": "https://acme.example/setup",
          "detail": null,
          "created_at": 1790005760000
        }
      ]
    }
  ],
  "next_cursor": null
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | limit: Too big: expected number to be <=200 |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## List a contact’s campaigns

`GET /contacts/{contact_id}/enrollments`

Every campaign the contact is in or has left, with why they left.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/contacts/$CONTACT_ID/enrollments" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "enrollments": [
    {
      "id": "j3j4wj99ibag7i1mnbqns6puq80idw37",
      "campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
      "contact": {
        "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
        "email": "maya@example.com",
        "first_name": "Maya",
        "last_name": "Patel"
      },
      "status": "active",
      "exit_reason": null,
      "next_action": {
        "type": "check_branch",
        "step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
        "due_at": 1790259200000
      },
      "created_at": 1790000060000,
      "exited_at": null
    }
  ],
  "next_cursor": null
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Export everything about a contact

`GET /contacts/{contact_id}/export`

A subject-access export: every fact held about the contact and its address, in one JSON document.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/contacts/$CONTACT_ID/export" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "contact": {
    "id": "j8ht9lgmxg9edn581u33xtplpft75v2s",
    "project_id": "ujzde8gxd6ncf10epf91dhodzdoc9is0",
    "email": "maya@example.com",
    "first_name": "Maya",
    "last_name": "Patel",
    "timezone": "Europe/London",
    "tags": ["trial"],
    "attributes": {
      "plan": "trial",
      "company": "Northwind"
    },
    "is_unsubscribed": false,
    "is_blocked": false,
    "created_at": 1790000000000
  },
  "enrollments": [
    {
      "id": "j3j4wj99ibag7i1mnbqns6puq80idw37",
      "campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
      "exits": [],
      "created_at": 1790000060000
    }
  ],
  "sends": [
    {
      "send": {
        "id": "thjxjqi3ogz5kok16zv0mwufxbv932by",
        "to": "maya@example.com",
        "subject": "Welcome to Acme, Maya",
        "source": {
          "type": "campaign_step",
          "campaign_id": "h5dnsipzz5fk2z9ri19r0wyojfljooa5",
          "step_id": "j8gxbenyjqwx4hh5344tfjgvq4k7bn7x",
          "enrollment_id": "j3j4wj99ibag7i1mnbqns6puq80idw37"
        },
        "status": "delivered",
        "accepted_at": 1790000120000,
        "failed_at": null,
        "created_at": 1790000120000
      },
      "events": [
        {
          "type": "delivered",
          "url": null,
          "detail": null,
          "created_at": 1790000180000
        },
        {
          "type": "opened",
          "url": null,
          "detail": null,
          "created_at": 1790005700000
        },
        {
          "type": "clicked",
          "url": "https://acme.example/setup",
          "detail": null,
          "created_at": 1790005760000
        }
      ]
    }
  ],
  "replies": [
    {
      "id": "kdfy6spsc3lkr2aqxv9upctnwlavyf4r",
      "send_id": "thjxjqi3ogz5kok16zv0mwufxbv932by",
      "from_email": "maya@example.com",
      "from_name": "Maya Patel",
      "subject": "Re: Welcome to Acme, Maya",
      "text": "Thanks! Quick question: can I import from a CSV?",
      "created_at": 1790010800000
    }
  ],
  "unsubscribes": [],
  "resubscribes": [],
  "bounces": [],
  "complaints": [],
  "manual_blocks": [],
  "unblocks": [],
  "consents": [],
  "transactional_emails": [],
  "test_emails": []
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_not_found` | Contact not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Erase an address

`POST /projects/{project_id}/contact-erasures`

Deletes the contact and everything sent to the address, in the background. Its unsubscribes and blocks are kept without the address, so it stays unmailable. Read the erasure until it is completed.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |

### Body

Send JSON with every field.

| Field | Type | Notes |
| --- | --- | --- |
| `email` | string | Required |

```sh Request
curl -X POST "https://sendocado.com/api/v1/projects/$PROJECT_ID/contact-erasures" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "maya@example.com"
}'
```

### Response

```json 201
{
  "id": "ez732pgojj7g3f9caioctiq71hget7my",
  "status": "pending",
  "created_at": 1790000000000,
  "completed_at": null
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 400 | `invalid_data` | Send a JSON request body. |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `project_not_found` | Project not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## Get an erasure

`GET /contact-erasures/{contact_erasure_id}`

status becomes completed once every record is gone.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `contact_erasure_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/contact-erasures/$CONTACT_ERASURE_ID" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "id": "ez732pgojj7g3f9caioctiq71hget7my",
  "status": "completed",
  "created_at": 1790000000000,
  "completed_at": 1790000120000
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `contact_erasure_not_found` | Contact erasure not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## List tags in use

`GET /projects/{project_id}/contact-tags`

Every distinct tag on the project’s contacts, for building audiences. is_complete is false when there are more than one read lists.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/contact-tags" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "tags": ["customer", "trial", "webinar"],
  "is_complete": true
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `project_not_found` | Project not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |

## List attribute keys in use

`GET /projects/{project_id}/contact-attribute-keys`

Every distinct attribute key on the project’s contacts. is_complete is false when there are more than one read lists.

### Parameters

| Name | In | Type | Notes |
| --- | --- | --- | --- |
| `project_id` | path | string | Required |

```sh Request
curl "https://sendocado.com/api/v1/projects/$PROJECT_ID/contact-attribute-keys" \
  -H "Authorization: Bearer $SENDOCADO_API_KEY"
```

### Response

```json 200
{
  "keys": ["company", "plan"],
  "is_complete": true
}
```

### Errors

| Status | Type | Example message |
| --- | --- | --- |
| 401 | `unauthorized` | Send a valid API key as "Authorization: Bearer sc_…", or sign in. |
| 404 | `project_not_found` | Project not found. |
| 429 | `rate_limited` | Too many requests. Try again in 12 seconds. |
