---
title: Prospect contacts
description: Create, match, read, update and delete prospect contacts through the /prospects endpoints.
product: signal
section: api
order: 14
---

# Prospect contacts

A prospect contact is a person you're prospecting who isn't yet a Contact at one of your Accounts. In the API they live at **`/prospects`**: every `/prospects` endpoint reads or writes prospect contacts. A prospect contact can be linked to a [Prospect Company](prospect-companies.md).

`POST /prospects` is built for capturing prospect contacts from forms and other tools: it matches on email, so sending the same person twice updates them instead of creating a duplicate.

## The prospect contact object

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | Opaque string ID. |
| `first_name` | string | Can be empty. |
| `last_name` | string | Can be empty. |
| `email` | string or null | Stored in lowercase. |
| `phone` | string or null | |
| `job_title` | string or null | |
| `linkedin_url` | string or null | |
| `stage_id` | string | Prospect stage. See [Stage IDs](#stage-ids). |
| `owner_id` | string or null | User ID of the person in **Assigned To**. |
| `lead_company_id` | string or null | The linked [Prospect Company](prospect-companies.md). |
| `converted_at` | string or null | When the prospect contact was converted. Read-only. |
| `archived_at` | string or null | When it was archived. Read-only. |
| `created_at` | string | ISO 8601 timestamp. |
| `updated_at` | string | ISO 8601 timestamp. |
| `custom_fields` | object | Your custom fields for prospect contacts, keyed by internal name. See [Custom fields](../conventions.md#custom-fields). |

`source_id` (where the prospect contact came from) can be set on create and update but isn't returned in responses.

```json
{
  "id": "e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63",
  "first_name": "Sam",
  "last_name": "Lee",
  "email": "sam.lee@fabrikam.example",
  "phone": "+1 646 555 0123",
  "job_title": "Operations Director",
  "linkedin_url": null,
  "stage_id": "rps_new",
  "owner_id": "5b8d1f3a-7e2c-4a9b-b6d4-1c3e5f7a9b20",
  "lead_company_id": "cm3k9x2p10004qz8f7h2d1a6b",
  "converted_at": null,
  "archived_at": null,
  "created_at": "2026-11-12T09:30:00.000Z",
  "updated_at": "2026-11-12T09:30:00.000Z",
  "custom_fields": {
    "budget_band": "50k-100k",
    "wants_demo": true
  }
}
```

## Stage IDs

| `stage_id` | Stage |
| --- | --- |
| `rps_new` | New (the default for new prospect contacts) |
| `rps_contacted` | Contacted |
| `rps_qualified` | Qualified |
| `rps_converted` | Converted |
| `rps_disqualified` | Disqualified |

---

## List prospect contacts

`GET /prospects` · Scope: `prospects:read`

Returns prospect contacts newest first, with their custom fields. Archived prospect contacts aren't listed.

### Query parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `limit` | integer | No | 1 to 100. Default 20. |
| `cursor` | string | No | `meta.cursor` from the previous page. |

### Example

```bash
curl "https://crm.empiraa.app/api/v1/prospects?limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
const res = await fetch("https://crm.empiraa.app/api/v1/prospects?limit=100", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data, meta } = await res.json();
```

### Response `200`

An array of [prospect contact objects](#the-prospect-contact-object) in `data`. `meta` has `cursor`, `hasMore` and `total` (the number of prospect contacts in the workspace).

```json
{
  "data": [ { "id": "e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63", "first_name": "Sam", "last_name": "Lee", "email": "sam.lee@fabrikam.example", "stage_id": "rps_new", "custom_fields": {} } ],
  "meta": { "cursor": null, "total": 1, "hasMore": false },
  "error": null
}
```

(Shortened: each item is a full prospect contact object.)

---

## Create or update a prospect contact

`POST /prospects` · Scope: `prospects:write` · Supports [`Idempotency-Key`](../conventions.md#idempotency)

If a prospect contact with the same email already exists in the workspace, it's updated with the fields you send and the response is `200` with `"_action": "updated"`. Otherwise a new one is created and the response is `201` with `"_action": "created"`.

### Request body

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `email` | string or null | No | Used to find an existing prospect contact. Trimmed and lowercased. |
| `first_name` | string | No | |
| `last_name` | string | No | |
| `username` | string | No | Used as `first_name` when neither `first_name` nor `last_name` is sent. Handy for form tools that only send one name field. |
| `phone` | string or null | No | |
| `job_title` | string or null | No | |
| `linkedin_url` | string or null | No | |
| `stage_id` | string | No | See [Stage IDs](#stage-ids). New prospect contacts default to `rps_new`. |
| `source_id` | string or null | No | An active [prospect source](prospect-sources.md). |
| `owner_id` | string or null | No | **Assigned To**: an active user in the workspace. |
| `lead_company_id` | string or null | No | A [Prospect Company](prospect-companies.md) in the workspace. `null` unlinks. |
| `notes` | string | No | Added as a new [note](notes.md) on the prospect contact. Blank values are ignored, and existing notes are never removed. |
| `custom_fields` | object | No | Custom field values by internal name. |
| *any other field* | any | No | Treated as a custom field when its name matches a custom field's internal name. |

All fields are optional, but send at least an email or a name.

**Email from other fields.** If `email` is missing or empty, the first other field whose name contains "email" and whose value contains "@" is used as the email. This lets form tools that name the field differently (such as `work_email`) still match.

**When a prospect contact is matched,** only the fields you send are changed. An empty `first_name` or `last_name` doesn't overwrite an existing name.

### Example

```bash
curl -X POST https://crm.empiraa.app/api/v1/prospects \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2c7e4a90-6b1d-4f3e-8a5c-0d9b7e3f1a26" \
  -d '{
    "email": "sam.lee@fabrikam.example",
    "first_name": "Sam",
    "last_name": "Lee",
    "job_title": "Operations Director",
    "source_id": "rls_website",
    "notes": "Requested a demo from the pricing page.",
    "custom_fields": { "budget_band": "50k-100k" }
  }'
```

```js
const res = await fetch("https://crm.empiraa.app/api/v1/prospects", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    email: "sam.lee@fabrikam.example",
    first_name: "Sam",
    last_name: "Lee",
    job_title: "Operations Director",
    source_id: "rls_website",
    notes: "Requested a demo from the pricing page.",
    custom_fields: { budget_band: "50k-100k" },
  }),
});
const { data } = await res.json();
console.log(data._action); // "created" or "updated"
```

### Response `201` or `200`

```json
{
  "data": {
    "id": "e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63",
    "first_name": "Sam",
    "last_name": "Lee",
    "email": "sam.lee@fabrikam.example",
    "phone": null,
    "job_title": "Operations Director",
    "linkedin_url": null,
    "stage_id": "rps_new",
    "owner_id": null,
    "lead_company_id": null,
    "converted_at": null,
    "archived_at": null,
    "created_at": "2026-11-12T09:30:00.000Z",
    "updated_at": "2026-11-12T09:30:00.000Z",
    "custom_fields": { "budget_band": "50k-100k", "wants_demo": null },
    "_action": "created"
  },
  "meta": { "cursor": null },
  "error": null
}
```

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `owner_id`, `source_id` or `lead_company_id` isn't a string or `null`. |
| 404 | `NOT_FOUND` | `owner_id` isn't an active user in the workspace ("User not found"), `lead_company_id` isn't a Prospect Company in the workspace, or `source_id` isn't an active prospect source. |

**Webhook events:** [`prospect.created`](../webhooks.md#event-catalog) for a new prospect contact, [`prospect.updated`](../webhooks.md#event-catalog) for a matched one. Also starts any Autopilot workflows that run when a prospect contact is created, updated or changes stage.

---

## Get a prospect contact

`GET /prospects/{id}` · Scope: `prospects:read`

### Path parameters

| Parameter | Type | Notes |
| --- | --- | --- |
| `id` | string | Prospect contact ID. |

### Example

```bash
curl https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

### Response `200`

The [prospect contact object](#the-prospect-contact-object) in `data`.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `NOT_FOUND` | No prospect contact with that ID in this workspace. |

---

## Update a prospect contact

`PATCH /prospects/{id}` · Scope: `prospects:write`

Send only the fields you want to change.

### Request body

| Field | Type | Notes |
| --- | --- | --- |
| `first_name` | string | |
| `last_name` | string | |
| `email` | string or null | |
| `phone` | string or null | |
| `job_title` | string or null | |
| `linkedin_url` | string or null | |
| `stage_id` | string | See [Stage IDs](#stage-ids). |
| `source_id` | string or null | An active [prospect source](prospect-sources.md), or `null` to clear. |
| `owner_id` | string or null | **Assigned To**: an active user in the workspace, or `null` for nobody. |
| `lead_company_id` | string or null | A Prospect Company in the workspace, or `null` to unlink. |
| `notes` | string | Added as a new note. Blank values are ignored. |
| `custom_fields` | object | Custom field values by internal name. Other unrecognized top-level fields are also treated as custom fields. |

### Example

```bash
curl -X PATCH https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stage_id": "rps_qualified", "owner_id": "5b8d1f3a-7e2c-4a9b-b6d4-1c3e5f7a9b20"}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63", {
  method: "PATCH",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ stage_id: "rps_qualified", owner_id: "5b8d1f3a-7e2c-4a9b-b6d4-1c3e5f7a9b20" }),
});
```

### Response `200`

The updated [prospect contact object](#the-prospect-contact-object) in `data`.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `owner_id`, `source_id` or `lead_company_id` isn't a string or `null`. |
| 404 | `NOT_FOUND` | The prospect contact, user, Prospect Company or prospect source doesn't exist in this workspace. |

**Webhook events:** [`prospect.updated`](../webhooks.md#event-catalog).

---

## Delete a prospect contact

`DELETE /prospects/{id}` · Scope: `prospects:write`

Moves the prospect contact to **Recently deleted** in Signal. It stays there for 30 days, during which it can be restored from **Settings → Recently deleted**. After 30 days it's removed for good.

### Example

```bash
curl -X DELETE https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/prospects/e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63", {
  method: "DELETE",
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

### Response `204`

No body.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `NOT_FOUND` | No prospect contact with that ID in this workspace. |

**Webhook events:** [`prospect.deleted`](../webhooks.md#event-catalog).
