Skip to content
MC0RE edited this page Aug 20, 2026 · 3 revisions

Orders

Retrieve orders in Teamleader Focus.

Overview

The Orders resource provides read-only access to orders. Orders are created when a quotation is accepted, or entered manually in Teamleader β€” there is no create, update or delete endpoint.

list() returns a summary per order. info() returns full detail including grouped_lines.

Access via Teamleader::orders().

list() paginates since v2.2.3. Before that the resource sent no page parameter and there was no way past the first twenty orders β€” no meta block, no page count, no error. An account with 1,100 orders returned 20 and looked complete. If you are enumerating orders, use all().

Pagination is not in the API specification. @teamleader/focus-api-specification declares only filter and includes on orders.list, where 40 of the 58 .list endpoints declare page. The endpoint honours it regardless β€” verified live: size: 100 returned all 30 orders in a test account, size: 5 returned five on page 1 and five different records on page 2. An ignored parameter cannot do that.

ids is the only filter. department_id, updated_since, order_date_after, term and status are each accepted by the API and ignored, returning the full unfiltered set with HTTP 200. Since v2.2.3 the SDK throws instead of sending them.

Sorting is accepted by the API and discarded. Sorting by order_date returns the same first record as no sort at all. Since v2.2.3 a sort option throws rather than silently doing nothing.

There is no withCustomFields() method, despite earlier versions of this page showing one. The fluent method is with('custom_fields').

Endpoint

orders

Capabilities

Capability Supported
Pagination βœ… Supported (undeclared in the spec β€” see above)
Filtering βœ… Supported (ids only)
Sorting ❌ Not supported β€” passing one throws
Sideloading βœ… Supported (custom_fields)
Creation ❌ Not supported
Update ❌ Not supported
Deletion ❌ Not supported

Methods

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

use McoreServices\TeamleaderSDK\Facades\Teamleader;

// First page β€” 20 records, the API default
$orders = Teamleader::orders()->list();

// Explicit page
$orders = Teamleader::orders()->list([], ['page_size' => 100, 'page_number' => 1]);

// Specific orders by ID
$orders = Teamleader::orders()->list(['ids' => ['order-uuid-1', 'order-uuid-2']]);

// Sideloading β€” both option keys work since v2.2.2
$orders = Teamleader::orders()->list([], ['include' => 'custom_fields']);

Passing neither page option sends no page key, so existing calls behave exactly as before v2.2.3.

Unknown filter keys throw since v2.2.3:

Teamleader::orders()->list(['updated_since' => '2026-01-01T00:00:00+00:00']);
// InvalidArgumentException: Unsupported filter key for orders.list: updated_since.
// Supported: ids. The API ignores every other filter key and returns the full set.

all(array $filters = [], array $options = [], int $maxPages = 100)

Added in v2.2.3. Walks every page and returns one data array.

$orders = Teamleader::orders()->all();

// With custom fields on every page
$orders = Teamleader::orders()->all([], ['include' => 'custom_fields']);

count($orders['data']); // every order in the account

The endpoint returns no total count, so the end of the list is inferred from a page shorter than the one requested. A complete final page therefore costs one extra empty request, and the number of requests a full pass needs cannot be known in advance.

$maxPages is a runaway guard, not a limit. Reaching it with a full page still coming throws a TeamleaderException rather than returning a partial set that looks complete β€” which is the failure this method exists to prevent. The default of 100 pages of 100 covers 10,000 orders.

The signature differs from PaymentMethods::all() and TaxRates::all(), which take (array $filters, int $maxPages). $options sits in the middle so a sideload can be applied to every page: the fluent include state is consumed by the first request, so with('custom_fields')->all() would otherwise sideload page 1 and nothing after it. all() resolves the include once and replays it on each page.


info(string $id, mixed $includes = null)

Returns the full order including grouped_lines.

$order = Teamleader::orders()->info('order-uuid');

$order = Teamleader::orders()->info('order-uuid', 'custom_fields');

// Fluent β€” the method is with(), not withCustomFields()
$order = Teamleader::orders()->with('custom_fields')->info('order-uuid');

Unlike Companies, info() does not validate includes. An unrecognised value is sent, ignored by the API, and returns 200 with no sideloaded data. custom_fields is the only include either endpoint accepts.


Helper Methods

byIds(array $ids)

$orders = Teamleader::orders()->byIds(['order-uuid-1', 'order-uuid-2']);

Sends no page parameter, so it returns at most 20 records. Add pagination via list() directly if you are requesting more ids than that.

getPaymentTermTypes()

Returns the local list of payment term types β€” no API call. Matches the enum in the API specification.

Teamleader::orders()->getPaymentTermTypes();
// ['cash', 'end_of_month', 'after_invoice_date']

getSupplierTypes()

Returns the local list of supplier types β€” no API call. Matches the enum in the API specification.

Teamleader::orders()->getSupplierTypes();
// ['company', 'contact']

Filters

Verified against @teamleader/focus-api-specification. This is the complete set β€” anything else throws.

Filter Type Description
ids array or string Filter by order UUIDs. A lone string wraps

The omission is deliberate, not an oversight in this SDK. orders.list declares no other filter, and the API silently ignores the ones people reach for β€” a deliberately invented filter key was accepted too, which is how the others were shown to be ignored rather than merely unhelpful.

