Skip to content

Units of Measure

MC0RE edited this page Aug 18, 2026 · 2 revisions

Units of Measure

Read unit of measure definitions in Teamleader Focus.

Overview

Units of measure define how products are quantified β€” pieces, hours, kilograms, metres, etc. They are read-only through the API and must be configured in the Teamleader Focus web interface.

Access via Teamleader::unitsOfMeasure().

unitsOfMeasure.list is the only endpoint. There is no info, no filtering, no sorting and no pagination β€” the specification declares no request body for it at all. list() posts an empty body and returns every unit in one response. All helpers are client-side.

Passing arguments to list() throws since v2.2.0. The method inherits the standard list(array $filters = [], array $options = []) signature, but the endpoint cannot honour any of it. Before v2.2.0 the arguments were silently discarded β€” see Why this throws.

Endpoint

unitsOfMeasure

Capabilities

Capability Supported
Pagination ❌ Not supported
Filtering ❌ Not supported
Sorting ❌ Not supported
Sideloading ❌ Not supported
Creation ❌ Not supported
Update ❌ Not supported
Deletion ❌ Not supported

Methods

list()

Returns all units in a single response. Takes no arguments.

use McoreServices\TeamleaderSDK\Facades\Teamleader;

$units = Teamleader::unitsOfMeasure()->list();

Any argument throws before the request is sent:

Teamleader::unitsOfMeasure()->list([], ['page_size' => 5]);
// InvalidArgumentException: unitsOfMeasure.list does not support pagination.
// Passed: page_size. The endpoint returns every record in a single response,
// so there are no pages to request.

info()

Not available. unitsOfMeasure.info does not exist in the API. Calling it throws with a pointer to the alternative:

Teamleader::unitsOfMeasure()->info('unit-uuid');
// InvalidArgumentException: unitsOfMeasure has no info endpoint β€” list() is the
// only one the API exposes. Use findById() to resolve a single unit from the
// full list.

Before v2.2.0 this raised Error: Call to undefined method instead, since neither this class nor its parent defined info().


Why this throws

The method signature offered filtering, sorting and pagination that the endpoint has never supported, and the arguments were quietly dropped:

$first  = Teamleader::unitsOfMeasure()->list([], ['page_size' => 5, 'page_number' => 1]);
$second = Teamleader::unitsOfMeasure()->list([], ['page_size' => 5, 'page_number' => 2]);

count($first['data']);              // 16 β€” not 5
$first['data'] === $second['data']; // true β€” page 2 was page 1

That produced a specific and delayed failure. A sync layer that pages through an entity and soft-deletes anything the enumeration didn't mention appeared to work with sixteen units: page 1 came back shorter than the page size, so the pager concluded it had reached the end. That was a coincidence, not correctness. Past the page size, page 2 returns page 1 again, the same records are re-processed forever, and the loop never terminates.

Failing at the call site is the alternative. If you are passing arguments here, the right fix is to stop β€” the full list is always returned.

There is no paginate() on this resource. Only Tags and Work Types define one.


Helper Methods

All helpers are client-side β€” they call list() then search or transform in PHP. Each makes one API call.

findByName(string $name)

Case-insensitive exact match, whitespace trimmed. Returns the matching unit array or null.

$unit = Teamleader::unitsOfMeasure()->findByName('piece');
$unit = Teamleader::unitsOfMeasure()->findByName('Hour'); // case-insensitive

findById(string $id)

Returns the unit with that UUID or null. This is the replacement for the info() endpoint the API doesn't provide.

$unit = Teamleader::unitsOfMeasure()->findById('unit-uuid');

asOptions()

Returns flat [id => name] map.

$options = Teamleader::unitsOfMeasure()->asOptions();
// ['uuid-1' => 'piece', 'uuid-2' => 'kilogram', 'uuid-3' => 'hour']

asCollection()

Returns a Laravel Collection for fluent manipulation.

$metreLike = Teamleader::unitsOfMeasure()->asCollection()
    ->filter(fn($u) => str_contains(strtolower($u['name']), 'meter'));

exists(string $name)

Returns true if a unit with that name exists. Calls findByName() internally.

$exists = Teamleader::unitsOfMeasure()->exists('piece');

count()

Returns the total number of configured units.

$total = Teamleader::unitsOfMeasure()->count();

Each helper triggers its own list() call. Chaining several β€” exists() then findByName() then count() β€” costs three requests against your rate limit budget. Call list() once and work with the result if you need more than one.


Response Structure

[
    'data' => [
        ['id' => 'uuid', 'name' => 'piece'],
        ['id' => 'uuid', 'name' => 'kilogram'],
        ['id' => 'uuid', 'name' => 'hour'],
        ['id' => 'uuid', 'name' => 'meter'],
    ],
    'headers' => ['X-RateLimit-Remaining' => ['199'], /* ... */],
]

headers is added by the SDK on every successful response and carries the rate-limit budget. There is no meta block and no pagination metadata β€” the complete list is always returned.


Usage Examples

Look up a unit and use it on a product

$unit = Teamleader::unitsOfMeasure()->findByName('hour');

if (! $unit) {
    throw new \Exception("Unit 'hour' not found β€” configure it in Teamleader first.");
}

Teamleader::products()->create([
    'name'               => 'Consulting Hour',
    'unit_of_measure_id' => $unit['id'],
    // ...
]);

Cache the list

Units rarely change, and every helper costs a request:

$options = Cache::remember('tl_units_of_measure', 86400, fn () =>
    Teamleader::unitsOfMeasure()->asOptions()
);

Resolve several units in one call

$units = collect(Teamleader::unitsOfMeasure()->list()['data'])
    ->keyBy(fn ($unit) => strtolower($unit['name']));

$hourId  = $units['hour']['id']  ?? null;
$pieceId = $units['piece']['id'] ?? null;

Error Handling

use InvalidArgumentException;

// Filtering
try {
    Teamleader::unitsOfMeasure()->list(['ids' => ['unit-uuid']]);
} catch (InvalidArgumentException $e) {
    // 'unitsOfMeasure.list does not support filtering. Passed: ids. Call list()
    //  without filters; the endpoint returns every record. See getCapabilities()
    //  for what this resource supports.'
}

// Pagination
try {
    Teamleader::unitsOfMeasure()->list([], ['page_size' => 5, 'page_number' => 2]);
} catch (InvalidArgumentException $e) {
    // 'unitsOfMeasure.list does not support pagination. Passed: page_size, page_number. ...'
}

// Sorting
try {
    Teamleader::unitsOfMeasure()->list([], ['sort' => 'name']);
} catch (InvalidArgumentException $e) {
    // 'unitsOfMeasure.list does not support sorting. Passed: sort. Records come
    //  back in the order the API chooses.'
}

// No info endpoint
try {
    Teamleader::unitsOfMeasure()->info('unit-uuid');
} catch (InvalidArgumentException $e) {
    // 'unitsOfMeasure has no info endpoint ... Use findById() ...'
}

Related Resources

Clone this wiki locally