# Contacts

The people a broadcast can go to: an address, optional names, typed properties, the segments they are in and the topics they have chosen.

## Addressing a contact

- Every `{contact}` below accepts the contact's ID or its email address. Matching on the address is case-insensitive.
- Creating a contact whose address already exists updates that contact and answers with its existing `id`. There is no duplicate error, and a repeat create never resubscribes someone who opted out.
- Reads work with either key permission. Creating, updating and deleting need a `full_access` key.

## Properties

A property is declared once — a key and a type — and then any contact can carry a value for it. Values are checked against the type on every write, and the key is also the merge variable a broadcast prints: `{{{PLAN}}}` for a property called `plan`, falling back to the property's `fallback_value` for a contact without one.

## Unsubscribing and erasure

- `unsubscribed: true` is a global opt-out: the contact receives no broadcast at all, whatever their topics say. Every change to it, and every topic choice, writes a consent record with its source and time.
- `DELETE` is an erasure, not a soft delete. The personal data goes; the consent records stay with a hash of the address, so an opt-out remains provable after the person is gone.

## Events

Three webhook types are sent for contacts. A CSV import sends none of them per row.

| Type | Sent when |
| --- | --- |
| `contact.created` | A contact was created through the API. |
| `contact.updated` | A contact's fields or properties changed through the API. Not sent for a topic or segment change, or for an unsubscribe made on the preference page. |
| `contact.deleted` | A contact was erased. The payload carries the contact as it was. |

## Endpoints

### `POST /contacts`

Add an address to your audience, with optional names, properties, segments and topics.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `email` (required) | string | The contact's address. |
| `first_name` | string | Up to 200 characters. |
| `last_name` | string | Up to 200 characters. |
| `unsubscribed` | boolean | Start the contact opted out of every broadcast. Defaults to `false`. A repeat create never sets it back to `false`. |
| `properties` | object | Values for declared contact properties, keyed by property key. Each value must match its property's type. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `segments` | object[] | `{ id }` entries naming segments to add the contact to. |
| `topics` | object[] | `{ id, subscription }` entries, with `subscription` one of `opt_in` or `opt_out`. |

Create a contact:

```sh
curl -X POST "https://api.rasket.com/contacts" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ronald.williams@example.com",
  "first_name": "Ronald",
  "last_name": "Williams",
  "properties": {
    "company": "Acme",
    "seats": 12
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "ronald.williams@example.com",
    first_name: "Ronald",
    last_name: "Williams",
    properties: {
      company: "Acme",
      seats: 12
    }
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/contacts",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "email": "ronald.williams@example.com",
    "first_name": "Ronald",
    "last_name": "Williams",
    "properties": {
      "company": "Acme",
      "seats": 12
    }
  },
)

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

#### Response `201`

```json
{
  "object": "contact",
  "id": "e169aa45-1ecf-4183-9955-b1499d5701d3"
}
```

- An address the team already holds a contact for is updated with the fields the body carries, and the existing `id` is returned. There is no 409.
- Every key in `properties` must name a declared contact property of a matching type; an unknown key or a wrong type is `422 invalid_parameter` naming the key.
- Your plan's contact limit is enforced here and on imports: past it, the call is `422 validation_error` with `Contact limit reached.`
- Emits `contact.created` — or `contact.updated` when the address already existed.

### `GET /contacts`

Every contact, newest first; optionally only the members of one segment.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment_id` | string | Only the contacts in this segment — the same membership a broadcast to it would resolve. |
| `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 contacts:

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

```ts
const response = await fetch("https://api.rasket.com/contacts?limit=20", {
  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?limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "data": [
    {
      "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
      "email": "ronald.williams@example.com",
      "first_name": "Ronald",
      "last_name": "Williams",
      "created_at": "2026-09-08T22:22:17.595Z",
      "unsubscribed": false
    }
  ]
}
```

- List items carry no `properties`; retrieve a contact for them.
- The envelope carries no `has_more`. Walk pages with `after`, using the last item's `id`; an empty page is the end.

### `GET /contacts/{contact}`

One contact, by ID or by address, with its properties.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |

Retrieve a contact:

```sh
curl -X GET "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3", {
  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/e169aa45-1ecf-4183-9955-b1499d5701d3",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact",
  "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "email": "ronald.williams@example.com",
  "first_name": "Ronald",
  "last_name": "Williams",
  "created_at": "2026-09-08T22:22:17.595Z",
  "unsubscribed": false,
  "properties": {
    "company": "Acme",
    "seats": 12
  }
}
```

- A contact in another team is `404 not_found`, indistinguishable from one that does not exist.

### `PATCH /contacts/{contact}`

Change any of the fields; only what the body carries is touched.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `email` | string | A new address. It must not belong to another contact. |
| `first_name` | string | Up to 200 characters. |
| `last_name` | string | Up to 200 characters. |
| `unsubscribed` | boolean | The contact's global subscription state. `true` is a global unsubscribe and writes a consent record; `false` resubscribes. |
| `properties` | object | Merged key by key into the stored map. A `null` value removes the key. |

Update a contact:

```sh
curl -X PATCH "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "last_name": "Williams-Smith",
  "properties": {
    "seats": 15
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    last_name: "Williams-Smith",
    properties: {
      seats: 15
    }
  }),
});

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

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "last_name": "Williams-Smith",
    "properties": {
      "seats": 15
    }
  },
)

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

