Seegnals

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 201 whether 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