There is no deal_id filter and no updated_since filter, and Teamleader publishes no order webhooks. A full pass with all() is the only mechanism available for keeping a local copy in step.


Sideloading

Include Endpoint Description
custom_fields orders.list, orders.info Custom field values on the order

Pass as options['include'] on list() and all(), as the $includes argument on info(), or via the fluent with('custom_fields'). Both include and includes are accepted as the option key since v2.2.2.

See Sideloading for general patterns.


Response Structure

list() response

[
    'data' => [
        [
            'id'            => 'order-uuid',
            'name'          => 'Project Services Q1',
            'status'        => 'delivered',       // see note below
            'order_date'    => '2025-01-15',      // nullable
            'order_number'  => 32,                // integer, nullable
            'delivery_date' => '2025-01-22',      // nullable
            'payment_term'  => [
                'type' => 'after_invoice_date',   // cash | end_of_month | after_invoice_date
                'days' => 30,
            ],
            'total' => [
                'tax_exclusive'                => ['amount' => 1000.0, 'currency' => 'EUR'],
                'tax_inclusive'                => ['amount' => 1210.0, 'currency' => 'EUR'],
                'purchase_price_tax_exclusive' => ['amount' => 750.0, 'currency' => 'EUR'],
                'purchase_price_tax_inclusive' => ['amount' => 907.5, 'currency' => 'EUR'],
                'taxes' => [
                    [
                        'rate'    => 0.21,
                        'taxable' => ['amount' => 1000.0, 'currency' => 'EUR'],
                        'tax'     => ['amount' => 210.0, 'currency' => 'EUR'],
                    ],
                ],
            ],
            'supplier'   => ['type' => 'company', 'id' => 'company-uuid'],   // company | contact
            'department' => ['type' => 'department', 'id' => 'department-uuid'],
            'deal'       => ['type' => 'deal', 'id' => 'deal-uuid'],
            'project'    => ['type' => 'project', 'id' => 'project-uuid'],   // legacy projects only
            'assignee'   => ['type' => 'user', 'id' => 'user-uuid'],
            'web_url'    => 'https://focus.teamleader.eu/order_detail.php?id=order-uuid',
            'custom_fields' => [                                             // only with the include
                ['definition' => ['type' => 'customFieldDefinition', 'id' => 'definition-uuid'], 'value' => 'Q1'],
            ],
        ],
    ],
    'headers' => ['X-RateLimit-Remaining' => ['199'], /* ... */],
]

There is no meta block. orders.list declares none and takes no pagination include, so there is no total count and no page count. The only end-of-list signal is a short page.

status is not in the API specification. It is present on live records β€” "status": "delivered" β€” but absent from both the list and info schemas, so the full set of values is unconfirmed. Treat it as observed rather than documented.

order_number is an integer, not a string.

project refers to the old projects module only. Next-gen project links appear per line item on info().

info() response

Everything from list(), plus a grouped_lines array of sections with line items. Each line item carries product, quantity, description, unit, unit_price, tax, discount, total, product_category, and β€” for next-gen projects β€” project, group and purchase_price.


Usage Examples

Enumerate every order

$orders = Teamleader::orders()->all();

foreach ($orders['data'] as $order) {
    // ...
}

Prefer this over list() in any sync or reconciliation job. list() without page options returns twenty records and gives no indication that more exist.

Fetch all orders for a deal

orders.list has no deal_id filter, so filter in PHP β€” but page through the full set first, or you are filtering twenty records rather than all of them.

$all = Teamleader::orders()->all();

$dealOrders = array_filter(
    $all['data'],
    fn ($order) => ($order['deal']['id'] ?? null) === $dealId
);

Page manually

If you need to process orders in batches rather than holding them all in memory:

$page = 1;

do {
    $response = Teamleader::orders()->list([], ['page_size' => 100, 'page_number' => $page]);

    foreach ($response['data'] as $order) {
        // ...
    }

    $page++;
} while (count($response['data']) === 100);

Read custom fields

$order = Teamleader::orders()->with('custom_fields')->info('order-uuid');

foreach ($order['data']['custom_fields'] ?? [] as $field) {
    echo $field['definition']['id'].': '.$field['value']."\n";
}

Error Handling

use InvalidArgumentException;
use McoreServices\TeamleaderSDK\Exceptions\{NotFoundException, TeamleaderException};

// Unsupported filter key β€” thrown before the request
try {
    Teamleader::orders()->list(['status' => 'delivered']);
} catch (InvalidArgumentException $e) {
    // 'Unsupported filter key for orders.list: status. Supported: ids.
    //  The API ignores every other filter key and returns the full set.'
}

// Sorting β€” thrown before the request
try {
    Teamleader::orders()->list([], ['sort' => 'order_date']);
} catch (InvalidArgumentException $e) {
    // 'orders.list does not support sorting. Passed: sort.
    //  Records come back in the order the API chooses.'
}

// all() hit its page cap with records still pending
try {
    $orders = Teamleader::orders()->all([], [], 5);
} catch (TeamleaderException $e) {
    // 'orders.list still had records after 5 pages of 100 (500 retrieved).
    //  Raise $maxPages if the account is genuinely this large β€” returning a
    //  partial set here would look complete to the caller.'
}

try {
    Teamleader::orders()->info('order-uuid');
} catch (NotFoundException $e) {
    // Order does not exist
}

Related Resources

Clone this wiki locally