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)
emailstringFilter by work email (exact match)
list_idintegerFilter by list membership
namestringFilter by first or last name (partial match, case-insensitive)
job_titlestringFilter by job title (partial match, case-insensitive)
seniority_levelstringFilter by seniority level (exact match)
company_namestringFilter by company name (partial match, case-insensitive)
company_idintegerFilter by company ID
countrystringFilter by country (exact match)
statestringFilter by state (exact match)
citystringFilter by city (partial match, case-insensitive)
job_departmentstringFilter by job department (partial match, case-insensitive)
industrystringFilter 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": "[email protected]",
      "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": "[email protected]",
    "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": "[email protected]",
    "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": "[email protected]",
        "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: "[email protected]",
    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": "[email protected]",
    "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": "[email protected]",
        "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_idintegerFilter 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": "[email protected]"
  }'

# 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": "[email protected]",
    "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