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

Deals

Manage sales deals in Teamleader Focus.

Overview

The Deals resource provides full CRUD operations for deal records, plus lifecycle methods (win, lose, move). Deals represent sales opportunities moving through your pipeline from open to won or lost.

Access via Teamleader::deals().

There are no tag methods on Deals. deals.tag and deals.untag do not exist in the Teamleader API, and there is no tags filter on deals.list. The SDK's tag(), untag() and withTags() were removed, along with the tags filter key. Tagging is available on Companies and Contacts, where the endpoints are real.

Unknown filter keys throw since v2.2.0. The API ignores filter keys it does not recognise and answers 200 with the full unfiltered set, so the SDK rejects them before sending.

Sorting worked for the first time in v2.2.0. list() called a buildSort() method that was never defined on this class, so passing a sort option raised Error: Call to undefined method. It now validates the field and builds the object shape the API expects.

Endpoint

deals

Capabilities

Capability Supported
Pagination βœ… Supported
Filtering βœ… Supported
Sorting βœ… Supported (created_at, weighted_value)
Sideloading βœ… Supported
Creation βœ… Supported
Update βœ… Supported
Deletion βœ… Supported

Methods

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

use McoreServices\TeamleaderSDK\Facades\Teamleader;

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

// Open deals only β€” status must be an array
$deals = Teamleader::deals()->list(['status' => ['open']]);

// With pagination and sorting
$deals = Teamleader::deals()->list([], [
    'page_size'   => 50,
    'page_number' => 1,
    'sort'        => 'created_at',
    'sort_order'  => 'desc',
]);

// With sideloading
$deals = Teamleader::deals()->list([], ['include' => 'lead.customer,responsible_user']);

Status filter: pass status as an array β€” ['open'], ['won'], ['lost']. A plain string is coerced to an array for you, but the array form is what the API declares.

Sideloading: the include option is sent as includes (plural) in the request body. Before v2.2.0 this method sent the singular form, which the API silently ignores β€” so ['include' => 'custom_fields'] returned deals with no custom fields and no error, while the fluent ->withCustomFields() form worked. The two now produce identical requests.


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

$deal = Teamleader::deals()->info('deal-uuid');

$deal = Teamleader::deals()->info('deal-uuid', 'lead.customer,responsible_user');

$deal = Teamleader::deals()
    ->withCustomer()
    ->withCurrentPhase()
    ->info('deal-uuid');

create(array $data)

Required fields (validated before the request):

  • lead.customer.type β€” contact or company (throws InvalidArgumentException for other values)
  • lead.customer.id β€” customer UUID
  • title β€” deal title

Other validated fields:

  • estimated_probability β€” must be between 0 and 1 inclusive
  • estimated_value.currency β€” must be a supported currency code
  • currency β€” must include both code and exchange_rate
  • custom_fields[] β€” each entry must include id and value
$deal = Teamleader::deals()->create([
    'title'  => 'New Business Deal',
    'lead'   => [
        'customer' => ['type' => 'company', 'id' => 'company-uuid'],
    ],
    'phase_id'               => 'phase-uuid',
    'estimated_value'        => ['amount' => 10000, 'currency' => 'EUR'],
    'estimated_probability'  => 0.75,
    'estimated_closing_date' => '2025-12-31',
    'responsible_user_id'    => 'user-uuid',
    'source_id'              => 'source-uuid',
    'purchase_order_number'  => 'PO-2025-042',
]);

update(mixed $id, array $data)

The id is injected into the request body before posting to deals.update.

Teamleader::deals()->update('deal-uuid', [
    'title'                  => 'Updated Deal Title',
    'estimated_probability'  => 0.90,
    'estimated_value'        => ['amount' => 15000, 'currency' => 'EUR'],
]);

delete(string $id)

Teamleader::deals()->delete('deal-uuid');

win(string $id)

Marks a deal as won.

Teamleader::deals()->win('deal-uuid');

lose(string $id, ?string $reasonId = null, ?string $extraInfo = null)

Marks a deal as lost. Both $reasonId and $extraInfo are optional and are omitted from the request when null.

// Without reason
Teamleader::deals()->lose('deal-uuid');

// With reason UUID from lostReasons resource
Teamleader::deals()->lose('deal-uuid', 'lost-reason-uuid');

