All docs

Contacts

List, create, update, and delete contacts in your workspace

Manage contacts in your graph8 workspace — your first-party CRM data.

Contacts get into your workspace through several paths: saving results from the Search endpoints, importing from a CSV or CRM, creating manually via the API, or automatically during enrichment. Once a contact is in your workspace, these endpoints give you full read/write access at no credit cost.


List Contacts

GET /contacts

Returns a paginated list of contacts.

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
limitinteger50Items per page (1-200)
emailstring—Filter by work email (exact match)
list_idinteger—Filter by list membership
namestring—Filter by first or last name (partial match, case-insensitive)
job_titlestring—Filter by job title (partial match, case-insensitive)
seniority_levelstring—Filter by seniority level (exact match)
company_namestring—Filter by company name (partial match, case-insensitive)
company_idinteger—Filter by company ID
countrystring—Filter by country (exact match)
statestring—Filter by state (exact match)
citystring—Filter by city (partial match, case-insensitive)
job_departmentstring—Filter by job department (partial match, case-insensitive)
industrystring—Filter by company industry (partial match, case-insensitive)
include_custom_fieldsbooleanfalseWhen true, each contact gains a custom_fields object of its custom column values keyed by column slug (name from GET /fields). Default false keeps the legacy shape and adds no extra query.

All filters are optional and can be combined. When multiple filters are provided, results must match all of them (AND logic).

Example

cURL

# Basic pagination
curl "https://be.graph8.com/api/v1/contacts?limit=10&page=1" \
  -H "Authorization: Bearer $API_KEY"

# Search by name and company
curl "https://be.graph8.com/api/v1/contacts?name=jane&company_name=acme" \
  -H "Authorization: Bearer $API_KEY"

# Filter by seniority and country
curl "https://be.graph8.com/api/v1/contacts?seniority_level=VP&country=US" \
  -H "Authorization: Bearer $API_KEY"

# Filter by department and industry
curl "https://be.graph8.com/api/v1/contacts?job_department=Engineering&industry=SaaS" \
  -H "Authorization: Bearer $API_KEY"

Python

# Basic pagination
response = requests.get(
    f"{BASE_URL}/contacts",
    headers=HEADERS,
    params={"limit": 10, "page": 1}
)
contacts = response.json()

# Search by name and company
response = requests.get(
    f"{BASE_URL}/contacts",
    headers=HEADERS,
    params={"name": "jane", "company_name": "acme"}
)

# Filter by department and industry
response = requests.get(
    f"{BASE_URL}/contacts",
    headers=HEADERS,
    params={"job_department": "Engineering", "industry": "SaaS"}
)

TypeScript

// Basic pagination
const response = await fetch(`${BASE_URL}/contacts?limit=10&page=1`, {
  headers: { Authorization: `Bearer ${API_KEY}` }
});
const contacts = await response.json();

// Search by name and company
const filtered = await fetch(
  `${BASE_URL}/contacts?name=jane&company_name=acme`,
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);

// Filter by department and industry
const deptFiltered = await fetch(
  `${BASE_URL}/contacts?job_department=Engineering&industry=SaaS`,
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);

Response

{
  "data": [
    {
      "id": 101,
      "first_name": "Jane",
      "last_name": "Doe",
      "work_email": "jane@acme.com",
      "direct_phone": "+1...",
      "mobile_phone": "+1...",
      "job_title": "VP Sales",
      "job_department": "Sales",
      "seniority_level": "VP",
      "linkedin_url": "https://...",
      "city": "Berlin",
      "state": "BE",
      "country": "DE",
      "company_id": 1,
      "owner_id": "user_x",
      "owner_name": "John",
      "custom_fields": null
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "has_next": false
  }
}

When called with ?include_custom_fields=true, custom_fields is an object like {"udo_lead_score_1712000000": "92", "udo_persona_1712000111": "Champion"} (only set values appear; keys are the column slug). Rows with no custom values return {}.


Get Contact

GET /contacts/{contact_id}

Returns detailed information about a single contact, including associated company data.

Path Parameters

ParameterTypeDescription
contact_idintegerContact ID

Query Parameters

ParameterTypeDefaultDescription
include_custom_fieldsbooleanfalseWhen true, the response gains a custom_fields object (same shape as the list endpoint). Default omits it (custom_fields: null).

Example

cURL

curl "https://be.graph8.com/api/v1/contacts/1" \
  -H "Authorization: Bearer $API_KEY"

Python

response = requests.get(f"{BASE_URL}/contacts/1", headers=HEADERS)
contact = response.json()["data"]

TypeScript

const response = await fetch(`${BASE_URL}/contacts/1`, {
  headers: { Authorization: `Bearer ${API_KEY}` }
});
const { data: contact } = await response.json();

Response

{
  "data": {
    "id": 1,
    "first_name": "Jane",
    "last_name": "Smith",
    "full_name": "Jane Smith",
    "work_email": "jane@acme.com",
    "personal_emails": null,
    "direct_phone": "+1-555-0100",
    "mobile_phone": null,
    "job_title": "VP of Engineering",
    "job_department": "Engineering",
    "seniority_level": "VP",
    "linkedin_url": "https://linkedin.com/in/janesmith",
    "twitter_url": null,
    "facebook_url": null,
    "city": "San Francisco",
    "state": "CA",
    "country": "US",
    "about": null,
    "confidence_score": 0.95,
    "company": {
      "id": 42,
      "name": "Acme Inc",
      "domain": "acme.com",
      "website": "https://acme.com",
      "industry": "Technology",
      "employee_count": "500",
      "city": "San Francisco",
      "state": "CA",
      "country": "US",
      "linkedin_url": "https://linkedin.com/company/acme",
      "logo_url": "https://logo.clearbit.com/acme.com"
    },
    "meta_data": {},
    "custom_fields": null,
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-02-01T14:20:00Z"
  }
}

Errors

StatusMeaning
404Contact not found

Create Contact

POST /contacts

Create a new contact and add it to a list.

Request Body

FieldTypeRequiredDescription
list_idintegerYesTarget list to add the contact to
first_namestringNoFirst name
last_namestringNoLast name
work_emailstringNoWork email address
personal_emailsstringNoPersonal email addresses
direct_phonestringNoDirect phone number
mobile_phonestringNoMobile phone number
job_titlestringNoJob title
job_departmentstringNoDepartment
seniority_levelstringNoSeniority level
linkedin_urlstringNoLinkedIn profile URL
citystringNoCity
statestringNoState or province
countrystringNoCountry
company_domainstringNoCompany domain for matching

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/contacts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 5,
    "first_name": "John",
    "last_name": "Doe",
    "work_email": "john@techcorp.io",
    "job_title": "CTO",
    "company_domain": "techcorp.io"
  }'

