# Contacts

The lead ledger: list contacts with their first and last touch, read one, and pull the full visitor journey behind it.

Source: https://sourceloop.ai/help/api/contacts/

---

| Endpoint | Method | Path | Permission |
| --- | --- | --- | --- |
| List contacts | GET | `/v1/contacts` | `conversions:read` |
| Get contact | GET | `/v1/contacts/{id}` | `conversions:read` |
| Update contact | PATCH | `/v1/contacts/{id}` | `conversions:write` |
| Get contact journey | GET | `/v1/contacts/{id}/journey` | `conversions:read` |

Base URL: `https://app.sourceloop.ai/api/v1`

## List contacts

`GET /v1/contacts`

Your contacts, one row per conversion

Requires scope: `conversions:read`

One row per CONVERSION, not per person: someone who converts twice appears
twice. `identity_id` is on every row for callers who need to group by person.

Returns your leads with their contact details. New keys include the `pii:read`
scope by default, so email and phone come back in full.

If a key is created WITHOUT that scope, email, phone and name are masked and the
email domain is preserved, so the row still identifies the company. Use that for
keys given to contractors, reporting tools, or AI assistants.

Paginate with `cursor`, taking `next_cursor` from the previous response.

### Query parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `website` | string | no | Website domain, e.g. acme.com. Required only when the key covers more than one. |
| `period` | string | no | Plain-language period: "last 30 days", "yesterday", "July 2026", or "2026-07-01..2026-07-31". Ignored when from/to are given. |
| `type` | string | no |  |
| `event_name` | string | no |  |
| `status` | string | no |  |
| `email` | string | no | Requires the pii:read scope: searching by email is reading it. |
| `include_spam` | boolean | no |  |
| `cursor` | string | no |  |
| `limit` | integer | no |  |
| `total_count` | boolean | no | Include meta.total_count, the exact number of matching rows. Off by default because counting scans every match while the page itself reads one page, so on a large workspace the count costs far more than the rows. Paginate with has_more and next_cursor unless you are rendering "70 of 366". |

### Responses

- `200` Conversions
- `403` Something was wrong with the request or the credential.
- `429` Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.

### Example (cURL)

```bash
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
```

### Example (Node)

```javascript
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form",
  {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
    },
  },
);

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

### Example (Python)

```python
import os, requests

res = requests.get(
    "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()
```

## Get contact

`GET /v1/contacts/{id}`

One conversion, with its deals and value history

Requires scope: `conversions:read`

Adds the CRM deals this person is attached to and the full history of value changes, each with the reason and the system that made it.

### Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

### Responses

- `200` Conversion detail
- `404` Something was wrong with the request or the credential.
- `429` Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.

### Example (cURL)

```bash
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts/{id}" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
```

### Example (Node)

```javascript
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/contacts/{id}",
  {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
    },
  },
);

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

### Example (Python)

```python
import os, requests

res = requests.get(
    "https://app.sourceloop.ai/api/v1/contacts/{id}",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()
```

## Update contact

`PATCH /v1/contacts/{id}`

Write an outcome back

Requires scope: `conversions:write`

How your CRM tells Sourceloop that a lead became worth 14,000, which turns
cost-per-lead reporting into cost-per-revenue reporting.

Send an `Idempotency-Key` header. A retried request returns the original
result rather than writing twice, and a request that changes nothing writes
nothing at all. Both matter because a conversion write propagates to your
connected CRM and to ad-platform conversion upload.

Attribution fields are computed by Sourceloop and cannot be set.

### Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

### Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | A unique token per distinct request. Strongly recommended. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | no |  |
| `sales_value` | number | no |  |
| `quote_value` | number | no |  |
| `currency` | string | no |  |
| `notes` | string | no |  |
| `lead_status_raw` | string | no |  |
| `lifecycle_stage_raw` | string | no |  |
| `is_spam` | boolean | no |  |

### Responses

- `200` Updated, or unchanged
- `404` Something was wrong with the request or the credential.
- `429` Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.

### Example (cURL)

```bash
curl -s -X PATCH "https://app.sourceloop.ai/api/v1/contacts/{id}" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "won",
    "sales_value": 14000,
    "currency": "USD"
  }'
```

### Example (Node)

```javascript
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/contacts/{id}",
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "status": "won",
      "sales_value": 14000,
      "currency": "USD"
    }),
  },
);

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

### Example (Python)

```python
import os, requests

res = requests.patch(
    "https://app.sourceloop.ai/api/v1/contacts/{id}",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
    json={
      "status": "won",
      "sales_value": 14000,
      "currency": "USD"
    },
)
data = res.json()
```

## Get contact journey

`GET /v1/contacts/{id}/journey`

Every session behind one conversion, in order

Requires scope: `conversions:read`

The evidence the attribution numbers rest on: how this person arrived each
time, and what they did once there.

Devices are merged via the identity graph first, so someone who browsed on a
phone and converted on a laptop is one timeline rather than two half-journeys.

When no raw events exist (a CRM import, or a server-side conversion) the
timeline is reconstructed from stored first-touch and last-touch attribution
and flagged synthetic:true. Do not present that as a complete history.

### Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

### Query parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer | no |  |
| `limit` | integer | no |  |

### Responses

- `200` Journey
- `404` Something was wrong with the request or the credential.
- `429` Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.

### Example (cURL)

```bash
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
```

### Example (Node)

```javascript
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000",
  {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
    },
  },
);

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

### Example (Python)

```python
import os, requests

res = requests.get(
    "https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()
```