// With reason and additional notes
Teamleader::deals()->lose('deal-uuid', 'lost-reason-uuid', 'Price too high for budget');

move(string $id, string $phaseId)

Moves a deal to a different phase. Throws InvalidArgumentException if $phaseId is empty.

Teamleader::deals()->move('deal-uuid', 'target-phase-uuid');

Helper Methods

Status shortcuts

All accept an optional $additionalFilters array.

$deals = Teamleader::deals()->open();
$deals = Teamleader::deals()->won();
$deals = Teamleader::deals()->lost();

// With extra filters
$deals = Teamleader::deals()->open(['responsible_user_id' => 'user-uuid']);

Filter helpers

Method Filter applied
search(string $term) term (title, reference, customer name)
forCustomer(string $type, string $id) customer β€” throws if type is not contact or company
byPhase(string $phaseId) phase_id
byIds(array $ids) ids
forUser(string $userId) responsible_user_id
updatedSince(string $date) updated_since
closingBetween(string $from, string $until) estimated_closing_date_from + estimated_closing_date_until
$deals = Teamleader::deals()->forCustomer('company', 'company-uuid');
$deals = Teamleader::deals()->byPhase('phase-uuid');
$deals = Teamleader::deals()->closingBetween('2025-07-01', '2025-09-30');
$deals = Teamleader::deals()->forUser('user-uuid', ['status' => ['open']]);

withTags() was removed in v2.2.0 β€” deals.list has no tags filter, so it returned every deal regardless of the tags passed.

Fluent include methods

Method Include
withCustomer() lead.customer
withResponsibleUser() responsible_user
withDepartment() department
withCurrentPhase() current_phase
withSource() source
withCustomFields() custom_fields
withAll() All of the above except custom_fields
$deals = Teamleader::deals()
    ->withCustomer()
    ->withResponsibleUser()
    ->withCurrentPhase()
    ->list(['status' => ['open']]);

These are queued and consumed when the request is built. Before v2.2.0 they raised Error: Call to undefined method Deals::with() β€” the fluent interface had never worked in any released version, on any resource.


Filters

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

Filter Type Description
ids array Filter by UUIDs. A single string is wrapped for you
term string Searches title, reference, customer name
status array open, won, lost β€” a string is wrapped for you
customer array {type: contact|company, id: uuid}
phase_id string Phase UUID
pipeline_ids array Pipeline UUIDs. A single string is wrapped for you
responsible_user_id string or array User UUID(s) β€” the API accepts both
estimated_closing_date string Exact closing date
estimated_closing_date_from string Closing date from (inclusive)
estimated_closing_date_until string Closing date until (inclusive)
updated_since string ISO 8601 datetime
created_before string ISO 8601 datetime

new is a deal status but not a filter value. The status field on a deal can be new, open, won or lost, but deals.list only accepts the last three as filter values. Filtering by ['new'] is not something the API supports.

There is no tags filter. See the note in the Overview.


Sorting

Field Description
created_at Deal creation date
weighted_value Probability Γ— estimated value
$deals = Teamleader::deals()->list([], [
    'sort'       => 'weighted_value',
    'sort_order' => 'desc',
]);

Several input forms are accepted β€” a field name, a list of field names, a single ['field' => ..., 'order' => ...] entry, or a list of those:

$deals = Teamleader::deals()->list([], [
    'sort' => [
        ['field' => 'weighted_value', 'order' => 'desc'],
        ['field' => 'created_at',     'order' => 'asc'],
    ],
]);

Any other field throws, rather than being silently coerced to a default.


Sideloading

Include Description
lead.customer Customer record (company or contact)
responsible_user Responsible user
department Assigned department
current_phase Current pipeline phase
source Deal source
custom_fields Custom field values

Unlike Companies, the same includes apply to both list() and info().

See Sideloading for general patterns.


Response Structure

list() response

