Skip to content

Contacts

MC0RE edited this page Aug 18, 2026 · 3 revisions

Contacts

Manage contacts in Teamleader Focus CRM.

Overview

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.info accepts 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 to info() throws.

contacts.list accepts exactly one include: custom_fields.

price_list is not an include. It is returned automatically whenever the account has access to price lists, and is null when no price list is set on the contact. Link one by passing price_list_id on create or update β€” and null to 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.

Endpoint

contacts

Capabilities

Capability Supported
Pagination βœ… Supported
Filtering βœ… Supported
Sorting βœ… Supported (name, added_at, updated_at)
Sideloading βœ… Supported on list() only
Creation βœ… Supported
Update βœ… Supported
Deletion βœ… Supported

Methods

list(array $filters = [], array $options = [])

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']);

info(string $id)

$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.


create(array $data)

$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: gender must be one of female, male, non_binary, prefers_not_to_say, unknown. Earlier versions of this page listed only three; the API and the SDK both accept five. An InvalidArgumentException is thrown for any other value.


update(mixed $id, array $data)

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() stripped null along with empty strings, so field clears never reached the API β€” no error, the value simply stayed as it was.


delete(string $id)

Teamleader::contacts()->delete('contact-uuid');

uploadAvatar(string $id, string|null $image)

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);

Company Link Methods

linkToCompany(string $id, string $companyId, array $data = [])

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,
]);

unlinkFromCompany(string $id, string $companyId)

Teamleader::contacts()->unlinkFromCompany('contact-uuid', 'company-uuid');

updateCompanyLink(string $id, string $companyId, array $data = [])

Updates position and/or decision_maker on an existing link.

Teamleader::contacts()->updateCompanyLink('contact-uuid', 'company-uuid', [
    'position'       => 'Managing Director',
    'decision_maker' => true,
]);

Tag Methods

tag(string $id, string|array $tags)

Teamleader::contacts()->tag('contact-uuid', ['VIP', 'Decision Maker']);
Teamleader::contacts()->tag('contact-uuid', 'Newsletter'); // string also accepted

untag(string $id, string|array $tags)

Teamleader::contacts()->untag('contact-uuid', ['Prospect']);

manageTags(string $id, array $tagsToAdd = [], array $tagsToRemove = [])

Makes two separate API calls internally. Returns ['tagged' => [...], 'untagged' => [...]].

Teamleader::contacts()->manageTags(
    'contact-uuid',
    ['Active', 'Customer'],
    ['Lead', 'Prospect']
);

contacts.tag and contacts.untag are real endpoints, unlike the Deals equivalents which were removed in v2.2.0 because no such endpoint exists there.


Helper Methods

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.list does declare a tags filter. Note also that active() and deactivated() exist on Contacts but not on Companies.

Fluent include methods

Method Include added Valid on
withCustomFields() custom_fields list()

That is the only one. withPriceList() was removed in v2.2.0.


Filters

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

status is 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.


Sorting

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',
]);

Sideloading

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.


Response Structure

list() response

[
    '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 meta block. Earlier versions of this page showed one with page and matches β€” the API returns pagination metadata only when a resource sends includes=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_list is present only when the account has access to price lists, and is null when none is set on the contact.


Usage Examples

Upsert by email

$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']],
    ]);
}

Sync/update a company link

$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',
    ]);
}

Get all decision makers for a company

$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;
});

Read custom field values

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']);

Move a contact onto a price list, then remove it

$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]);

Error Handling

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
}

Related Resources

  • Companies β€” Contacts are linked to companies
  • Price Lists β€” Link via price_list_id on create/update
  • Tags β€” Tag reference list
  • Deals β€” Deals reference contacts as customers
  • Custom Fields β€” Definitions for the custom_fields sideload
  • Files β€” Use files()->forContact() to list attachments
  • Sideloading β€” Loading related data
  • Filtering β€” Filter and pagination reference

Clone this wiki locally