-
-
Notifications
You must be signed in to change notification settings - Fork 0
Contacts
Manage contacts in Teamleader Focus CRM.
The Contacts resource provides full CRUD operations for contact records. Beyond standard CRUD it exposes tag management, avatar upload, and company link management (link, unlink, update).
Access via Teamleader::contacts().
contacts.infoaccepts no includes at all. The API declares no includes parameter on that endpoint β custom fields and the price list come back automatically. Passing any include toinfo()throws.
contacts.listaccepts exactly one include:custom_fields.
price_listis not an include. It is returned automatically whenever the account has access to price lists, and isnullwhen no price list is set on the contact. Link one by passingprice_list_idon create or update β andnullto remove it.
withPriceList()was removed in v2.2.0. It requested a value the API does not recognise; because the field is returned by default, it looked like it worked.
contacts
| Capability | Supported |
|---|---|
| Pagination | β Supported |
| Filtering | β Supported |
| Sorting | β
Supported (name, added_at, updated_at) |
| Sideloading | β
Supported on list() only |
| Creation | β Supported |
| Update | β Supported |
| Deletion | β Supported |
use McoreServices\TeamleaderSDK\Facades\Teamleader;
$contacts = Teamleader::contacts()->list();
$contacts = Teamleader::contacts()->list(
['status' => 'active', 'company_id' => 'company-uuid'],
['page_size' => 50, 'sort' => 'name', 'sort_order' => 'asc']
);
// Sideloading β custom_fields is the only include contacts.list accepts
$contacts = Teamleader::contacts()->list([], ['include' => 'custom_fields']);$contact = Teamleader::contacts()->info('contact-uuid');Custom fields and the price list are returned automatically. There is no includes parameter on this endpoint, so passing one throws:
Teamleader::contacts()->info('contact-uuid', 'custom_fields');
// InvalidArgumentException: contacts.info accepts no includes parameter.
// Custom fields and price_list are returned automatically. Use list() if you
// need includes=custom_fields.The same applies to chaining the fluent method into info() β use list() for that.
$contact = Teamleader::contacts()->create([
'first_name' => 'Sarah',
'last_name' => 'De Smedt',
'salutation' => 'mrs', // optional
'gender' => 'female', // optional β see below
'language' => 'nl',
'birthdate' => '1989-08-19',
'iban' => 'BE12123412341234',
'bic' => 'GEBABEBB',
'price_list_id' => 'price-list-uuid',
'marketing_mails_consent' => true,
'emails' => [['type' => 'primary', 'email' => 'sarah@acme.be']],
'telephones' => [['type' => 'mobile', 'number' => '+32 475 12 34 56']],
'tags' => ['Decision Maker'],
]);At least one of first_name or last_name is required β the SDK throws before sending if both are absent.
Gender validation:
gendermust be one offemale,male,non_binary,prefers_not_to_say,unknown. Earlier versions of this page listed only three; the API and the SDK both accept five. AnInvalidArgumentExceptionis thrown for any other value.
The id is injected into the request body before posting to contacts.update.
Teamleader::contacts()->update('contact-uuid', [
'last_name' => 'De Smedt-Janssen',
'telephones' => [['type' => 'mobile', 'number' => '+32 475 99 88 77']],
]);Clearing a field. null is preserved in the payload, so passing it clears a nullable field rather than being silently stripped:
// Remove the price list from a contact
Teamleader::contacts()->update('contact-uuid', ['price_list_id' => null]);
// Clear banking details
Teamleader::contacts()->update('contact-uuid', ['iban' => null, 'bic' => null]);This only works from v1.2.6 onward. Before that,
validateContactData()strippednullalong with empty strings, so field clears never reached the API β no error, the value simply stayed as it was.
Teamleader::contacts()->delete('contact-uuid');Uploads or removes a contact avatar. The image must be a base64 data URI starting with data:image/. Pass null to remove. An InvalidArgumentException is thrown if a non-null value doesn't start with data:image/.
Returns empty array (HTTP 204) on success.
$imageData = base64_encode(file_get_contents('/path/to/avatar.jpg'));
Teamleader::contacts()->uploadAvatar('contact-uuid', 'data:image/jpeg;base64,' . $imageData);
// Remove avatar
Teamleader::contacts()->uploadAvatar('contact-uuid', null);Links a contact to a company. Optional data: position (string), decision_maker (bool).
// Basic link
Teamleader::contacts()->linkToCompany('contact-uuid', 'company-uuid');
// With position and decision-maker flag
Teamleader::contacts()->linkToCompany('contact-uuid', 'company-uuid', [
'position' => 'CEO',
'decision_maker' => true,
]);Teamleader::contacts()->unlinkFromCompany('contact-uuid', 'company-uuid');Updates position and/or decision_maker on an existing link.
Teamleader::contacts()->updateCompanyLink('contact-uuid', 'company-uuid', [
'position' => 'Managing Director',
'decision_maker' => true,
]);Teamleader::contacts()->tag('contact-uuid', ['VIP', 'Decision Maker']);
Teamleader::contacts()->tag('contact-uuid', 'Newsletter'); // string also acceptedTeamleader::contacts()->untag('contact-uuid', ['Prospect']);Makes two separate API calls internally. Returns ['tagged' => [...], 'untagged' => [...]].
Teamleader::contacts()->manageTags(
'contact-uuid',
['Active', 'Customer'],
['Lead', 'Prospect']
);
contacts.tagandcontacts.untagare real endpoints, unlike the Deals equivalents which were removed in v2.2.0 because no such endpoint exists there.
| Method | Filter applied |
|---|---|
search(string $term) |
term (first name, last name, email, telephone) |
byEmail(string $email) |
email β {type: primary, email: $email}
|
forCompany(string $companyId) |
company_id |
active() |
status: active |
deactivated() |
status: deactivated |
withTags(string|array $tags) |
tags |
updatedSince(string $date) |
updated_since |
All helpers accept an optional $options array as their last parameter.
Unlike Companies,
withTags()works here βcontacts.listdoes declare atagsfilter. Note also thatactive()anddeactivated()exist on Contacts but not on Companies.
| Method | Include added | Valid on |
|---|---|---|
withCustomFields() |
custom_fields |
list() |
That is the only one. withPriceList() was removed in v2.2.0.
Verified against @teamleader/focus-api-specification. This is the complete set.
| Filter | Type | Description |
|---|---|---|
ids |
array | Filter by UUIDs |
email |
array or string | Email β string auto-wraps as {type: primary}
|
company_id |
string | Contacts linked to this company |
term |
string | Searches first name, last name, email, telephone |
updated_since |
string | ISO 8601 datetime |
tags |
array | All specified tags must be present |
status |
string |
active or deactivated β a string, not an array |
marketing_mails_consent |
bool | Marketing consent flag |
statusis one of the exceptions to the "status filters are arrays" rule on Home. The API declares it as a string enum here, the same as on Companies.
| Field | Description |
|---|---|
name |
First name + last name |
added_at |
Date added |
updated_at |
Date last updated |
$contacts = Teamleader::contacts()->list([], [
'sort' => 'name',
'sort_order' => 'asc',
]);| Include | Endpoint | Description |
|---|---|---|
custom_fields |
contacts.list |
Custom field values |
That is the complete list. contacts.info has no includes parameter at all.
price_list, addresses, responsible_user and tags are returned by default where applicable and cannot β and need not β be requested.
See Sideloading for general patterns.
[
'data' => [
[
'id' => 'contact-uuid',
'first_name' => 'Sarah',
'last_name' => 'De Smedt',
'salutation' => 'mrs',
'gender' => 'female',
'status' => 'active',
'language' => 'nl',
'emails' => [['type' => 'primary', 'email' => 'sarah@acme.be']],
'price_list' => ['type' => 'priceList', 'id' => 'price-list-uuid'],
'companies' => [
[
'customer' => ['type' => 'company', 'id' => 'company-uuid'],
'position' => 'CEO',
'decision_maker' => true,
],
],
'added_at' => '2025-01-15T10:00:00+00:00',
'updated_at' => '2025-03-01T09:00:00+00:00',
],
],
'headers' => ['X-RateLimit-Remaining' => ['199'], /* ... */],
]There is no
metablock. Earlier versions of this page showed one withpageandmatchesβ the API returns pagination metadata only when a resource sendsincludes=pagination, and Contacts does not. There is no total count, so the end of a list is a page shorter than the requested page size.
price_listis present only when the account has access to price lists, and isnullwhen none is set on the contact.
$existing = Teamleader::contacts()->byEmail('sarah@acme.be');
if (!empty($existing['data'])) {
Teamleader::contacts()->update($existing['data'][0]['id'], ['last_name' => 'De Smedt-Janssen']);
} else {
Teamleader::contacts()->create([
'first_name' => 'Sarah',
'last_name' => 'De Smedt',
'emails' => [['type' => 'primary', 'email' => 'sarah@acme.be']],
]);
}$contact = Teamleader::contacts()->info('contact-uuid');
$isLinked = false;
foreach ($contact['data']['companies'] ?? [] as $link) {
if ($link['customer']['id'] === 'company-uuid') {
$isLinked = true;
break;
}
}
if ($isLinked) {
Teamleader::contacts()->updateCompanyLink('contact-uuid', 'company-uuid', [
'position' => 'Managing Director',
]);
} else {
Teamleader::contacts()->linkToCompany('contact-uuid', 'company-uuid', [
'position' => 'Managing Director',
]);
}$contacts = Teamleader::contacts()->forCompany('company-uuid');
$decisionMakers = array_filter($contacts['data'], function ($contact) {
foreach ($contact['companies'] ?? [] as $link) {
if ($link['decision_maker'] === true) return true;
}
return false;
});On info() they arrive automatically:
$contact = Teamleader::contacts()->info('contact-uuid');
foreach ($contact['data']['custom_fields'] ?? [] as $field) {
$definitionId = $field['definition']['id'];
$value = $field['value'];
}On list() they must be requested:
$contacts = Teamleader::contacts()
->withCustomFields()
->list(['status' => 'active']);$priceLists = Teamleader::priceLists()->list();
Teamleader::contacts()->update('contact-uuid', [
'price_list_id' => $priceLists['data'][0]['id'],
]);
// Later β remove it again
Teamleader::contacts()->update('contact-uuid', ['price_list_id' => null]);use InvalidArgumentException;
use McoreServices\TeamleaderSDK\Exceptions\{NotFoundException, ValidationException, TeamleaderException};
// Includes on info()
try {
Teamleader::contacts()->info('contact-uuid', 'custom_fields');
} catch (InvalidArgumentException $e) {
// 'contacts.info accepts no includes parameter. Custom fields and price_list
// are returned automatically. Use list() if you need includes=custom_fields.'
}
// Missing name
try {
Teamleader::contacts()->create(['emails' => [['type' => 'primary', 'email' => 'x@y.be']]]);
} catch (InvalidArgumentException $e) {
// 'Contact must have at least a first_name or last_name'
}
// Invalid gender value
try {
Teamleader::contacts()->create(['first_name' => 'Alex', 'gender' => 'other']);
} catch (InvalidArgumentException $e) {
// 'Invalid gender. Must be one of: female, male, non_binary, prefers_not_to_say, unknown'
}
// Invalid email format β checked client-side
try {
Teamleader::contacts()->create([
'first_name' => 'Alex',
'emails' => [['type' => 'primary', 'email' => 'not-an-email']],
]);
} catch (InvalidArgumentException $e) {
// 'Invalid email format: not-an-email'
}
// Invalid avatar URI
try {
Teamleader::contacts()->uploadAvatar('contact-uuid', 'plain-string');
} catch (InvalidArgumentException $e) {
// 'Image must be a base64 data URI (e.g. data:image/png;base64,...) or null to remove the avatar'
}
try {
Teamleader::contacts()->info('contact-uuid');
} catch (NotFoundException $e) {
// Contact does not exist
}- Companies β Contacts are linked to companies
-
Price Lists β Link via
price_list_idon create/update - Tags β Tag reference list
- Deals β Deals reference contacts as customers
-
Custom Fields β Definitions for the
custom_fieldssideload -
Files β Use
files()->forContact()to list attachments - Sideloading β Loading related data
- Filtering β Filter and pagination reference
Last Updated: August 2026 β’ SDK Version: 2.2.2 β’ Made with β€οΈ by MCore Services
- Departments
- Users
- Teams
- Custom Fields
- Work Types
- Document Templates
- Currencies
- Notes
- Email Tracking
- Closing Days
- Day Off Types
- Days Off
- User Schedules
- Invoices
- Credit Notes
- Subscriptions
- Payment Methods
- Payment Terms
- Tax Rates
- Withholding Tax Rates
- Commercial Discounts
Next Gen Projects
Legacy Projects