[
    'data' => [
        [
            'id'                     => 'deal-uuid',
            'title'                  => 'New Business Deal',
            'status'                 => 'open',
            'reference'              => 'D-2025-001',
            'lead'                   => [
                'customer'       => ['type' => 'company', 'id' => 'company-uuid'],
                'contact_person' => null,
            ],
            'estimated_value'        => ['amount' => 10000.0, 'currency' => 'EUR'],
            'estimated_probability'  => 0.75,
            'estimated_closing_date' => '2025-12-31',
            'weighted_value'         => ['amount' => 7500.0, 'currency' => 'EUR'],
            'current_phase'          => ['type' => 'dealPhase', 'id' => 'phase-uuid'],
            'responsible_user'       => ['type' => 'user', 'id' => 'user-uuid'],
            'created_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 Deals does not. There is no total count, so the end of a list is a page shorter than the requested page size.


Usage Examples

Track a deal through the pipeline

$deal = Teamleader::deals()->create([
    'title' => 'Cloud Migration Project',
    'lead'  => ['customer' => ['type' => 'company', 'id' => 'company-uuid']],
    'phase_id'              => 'qualification-phase-uuid',
    'estimated_value'       => ['amount' => 25000, 'currency' => 'EUR'],
    'estimated_probability' => 0.3,
]);

// Move forward
Teamleader::deals()->move($deal['data']['id'], 'proposal-phase-uuid');
Teamleader::deals()->update($deal['data']['id'], ['estimated_probability' => 0.65]);
Teamleader::deals()->move($deal['data']['id'], 'closing-phase-uuid');
Teamleader::deals()->win($deal['data']['id']);

Mark as lost with reason

$reasons = Teamleader::lostReasons()->list();
$priceReason = collect($reasons['data'])->firstWhere('name', 'Price');

Teamleader::deals()->lose(
    'deal-uuid',
    $priceReason['id'],
    'Budget was 30% below our minimum'
);

Get deals closing this quarter

$deals = Teamleader::deals()
    ->withCustomer()
    ->closingBetween('2025-07-01', '2025-09-30', ['status' => ['open']]);

Read custom fields on a deal

$deal = Teamleader::deals()
    ->withCustomFields()
    ->info('deal-uuid');

foreach ($deal['data']['custom_fields'] ?? [] as $field) {
    $definitionId = $field['definition']['id'];
    $value        = $field['value'];
}

Deal custom field definitions come back from the API with context: sale, which the SDK normalises to deal. See Custom Fields.

Page through every open deal

$all  = [];
$page = 1;

do {
    $response = Teamleader::deals()->list(
        ['status' => ['open']],
        ['page_size' => 100, 'page_number' => $page]
    );
    $all = array_merge($all, $response['data']);
    $page++;
} while (count($response['data']) === 100);

Error Handling

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

// Unsupported filter key β€” thrown before the request
try {
    Teamleader::deals()->list(['tags' => ['VIP']]);
} catch (InvalidArgumentException $e) {
    // 'Unsupported filter key for deals.list: tags. Supported: ids, term,
    //  customer, phase_id, estimated_closing_date, estimated_closing_date_from,
    //  estimated_closing_date_until, responsible_user_id, updated_since,
    //  created_before, status, pipeline_ids.'
}

// Unsupported sort field
try {
    Teamleader::deals()->list([], ['sort' => 'title']);
} catch (InvalidArgumentException $e) {
    // 'Invalid sort field: title. deals.list accepts: created_at, weighted_value.'
}

// Invalid customer type on create
try {
    Teamleader::deals()->create([
        'title' => 'Test',
        'lead'  => ['customer' => ['type' => 'lead', 'id' => 'uuid']],
    ]);
} catch (InvalidArgumentException $e) {
    // "Invalid customer type: lead. Must be 'contact' or 'company'"
}

// move() with empty phase
try {
    Teamleader::deals()->move('deal-uuid', '');
} catch (InvalidArgumentException $e) {
    // 'Phase ID is required to move a deal'
}

// Invalid probability
try {
    Teamleader::deals()->create([/* ... */ 'estimated_probability' => 1.5]);
} catch (InvalidArgumentException $e) {
    // 'Estimated probability must be a number between 0 and 1 (inclusive)'
}

Related Resources

  • Quotations β€” Quotations belong to deals
  • Orders β€” Orders created from accepted quotations
  • Deal Phases β€” Phases deals move through
  • Deal Pipelines β€” Pipelines containing phases
  • Deal Sources β€” Source reference list
  • Lost Reasons β€” Lost reason reference list
  • Companies β€” Companies as deal customers, and where tagging does exist
  • Contacts β€” Contacts as deal customers
  • Custom Fields β€” Definitions for the custom_fields sideload
  • Files β€” Use files()->forDeal() to list attachments
  • Sideloading β€” Loading related data
  • Filtering β€” Filter and pagination reference

Clone this wiki locally