Skip to content

Commercial Discounts

MC0RE edited this page Aug 18, 2026 · 3 revisions

Commercial Discounts

Read commercial discount definitions in Teamleader Focus.

Overview

Commercial discounts are named discounts configured in Teamleader settings. They are department-scoped and read-only through the API.

Access via Teamleader::commercialDiscounts().

Response objects now carry an id. Teamleader added the field to commercialDiscounts.list in July 2026. Earlier versions of this page β€” and of the SDK β€” worked around its absence by using the discount name as an identifier. asOptions() is now keyed by UUID.

Applying a discount does not reference these records. When you put a discount on an invoice or credit note you send {type, value, description} β€” not a discount id. See Applying a discount.

No pagination or sorting. list() returns all discounts in a single response. Passing sort or page options throws since v2.2.0.

Endpoint

commercialDiscounts

Capabilities

Capability Supported
Pagination ❌ Not supported
Filtering βœ… Supported (department_id)
Sorting ❌ Not supported
Sideloading ❌ Not supported
Creation ❌ Not supported
Update ❌ Not supported
Deletion ❌ Not supported

Methods

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

use McoreServices\TeamleaderSDK\Facades\Teamleader;

// All discounts
$discounts = Teamleader::commercialDiscounts()->list();

// For a specific department
$discounts = Teamleader::commercialDiscounts()->list(['department_id' => 'dept-uuid']);

department_id is the only filter. Unknown filter keys throw, as do sort and page options:

Teamleader::commercialDiscounts()->list(['name' => 'Trade']);
// InvalidArgumentException: Unsupported filter key for commercialDiscounts.list:
// name. Supported: department_id.

There is no info() endpoint β€” use find().


Helper Methods

All helpers are client-side: they call list() and filter in PHP. Each makes its own API request.

forDepartment(string $departmentId)

$discounts = Teamleader::commercialDiscounts()->forDepartment('dept-uuid');

find(string $id)

Resolves a single discount by UUID from the full list. Added in v2.2.0, alongside the id field.

$discount = Teamleader::commercialDiscounts()->find('discount-uuid');
// ['id' => 'discount-uuid', 'name' => 'Early payment', 'department' => [...]]

Returns null if not found.

findByName(string $name, ?string $departmentId = null, bool $exactMatch = true)

Case-insensitive. Default is exact match.

$discount = Teamleader::commercialDiscounts()->findByName('Early payment');
$discount = Teamleader::commercialDiscounts()->findByName('early', null, false); // partial
$discount = Teamleader::commercialDiscounts()->findByName('Trade', 'dept-uuid');

search(string $searchTerm, ?string $departmentId = null)

Partial name match. Returns a plain array of matching discounts.

$matches = Teamleader::commercialDiscounts()->search('discount');

asOptions(?string $departmentId = null)

Returns [id => name].

$options = Teamleader::commercialDiscounts()->asOptions();
// ['discount-uuid-1' => 'Early payment', 'discount-uuid-2' => 'Trade']

$options = Teamleader::commercialDiscounts()->asOptions('dept-uuid');

This changed in v2.2.0. It previously returned [name => name], because the API returned no id. Two discounts sharing a name across departments collapsed into a single entry; they no longer do. If you were relying on the old keying, this is a breaking change in your code β€” the values are unchanged.

If a record arrives without an id β€” an account whose API has not yet been updated β€” the method falls back to using the name as the key for that entry.

names(?string $departmentId = null)

Returns a plain array of discount names.

$names = Teamleader::commercialDiscounts()->names();
// ['Early payment', 'Trade', 'Partner']

groupedByDepartment()

Returns [departmentId => ['department' => ..., 'discounts' => [...]]].

$grouped = Teamleader::commercialDiscounts()->groupedByDepartment();

exists(string $name, ?string $departmentId = null)

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

$exists = Teamleader::commercialDiscounts()->exists('Early payment');

Filters

Filter Type Description
department_id string Filter by department UUID

That is the complete set. Anything else throws.


Response Structure

[
    'data' => [
        [
            'id'         => 'discount-uuid',
            'name'       => 'Early payment',
            'department' => ['type' => 'department', 'id' => 'dept-uuid'],
        ],
        [
            'id'         => 'discount-uuid',
            'name'       => 'Trade',
            'department' => ['type' => 'department', 'id' => 'dept-uuid'],
        ],
    ],
    'headers' => ['X-RateLimit-Remaining' => ['199'], /* ... */],
]

id was added in July 2026. Earlier versions of this page stated there was no id and that the name was the only unique identifier. That is no longer true, and code written around it β€” keying maps by name, matching records by name across departments β€” is worth revisiting.

There is no meta block; the complete list is always returned.


Applying a discount

Reading a discount and applying one are separate things. The write side does not take a discount id β€” invoice and credit note endpoints accept a discount object built from scratch:

Field Type Notes
type string percentage β€” the only value the API declares
value number Between 0 and 100
description string Free text, e.g. winter promotion
Teamleader::invoices()->creditPartially(
    'invoice-uuid',
    '2026-02-04',
    $groupedLines,
    [['type' => 'percentage', 'value' => 15.5, 'description' => 'winter promotion']]
);

So this resource is a reference list β€” it tells you which discounts your account has configured, so you can present them or reuse their names and values. It is not a set of records you attach by id.

// Present configured discounts, then send the chosen one's shape
$discount = Teamleader::commercialDiscounts()->findByName('Early payment', 'dept-uuid');

$payload = [
    'type'        => 'percentage',
    'value'       => 5.0,               // the percentage is yours to supply
    'description' => $discount['name'],
];

The list response carries name and department only β€” no percentage value. The discount amount lives in Teamleader's settings UI and is not exposed here, so you supply the number yourself.


Usage Examples

Verify a discount exists before applying it

if (Teamleader::commercialDiscounts()->exists('Early payment', 'dept-uuid')) {
    // Safe to reference by that name
}

Build a select list

$options = Cache::remember("tl_discounts_{$deptId}", 3600, fn () =>
    Teamleader::commercialDiscounts()->asOptions($deptId)
);

// ['discount-uuid' => 'Early payment', ...]

Resolve everything in one call

Each helper is its own request, so if you need more than one thing, fetch once:

$discounts = collect(Teamleader::commercialDiscounts()->forDepartment($deptId)['data']);

$options = $discounts->pluck('name', 'id')->all();
$names   = $discounts->pluck('name')->all();
$early   = $discounts->firstWhere('name', 'Early payment');

Error Handling

use InvalidArgumentException;
use McoreServices\TeamleaderSDK\Exceptions\TeamleaderException;

// Unsupported filter key
try {
    Teamleader::commercialDiscounts()->list(['name' => 'Trade']);
} catch (InvalidArgumentException $e) {
    // 'Unsupported filter key for commercialDiscounts.list: name.
    //  Supported: department_id.'
}

// Sorting and pagination are not supported
try {
    Teamleader::commercialDiscounts()->list([], ['page_size' => 10]);
} catch (InvalidArgumentException $e) {
    // 'commercialDiscounts.list does not support pagination. ...'
}

// API-level errors
try {
    Teamleader::commercialDiscounts()->list();
} catch (TeamleaderException $e) {
    Log::error('Teamleader error', ['message' => $e->getMessage()]);
}

Related Resources

  • Invoices β€” Discounts applied at invoice level via creditPartially()
  • Quotations β€” Discounts applied at quotation level
  • Tax Rates β€” Per-line tax rates (department-scoped)
  • Departments β€” department_id filter reference

Clone this wiki locally