Python

response = requests.post(
    f"{BASE_URL}/contacts",
    headers=HEADERS,
    json={
        "list_id": 5,
        "first_name": "John",
        "last_name": "Doe",
        "work_email": "john@techcorp.io",
        "job_title": "CTO",
        "company_domain": "techcorp.io"
    }
)
result = response.json()["data"]

TypeScript

const response = await fetch(`${BASE_URL}/contacts`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    list_id: 5,
    first_name: "John",
    last_name: "Doe",
    work_email: "john@techcorp.io",
    job_title: "CTO",
    company_domain: "techcorp.io"
  })
});
const { data: result } = await response.json();

Response 201 Created

{
  "data": {
    "status": "ok",
    "count": 1,
    "validation_errors": []
  }
}

Update Contact

PATCH /contacts/{contact_id}

Update one or more fields on a contact. Only include the fields you want to change.

Path Parameters

ParameterTypeDescription
contact_idintegerContact ID

Request Body

All fields are optional. Include only the fields to update.

FieldTypeDescription
first_namestringFirst name
last_namestringLast name
work_emailstringWork email address
direct_phonestringDirect phone number
mobile_phonestringMobile phone number
job_titlestringJob title
job_departmentstringDepartment
seniority_levelstringSeniority level
linkedin_urlstringLinkedIn profile URL
citystringCity
statestringState or province
countrystringCountry

Example

cURL

curl -X PATCH "https://be.graph8.com/api/v1/contacts/1" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_title": "CTO", "city": "New York"}'

Python

response = requests.patch(
    f"{BASE_URL}/contacts/1",
    headers=HEADERS,
    json={"job_title": "CTO", "city": "New York"}
)

TypeScript

const response = await fetch(`${BASE_URL}/contacts/1`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ job_title: "CTO", city: "New York" })
});

Response

{
  "data": {
    "updated": 1
  }
}

Errors

StatusMeaning
400No fields provided to update
404Contact not found

Delete Contact

DELETE /contacts/{contact_id}

Soft-delete a contact. The contact is marked as deleted but not permanently removed.

Path Parameters

ParameterTypeDescription
contact_idintegerContact ID

Example

cURL

curl -X DELETE "https://be.graph8.com/api/v1/contacts/1" \
  -H "Authorization: Bearer $API_KEY"

Python

response = requests.delete(f"{BASE_URL}/contacts/1", headers=HEADERS)

TypeScript

