Contacts
A contact is a person identified by an email address, a personal LinkedIn profile URL, or both. Each identity is unique within a workspace.
The contact object
{
"id": 42,
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Martin",
"full_name": "Alice Martin",
"company_id": 3,
"company_name": "Acme Corp",
"company": { "id": 3, "name": "Acme Corp" },
"phone": "+33123456789",
"country": "France",
"city": "Paris",
"linkedin_url": "https://linkedin.com/in/alice",
"job_title": "CTO",
"status": "active",
"lifecycle_stage": "lead",
"custom_fields": { "account_tier": "enterprise" },
"subscribed_at": "2026-02-23T10:00:00.000000Z",
"unsubscribed_at": null,
"created_at": "2026-02-23T10:00:00.000000Z",
"updated_at": "2026-02-23T10:00:00.000000Z",
"lists": [{ "id": 1, "name": "Newsletter", "color": "#6366f1", "contacts_count": 120 }],
"tags": [{ "id": 5, "name": "VIP", "color": "#f59e0b" }]
}| Field | Type | Description |
|---|---|---|
full_name | string | First and last name. Falls back to the email or LinkedIn identity when both are empty. |
email | string | null | Email identity. Required when linkedin_url is omitted. |
linkedin_url | string | null | Canonical personal LinkedIn profile URL. Required when email is omitted. |
company_id | integer | null | Linked CRM company in the same workspace. |
company_name | string | null | Display name of the linked company. |
company | object | null | Linked company summary with id and name. |
status | enum | active, unsubscribed, bounced, complained |
lifecycle_stage | enum | null | subscriber, lead, marketing_qualified_lead, sales_qualified_lead, opportunity, customer, evangelist, or other |
custom_fields | object | Values for workspace-defined CRM contact fields. Only writable text fields can be set. |
lists, tags | array | Included on list, get, create and update responses |
Endpoints
| Method | Path | Permission |
|---|---|---|
| GET | /contacts | contacts:read |
| POST | /contacts | contacts:write |
| GET | /contacts/{id} | contacts:read |
| PUT | /contacts/{id} | contacts:write |
| DELETE | /contacts/{id} | contacts:write |
| GET | /contacts/{id}/timeline | contacts:read |
| POST | /contacts/{id}/unsubscribe | contacts:write |
| POST | /contacts/bulk-create | contacts:write |
| PUT | /contacts/bulk-update | contacts:write |
| DELETE | /contacts/bulk-delete | contacts:write |
| POST | /contacts/import | contacts:import |
| GET | /contacts/export | contacts:export |
List contacts
GET /contactsReturns contacts from the token workspace. Results are ordered by created_at desc, then id desc, unless another sort is requested.
Pass filters in the URL query string. Invalid values and relationship IDs outside the token workspace return 422.
| Query | Type | Description |
|---|---|---|
page | integer | Page number, minimum 1, default 1. |
per_page | integer | Contacts per page, from 1 to 100, default 15. |
search | string | Partial search over email, LinkedIn URL, first name, last name, job title and linked company. Every term must match; at most six terms are used. |
status | enum | active, unsubscribed, bounced, or complained. |
lifecycle_stage | enum | subscriber, lead, marketing_qualified_lead, sales_qualified_lead, opportunity, customer, evangelist, or other. |
company_id | integer | Only contacts linked to this workspace company. |
list_id | integer | Only contacts in this workspace list. |
tag_id | integer | Only contacts with this workspace tag. |
has_email | boolean | 1 keeps contacts with an email; 0 keeps contacts without one. |
has_linkedin | boolean | 1 keeps contacts with a personal LinkedIn URL; 0 keeps contacts without one. |
has_company | boolean | 1 keeps contacts linked to a company; 0 keeps unlinked contacts. |
sort | enum | created_at, updated_at, first_name, last_name, email, linkedin_url, job_title, status, or lifecycle_stage. |
direction | enum | asc or desc. Default desc. |
curl --get https://app.beeving.com/api/v1/contacts \
-H "Authorization: Bearer TOKEN" \
-H "Accept: application/json" \
--data-urlencode "page=2" \
--data-urlencode "per_page=15" \
--data-urlencode "search=alice acme" \
--data-urlencode "lifecycle_stage=lead" \
--data-urlencode "has_email=1" \
--data-urlencode "sort=first_name" \
--data-urlencode "direction=asc"The response contains data, navigation URLs in links, and meta with current_page, last_page, per_page, total, and count for the current page.
Create a contact
POST /contacts| Field | Type | Rules |
|---|---|---|
email | string | null | Required when linkedin_url is omitted. Valid email, unique in the workspace. |
linkedin_url | string | null | Required when email is omitted. Personal LinkedIn profile URL, unique in the workspace, max 500 characters. |
first_name, last_name, job_title | string | null | Max 255 characters. |
company_id | integer | null | ID of a company in the same workspace. |
phone | string | null | Max 50 characters. |
country, city | string | null | Max 100 characters. |
lifecycle_stage | string | null | subscriber, lead, marketing_qualified_lead, sales_qualified_lead, opportunity, customer, evangelist, or other. |
custom_fields | object | null | Up to 50 text values keyed by workspace-defined writable CRM contact fields. Unknown or read-only keys are rejected; values are at most 5000 characters. |
custom_properties | — | Prohibited. Use custom_fields. |
tags | integer[] | Tag IDs |
lists | integer[] | List IDs |
A new contact always starts with status: active.
curl -X POST https://app.beeving.com/api/v1/contacts \
-H "Authorization: Bearer TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Martin",
"lists": [1],
"custom_fields": { "plan": "enterprise" }
}'201 Created: { "data": Contact }
Get a contact
GET /contacts/{id}200: { "data": Contact }. Returns 404 if the contact isn't in your workspace.
Update a contact
PUT /contacts/{id}This is a partial update: send only the fields you want to change.
- It accepts the same fields as create, plus
status. - A field sent as
nullis ignored. Null does not clear the value. - When
tagsorlistsis sent, it replaces the contact's current tags or lists.
200: { "data": Contact }
Delete a contact
DELETE /contacts/{id}204 No Content
Timeline
GET /contacts/{id}/timelineReturns the contact's 50 most recent email events. This endpoint is not paginated.
{
"data": [
{
"type": "email_sent",
"subject": "Quick question",
"campaign_id": 12,
"status": "sent",
"sent_at": "2026-03-01T09:00:00.000000Z",
"opened_at": "2026-03-01T10:12:00.000000Z",
"clicked_at": null,
"created_at": "2026-03-01T09:00:00.000000Z"
}
]
}Unsubscribe
POST /contacts/{id}/unsubscribeSets status to unsubscribed and records unsubscribed_at.
200: { "data": Contact, "message": "Contact unsubscribed successfully" }
Bulk create
POST /contacts/bulk-create{
"contacts": [
{ "email": "a@example.com", "first_name": "A" },
{ "linkedin_url": "https://www.linkedin.com/in/example", "custom_fields": { "source": "webinar" } }
]
}- Up to 1000 contacts per call.
- Each item requires
emailorlinkedin_urland acceptsfirst_name,last_name,company_id,phone,lifecycle_stage, and relationalcustom_fields. - Existing email or LinkedIn identities are reported as failures. They are not updated.
201
{
"data": [ /* created contacts */ ],
"message": "Bulk create completed",
"created": 1,
"failed": 1,
"errors": [{ "index": 0, "email": "a@example.com", "error": "Contact already exists" }]
}Bulk update
PUT /contacts/bulk-update{
"contact_ids": [1, 2, 3],
"updates": { "status": "unsubscribed" }
}updates accepts these keys:
statuscustom_fields: replaces the whole object
200: { "data": { "message": "Bulk update completed", "updated": 3 } }
Bulk delete
DELETE /contacts/bulk-delete{ "contact_ids": [1, 2, 3] }200: { "data": { "message": "Bulk delete completed", "deleted": 3 } }
Import a CSV
POST /contacts/importSend multipart/form-data:
| Field | Description |
|---|---|
file | Required. A .csv or .txt file, max 10 MB |
mapping[email], mapping[linkedin_url] | Map at least one identity column. Both may be mapped. |
mapping[first_name], mapping[last_name], mapping[company_id], mapping[phone], mapping[lifecycle_stage] | Optional column names. company_id accepts a workspace company ID or unambiguous company name. |
mapping[custom_fields.KEY] | Optional column for an existing writable CRM contact field. |
curl -X POST https://app.beeving.com/api/v1/contacts/import \
-H "Authorization: Bearer TOKEN" \
-H "Accept: application/json" \
-F "file=@contacts.csv" \
-F "mapping[email]=Email" \
-F "mapping[first_name]=First name"Rows without a valid email or personal LinkedIn profile URL are skipped.
200
{
"data": {
"message": "Import completed",
"created": 120,
"skipped": 4,
"errors": 0,
"error_details": []
}
}error_details lists the first 10 errors as { "row": 12, "error": "..." }.
Export as CSV
GET /contacts/export?status=active&search=acmeReturns a text/csv file named contacts-YYYY-MM-DD.csv. The export uses import-compatible columns for Email, LinkedIn URL, names, job title, phone, location, lifecycle stage, CRM company ID, and each writable relational custom field. LinkedIn-only contacts are included and can be imported again.