REST API
Prospects
Create a prospect, optionally enrol them in a campaign, list and read prospects with their activity columns, and update them by id. Fields, validation, PATCH semantics.
Updated 4 September 2026
A prospect is one person with one email address in your workspace. Every prospect belongs to a company (derived from the email domain or from company_name) unless the address is on a public mail provider and no company name is given.
POST /api/v1/prospects
Creates a prospect. Optional query parameter campaign=<uuid> also enrols them.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Trimmed and lower-cased. Must match ^[^\s@]+@[^\s@]+\.[a-z][a-z0-9-]*$ (the last domain label starts with a letter, so IP literals are rejected). |
first_name |
string or null | no | Trimmed; empty becomes null. |
last_name |
string or null | no | Same. |
timezone |
string or null | no | Free text such as Europe/Berlin. Not validated against a timezone database; the sending schedule falls back to the workspace timezone if it cannot be interpreted. |
company_name |
string or null | no | The company as you know it. Wins over the email domain when grouping. Whitespace is collapsed. |
custom_fields |
object | no | Keys are trimmed, empty keys dropped. Values may be string, number or boolean and are stored as strings. An object, array or null value fails with 400. Missing means {}. |
Unknown fields are ignored. Body limit 64 KiB.
curl -X POST https://app.seegnals.com/api/v1/prospects \
-H "Authorization: Bearer $SEEGNALS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "anke@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"company_name": "Nordwind Logistics",
"custom_fields": { "plant": "Bremen", "lines": 3 }
}'
const res = await fetch("https://app.seegnals.com/api/v1/prospects", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"email": "anke@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"company_name": "Nordwind Logistics",
"custom_fields": {
"plant": "Bremen",
"lines": 3
}
})
});
const json = await res.json().catch(() => null);
console.log(res.status, json);
import os, requests
res = requests.post(
"https://app.seegnals.com/api/v1/prospects",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"}, json={
"email": "anke@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"company_name": "Nordwind Logistics",
"custom_fields": {
"plant": "Bremen",
"lines": 3
}
},
timeout=20,
)
print(res.status_code, res.json() if res.content else None)
Response 201
{ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "email": "anke@nordwind.example", "company_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427" }
company_id is null for a public-mail address (gmail.com, outlook.com and the like) sent without company_name. That is a correct result, and your code should treat it as success.
How the company is chosen
company_name |
Email domain | Result |
|---|---|---|
| given | company domain | company keyed by domain and name |
| given | public mail provider | company keyed by name only |
| missing | company domain | company keyed by domain |
| missing | public mail provider | no company (company_id: null) |
A new company row is created when none matches. The company card in the app then shows this person alongside colleagues from the same domain.
Enrolling with ?campaign=
curl -X POST "https://app.seegnals.com/api/v1/prospects?campaign=4939897b-02fe-4974-ba88-1ea9c06bb447" \
-H "Authorization: Bearer $SEEGNALS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "anke@nordwind.example", "company_name": "Nordwind Logistics" }'
const res = await fetch("https://app.seegnals.com/api/v1/prospects?campaign=4939897b-02fe-4974-ba88-1ea9c06bb447", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"email": "anke@nordwind.example",
"company_name": "Nordwind Logistics"
})
});
const json = await res.json().catch(() => null);
console.log(res.status, json);
import os, requests
res = requests.post(
"https://app.seegnals.com/api/v1/prospects?campaign=4939897b-02fe-4974-ba88-1ea9c06bb447",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"}, json={
"email": "anke@nordwind.example",
"company_name": "Nordwind Logistics"
},
timeout=20,
)
print(res.status_code, res.json() if res.content else None)
- The campaign must belong to your workspace and be in draft or running state. Otherwise the request fails with
400 { "error": "Unknown or unusable campaign." }and nothing is created. - After the prospect is created, enrolment runs through the same path as Add prospects in the app. Suppressed addresses, people already in this campaign, people active in another campaign and prospects missing a field that a snippet needs are skipped, not errored.
- The HTTP response is the same
201whether or not the enrolment went through. The outcome is logged per call in Settings → Integrations → History (provider “Clay row → add to campaign”), which is where to look when a person did not start receiving the sequence. - Calls without
?campaign=are not logged to History.
Errors
| Status | Body |
|---|---|
400 |
{ "error": "Enter a valid email address.", "code": "invalid-email" } and the parser errors listed under Rate limits and errors |
409 |
{ "error": "anke@nordwind.example is already in your workspace.", "code": "duplicate" } |
500 |
{ "error": "Couldn't add the prospect: …", "code": "db-error" } |
502 |
{ "error": "Couldn't create or find a company for that prospect. Try again.", "code": "company-failed" } |
No email verification, suppression check or enrichment runs on create. Verification happens on CSV import in the app; suppression is checked at send time, so a suppressed address can be created but will never be emailed.
GET /api/v1/prospects
Lists prospects with the activity columns you see on the Prospects page.
Query: email (exact, case-insensitive), company_id (uuid), status (blocked, bounced, replied, active, contacted, not_contacted; resolved in that order, like the Status column), created_since (ISO 8601), limit and cursor. There is no updated_since: prospects have no modification timestamp, and the parameter is refused with 400 rather than ignored.
curl "https://app.seegnals.com/api/v1/prospects?status=replied&limit=2" -H "Authorization: Bearer $SEEGNALS_TOKEN"
const res = await fetch("https://app.seegnals.com/api/v1/prospects?status=replied&limit=2", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`
}
});
const json = await res.json().catch(() => null);
console.log(res.status, json);
import os, requests
res = requests.get(
"https://app.seegnals.com/api/v1/prospects?status=replied&limit=2",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"},
timeout=20,
)
print(res.status_code, res.json() if res.content else None)
{
"data": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": null,
"company": { "id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427", "name": "Example Ltd", "domain": "example.com" },
"timezone": null,
"custom_fields": { "role": "CTO" },
"tags": ["q3"],
"source": "API",
"verification_status": null,
"verification_checked_at": null,
"status": "replied",
"campaign_count": 1,
"emails_sent": 3,
"date_contacted": "2026-08-20T08:02:11.512+00:00",
"date_responded": "2026-08-25T16:19:38.123+00:00",
"created_at": "2026-08-19T12:00:00.000+00:00"
}
],
"next_cursor": "eyJrIjoiMjAyNi0wOC0xOVQxMjowMDowMC4wMDArMDA6MDAiLCJpIjoiN2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3In0"
}
Errors: 400 for a bad limit, cursor, company_id, status, created_since or an updated_since parameter; 401; 429.
GET /api/v1/prospects/{id}
One prospect in the shape above, without the data envelope. 404 { "error": "Prospect not found." } for an unknown id, another workspace’s id or a non-UUID.
PATCH /api/v1/prospects/{id}
Updates a prospect by id. Same authentication, limits and field rules as POST.
Semantics you must know
Send the full desired state. Despite the verb, this is not a partial update: email is required on every call, and an omitted first_name, last_name or timezone is set to null, an omitted custom_fields becomes {}. The one exception is company_name: omit it to keep the current company name, send null or "" to clear it.
Changing the email domain or the company name re-assigns the prospect to another (possibly new) company. The old company row is kept. source is not changed by PATCH.
curl -X PATCH https://app.seegnals.com/api/v1/prospects/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer $SEEGNALS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "anke.weber@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"custom_fields": { "plant": "Bremen", "lines": "4" }
}'
const res = await fetch("https://app.seegnals.com/api/v1/prospects/7c9e6679-7425-40de-944b-e07fc1f90ae7", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"email": "anke.weber@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"custom_fields": {
"plant": "Bremen",
"lines": "4"
}
})
});
const json = await res.json().catch(() => null);
console.log(res.status, json);
import os, requests
res = requests.patch(
"https://app.seegnals.com/api/v1/prospects/7c9e6679-7425-40de-944b-e07fc1f90ae7",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"}, json={
"email": "anke.weber@nordwind.example",
"first_name": "Anke",
"last_name": "Weber",
"timezone": "Europe/Berlin",
"custom_fields": {
"plant": "Bremen",
"lines": "4"
}
},
timeout=20,
)
print(res.status_code, res.json() if res.content else None)
Response 200
{ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "email": "anke.weber@nordwind.example", "company_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427" }
Errors
| Status | Body |
|---|---|
400 |
as for POST; a missing email is invalid-email |
404 |
{ "error": "Prospect not found.", "code": "not-found" } for an unknown id, another workspace’s id, or a value that is not a UUID |
409 |
{ "error": "<email> is already used by another prospect in your workspace.", "code": "duplicate" } |
500 |
{ "error": "Couldn't load the prospect: …" } or { "error": "Couldn't save changes: …" } (db-error) |
502 |
company-failed, as for POST |