const response = await fetch(`${BASE_URL}/contacts/1`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${API_KEY}` }
});

Response

{
  "data": {
    "deleted": true
  }
}

Errors

StatusMeaning
404Contact not found

Upsert Contact

PUT /contacts/assert

Single contact upsert. Matches an existing contact by work_email first, then by linkedin_url. Creates a new contact if no match is found.

Request Body

FieldTypeRequiredDescription
list_idintegerYesTarget list
work_emailstringConditionalWork email — at least one of work_email or linkedin_url is required
linkedin_urlstringConditionalLinkedIn profile URL — at least one of work_email or linkedin_url is required
first_namestringNoFirst name
last_namestringNoLast name
personal_emailsstringNoPersonal email addresses
direct_phonestringNoDirect phone number
mobile_phonestringNoMobile phone number
job_titlestringNoJob title
job_departmentstringNoDepartment
seniority_levelstringNoSeniority level
citystringNoCity
statestringNoState or province
countrystringNoCountry
company_domainstringNoCompany domain for matching

Example

cURL

curl -X PUT "https://be.graph8.com/api/v1/contacts/assert" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 12345,
    "work_email": "jane@acme.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "job_title": "VP Sales",
    "company_domain": "acme.com"
  }'

Response

{ "data": { "action": "created", "count": 1 } }

The action field is either "created" or "updated".


Bulk Upsert Contacts

PUT /contacts/assert/batch

Bulk upsert of up to 100 contacts. Uses the same match keys as the single upsert (work_email then linkedin_url).

Request Body

FieldTypeRequiredDescription
list_idintegerYesTarget list for all contacts in this batch
contactsarrayYesList of contact objects (1–100 items). Each item follows the same field set as PUT /contacts/assert, with at least one of work_email or linkedin_url per row.

Example

cURL

curl -X PUT "https://be.graph8.com/api/v1/contacts/assert/batch" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 12345,
    "contacts": [
      {
        "work_email": "alice@acme.com",
        "first_name": "Alice",
        "last_name": "Smith",
        "job_title": "CEO",
        "company_domain": "acme.com"
      },
      {
        "linkedin_url": "https://linkedin.com/in/bobdoe",
        "first_name": "Bob",
        "last_name": "Doe"
      }
    ]
  }'

Response

{ "data": { "total": 100, "created": 87, "updated": 13, "errors": [] } }

List Contact Columns

GET /contacts/columns

List custom contact columns defined in your workspace.

Query Parameters

ParameterTypeDefaultDescription
list_idinteger—Filter columns by list. Omit to return all (including global) columns.

Example

cURL

curl "https://be.graph8.com/api/v1/contacts/columns" \
  -H "Authorization: Bearer $API_KEY"

# Filter by list
curl "https://be.graph8.com/api/v1/contacts/columns?list_id=12345" \
  -H "Authorization: Bearer $API_KEY"

Response

{
  "data": [
    {
      "id": 1,
      "title": "Job Level",
      "name": "job_level",
      "data_type": "text",
      "is_global": true
    }
  ]
}

Create Contact Column

POST /contacts/columns/create

Create a custom contact column. Optionally provision an enrichment waterfall pipeline in the same request so the column is immediately enrichable.

Request Body

FieldTypeRequiredDescription
titlestringYesDisplay name for the column
data_typestringYesColumn data type — currently only "text" is supported
list_idintegerNoScope the column to a specific list. Omit (or null) for a global column. Required when enrichment is provided (waterfall pipelines are list-scoped).
created_bystringNoEmail of the user creating the column
enrichmentobjectNoEnrichment waterfall config — strongly recommended for enrichable columns. When supplied, a companion waterfall pipeline is provisioned in the same request so the UI Enrich button and POST /enrichment/waterfall/enrich work out-of-the-box. Without it the column exists but is not enrichable until you call POST /enrichment/waterfall/configs.

enrichment object

FieldTypeDescription
typestringEnrichment type — use "waterfall"
providersarrayOrdered list of provider slugs. Mutually exclusive with steps. Allowed values: graph8, hunter, prospeo, dropcontact, icypeas, leadmagic, emaillistverify, apollo, lusha, rocketreach.
email_verificationobjectOptional email verification config (see below)

enrichment.email_verification object

FieldTypeDescription
providerstringVerification provider (e.g. "zerobounce")
enabledbooleanWhether verification is active
use_system_credentialsbooleanUse the platform’s credentials instead of your own
accept_catchallbooleanWhether to accept catch-all addresses
valid_statusesarrayList of statuses to accept (e.g. ["valid"])

Example

cURL

# Basic column (no enrichment)
curl -X POST "https://be.graph8.com/api/v1/contacts/columns/create" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Job Level",
    "data_type": "text",
    "created_by": "user@graph8.com"
  }'

# Column with enrichment waterfall
curl -X POST "https://be.graph8.com/api/v1/contacts/columns/create" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Job Level",
    "data_type": "text",
    "list_id": 12345,
    "created_by": "user@graph8.com",
    "enrichment": {
      "type": "waterfall",
      "providers": ["hunter", "prospeo"],
      "email_verification": {
        "provider": "zerobounce",
        "enabled": true,
        "use_system_credentials": true,
        "accept_catchall": false,
        "valid_statuses": ["valid"]
      }
    }
  }'

Response 201 Created

Without enrichment:

{
  "data": {
    "id": 1,
    "title": "Job Level",
    "data_type": "text",
    "name": "job_level",
    "object_id": "12345",
    "is_global": false,
    "enrichment": null
  }
}

With enrichment:

{
  "data": {
    "id": 1,
    "title": "Job Level",
    "data_type": "text",
    "name": "job_level",
    "object_id": "12345",
    "is_global": false,
    "enrichment": {
      "column_id": "job_level",
      "config_id": "uuid",
      "name": "Job Level Enrichment",
      "type": "waterfall",
      "providers": ["hunter", "prospeo"],
      "email_verification": { "enabled": true, "provider": "zerobounce" }
    }
  }
}
Build with graph8