#### Response `200`

```json
{
  "object": "contact",
  "id": "e169aa45-1ecf-4183-9955-b1499d5701d3"
}
```

- Emits `contact.updated`. Changing the contact's topics or segments through their own routes does not.

### `DELETE /contacts/{contact}`

Erase the contact's personal data. Not a soft delete.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |

Delete a contact:

```sh
curl -X DELETE "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3", {
  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/e169aa45-1ecf-4183-9955-b1499d5701d3",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact",
  "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "deleted": true
}
```

- The address and names are removed and the contact is `404` from that moment. Consent records are kept with a hash of the address only, so a later opt-out can still be proven.
- Emits `contact.deleted`, carrying the contact as it was.

### `POST /contact-properties`

Declare a typed key contacts can carry a value for.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `key` (required) | string | Up to 50 characters, letters, digits and underscores, unique within the team. It is also the merge variable a broadcast can print. |
| `type` (required) | string | `string` or `number`. |
| `fallback_value` | string \| number \| null | What a broadcast prints for a contact with no value. Must match `type`. |

Create a contact property:

```sh
curl -X POST "https://api.rasket.com/contact-properties" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "key": "company",
  "type": "string"
}'
```

```ts
const response = await fetch("https://api.rasket.com/contact-properties", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    key: "company",
    type: "string"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/contact-properties",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "key": "company",
    "type": "string"
  },
)

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

#### Response `201`

```json
{
  "object": "contact_property",
  "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
}
```

- A key that already exists is `422 invalid_parameter`.
- `boolean` properties exist too, but arrive only through a CSV import's column map.

### `GET /contact-properties`

Every declared property, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `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 contact properties:

```sh
curl -X GET "https://api.rasket.com/contact-properties" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contact-properties", {
  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/contact-properties",
    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": [
    {
      "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
      "key": "company",
      "type": "string",
      "fallback_value": "your company",
      "created_at": "2026-09-08T22:22:17.595Z"
    }
  ]
}
```

### `GET /contact-properties/{contact_property_id}`

One declaration by ID.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_property_id` (required) | string | The contact property's ID. |

Retrieve a contact property:

```sh
curl -X GET "https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e", {
  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/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact_property",
  "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
  "key": "company",
  "type": "string",
  "fallback_value": "your company",
  "created_at": "2026-09-08T22:22:17.595Z"
}
```

- `fallback_value` is absent, not `null`, on a property that has none.

### `PATCH /contact-properties/{contact_property_id}`

Change the fallback value. The key and the type are fixed.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_property_id` (required) | string | The contact property's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `fallback_value` | string \| number \| null | The new fallback, matching the property's type. `null` clears it. |

Update a contact property:

```sh
curl -X PATCH "https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "fallback_value": "your company"
}'
```

```ts
const response = await fetch("https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    fallback_value: "your company"
  }),
});

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

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "fallback_value": "your company"
  },
)

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

#### Response `200`

```json
{
  "object": "contact_property",
  "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
}
```

### `DELETE /contact-properties/{contact_property_id}`

Remove the declaration, and the key from every contact that had a value.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_property_id` (required) | string | The contact property's ID. |

Delete a contact property:

```sh
curl -X DELETE "https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e", {
  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/contact-properties/b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `200`

```json
{
  "object": "contact_property",
  "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
  "deleted": true
}
```

- A segment whose filter names the property stops matching on it.

### `GET /contacts/{contact}/segments`

Every segment the contact is in right now.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `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 a contact's segments:

```sh
curl -X GET "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments", {
  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/e169aa45-1ecf-4183-9955-b1499d5701d3/segments",
    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": [
    {
      "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
      "name": "Trial accounts",
      "created_at": "2026-09-08T22:22:17.595Z"
    }
  ]
}
```

- Both kinds of membership are listed: segments the contact was added to explicitly and segments whose filter it matches. `created_at` is when the contact was added for the first, and the later of the two creations for the second.

### `POST /contacts/{contact}/segments/{segment}`

An explicit membership, alongside whatever the segment's filter matches.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |
| `segment` (required) | string | The segment's ID. |

Add a contact to a segment:

```sh
curl -X POST "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf", {
  method: "POST",
  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.post(
    "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "contact_segment",
  "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf"
}
```

- Adding a contact that is already a member changes nothing and is not an error.

### `DELETE /contacts/{contact}/segments/{segment}`

Remove an explicit membership.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact` (required) | string | The contact's ID or its email address. |
| `segment` (required) | string | The segment's ID. |

Remove a contact from a segment:

```sh
curl -X DELETE "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf", {
  method: "DELETE",
  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.delete(
    "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "contact_segment",
  "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "deleted": true
}
```

- A contact the segment's filter matches stays a member: a filter is a rule, not a list. To exclude someone, change the filter or unsubscribe them.

### `GET /contacts/{contact_id}/topics`

Every live topic, with the contact's effective subscription to each.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_id` (required) | string | The contact's ID or its email address. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `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 a contact's topics:

```sh
curl -X GET "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/topics" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/topics", {
  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/e169aa45-1ecf-4183-9955-b1499d5701d3/topics",
    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": [
    {
      "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
      "name": "Product updates",
      "description": "What shipped this month.",
      "subscription": "opt_in"
    }
  ]
}
```

- `subscription` is the contact's own choice where one was recorded, and the topic's `default_subscription` otherwise.

### `PATCH /contacts/{contact_id}/topics`

Record an explicit choice for each topic named.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_id` (required) | string | The contact's ID or its email address. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `topics` (required) | object[] | `{ id, subscription }` entries, with `subscription` one of `opt_in` or `opt_out`. |

Update a contact's topics:

```sh
curl -X PATCH "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/topics" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "topics": [
    {
      "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
      "subscription": "opt_out"
    }
  ]
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/topics", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    topics: [
      {
        id: "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
        subscription: "opt_out"
      }
    ]
  }),
});

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

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/topics",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "topics": [
      {
        "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
        "subscription": "opt_out"
      }
    ]
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "contact_topics",
  "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "topics": [
    {
      "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
      "subscription": "opt_out"
    }
  ]
}
```

- Every change writes a consent record with its source and time, which is what makes an opt-out provable later.
- A topic id that is not a live topic of the team is `422 invalid_parameter`.
