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

Files

Manage file uploads and downloads in Teamleader Focus.

Overview

The Files resource handles attaching files to Teamleader entities. Upload and download both work via temporary signed URLs β€” the SDK requests the URL from the API, then you transfer the file content to/from that URL yourself using any HTTP client.

Access via Teamleader::files().

files.list and files.upload accept different subject types. meeting, product and project can be listed but not uploaded to; temporary can be uploaded but not listed. See Valid Subject Types β€” there are two tables, not one.

The subject filter is required on list(). The API rejects a request without one, so the SDK throws before sending it. Use one of the for*() helpers if you don't want to build the filter by hand.

Both project and nextgenProject are valid list subject types. project is the legacy project system, nextgenProject the current one. forProject() maps to nextgenProject; forLegacyProject() maps to project. Only nextgenProject is valid for upload.

No update() method β€” files cannot be renamed or moved after upload.

temporary uploads do not require a subjectId β€” all other subject types throw without one.

Endpoint

files

Capabilities

Capability Supported
Pagination βœ… Supported
Filtering βœ… Supported (subject, required)
Sorting βœ… Supported (updated_at only)
Sideloading ❌ Not supported
Creation βœ… Supported (via upload())
Update ❌ Not supported
Deletion βœ… Supported

Methods

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

Lists files for a given subject. The subject filter is required β€” both type and id must be present, and type is validated against the list subject types.

use McoreServices\TeamleaderSDK\Facades\Teamleader;

$files = Teamleader::files()->list([
    'subject' => ['type' => 'company', 'id' => 'company-uuid'],
]);

$files = Teamleader::files()->list(
    ['subject' => ['type' => 'deal', 'id' => 'deal-uuid']],
    ['sort' => 'updated_at', 'sort_order' => 'desc', 'page_size' => 50]
);

Calling list() with no arguments throws β€” there is no "all files" view in the API.


info(string $id)

Throws if $id is empty.

$file = Teamleader::files()->info('file-uuid');

The response carries id, subject, name, content_type, size, uploaded_at and the uploader. Note that subject.type on a response can include values you cannot filter by β€” order and workOrder among them.


upload(string $name, string $subjectType, ?string $subjectId = null, ?string $folder = null)

Requests a signed upload URL from the API. Does not upload the file β€” you must then send the file content to the returned location URL yourself.

  • $name β€” filename with extension (e.g. contract.pdf)
  • $subjectType β€” validated against the upload subject types
  • $subjectId β€” required for all types except temporary
  • $folder β€” optional; defaults to the account's "General" folder

Returns data.location (upload URL) and data.expires_at. It does not return a file id β€” see Getting the file id below.

The upload is a POST with the raw binary body, not a PUT, and not multipart/form-data. The API description is explicit about this.

// Attach a file to a company
$upload = Teamleader::files()->upload('contract.pdf', 'company', 'company-uuid', 'Contracts');
$uploadUrl = $upload['data']['location'];

// Now POST the raw file content to that URL
$fileContent = file_get_contents('/path/to/contract.pdf');

$ch = curl_init($uploadUrl);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $fileContent,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/pdf',
        'Content-Length: '.strlen($fileContent),
    ],
]);

$response = curl_exec($ch);
curl_close($ch);

Temporary uploads

When $subjectType is temporary, no $subjectId is needed. The resulting file can be linked to another resource (e.g. file_id on a receipt or incoming invoice) before it expires.

$upload = Teamleader::files()->upload('receipt.jpg', 'temporary');
$uploadUrl = $upload['data']['location'];
// ... POST the file content to $uploadUrl ...

Temporary files expire after 24 hours if not linked to an entity. They do not appear in any file overview and are not included in external syncs.

Getting the file id

files.upload returns only location and expires_at. There is no id in that response, so $upload['data']['id'] is null β€” earlier versions of this page showed that, and it does not work.

The id comes back from the transfer itself: capture the response body of the POST to location rather than discarding it.

$upload = Teamleader::files()->upload('receipt.jpg', 'temporary');

$ch = curl_init($upload['data']['location']);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => file_get_contents('/local/receipt.jpg'),
    CURLOPT_RETURNTRANSFER => true,
]);

$body = curl_exec($ch);
curl_close($ch);

$fileId = json_decode($body, true)['data']['id'] ?? null;

download(string $id)

Requests a signed download URL. Does not download the file β€” you must then fetch the file content from the returned location URL yourself.

Returns data.location (download URL) and data.expires_at.

$download = Teamleader::files()->download('file-uuid');
$downloadUrl = $download['data']['location'];

$fileContent = file_get_contents($downloadUrl);
file_put_contents('/local/path/file.pdf', $fileContent);

delete(mixed $id)

Throws if $id is empty.

Teamleader::files()->delete('file-uuid');

Helper Methods

All helpers call forSubject() internally, which validates the type against the list subject types before building the request.

Method Subject type sent
forCompany(string $id) company
forContact(string $id) contact
forCreditNote(string $id) creditNote
forDeal(string $id) deal
forInvoice(string $id) invoice
forMeeting(string $id) meeting
forProduct(string $id) product
forProject(string $id) nextgenProject
forLegacyProject(string $id) project
forTicket(string $id) ticket
forSubject(string $type, string $id) any valid list type

All accept an optional $options array for pagination and sorting:

$files = Teamleader::files()->forCompany('company-uuid');
$files = Teamleader::files()->forDeal('deal-uuid', ['page_size' => 50, 'sort_order' => 'desc']);
$files = Teamleader::files()->forProduct('product-uuid');

forProduct() is the one to reach for when mirroring a product catalogue β€” Teamleader products carry technical data sheets, EPB documentation and installation instructions as attachments.


Valid Subject Types

The two endpoints do not accept the same set. Verified against @teamleader/focus-api-specification.

files.list β€” filtering

Type Notes
company
contact
creditNote
deal
invoice
meeting List only β€” cannot upload to a meeting
nextgenProject Current project system
product List only
project Legacy project system. List only
ticket

temporary is not valid here β€” a temporary file has no subject to filter on.

files.upload β€” attaching

Type Notes
company
contact
creditNote
deal
invoice
nextgenProject
ticket
temporary No subjectId required; expires in 24 h

meeting, product and project are not valid here.


Filters

Filter Type Description
subject object Required. {type: string, id: uuid} β€” both validated

Sorting

Field Description
updated_at Last modification date

Any other field throws. The API accepts exactly one sort field on this endpoint.

$files = Teamleader::files()->forCompany('company-uuid', [
    'sort'       => 'updated_at',
    'sort_order' => 'desc',
]);

The sort parameter is sent as an array of objects β€” [['field' => 'updated_at', 'order' => 'desc']]. Before v2.2.0 the SDK built a string array (['-updated_at']), which the API silently ignores, so sorting had no effect on this resource.


Usage Examples

Attach a contract to a deal

// 1. Request upload URL
$upload = Teamleader::files()->upload('contract.pdf', 'deal', 'deal-uuid', 'Contracts');

// 2. POST the raw file content to the signed URL
$content = file_get_contents('/local/contract.pdf');
$ch = curl_init($upload['data']['location']);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $content,
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// 3. List files on the deal to confirm
$files = Teamleader::files()->forDeal('deal-uuid');

Stage a receipt before creating an incoming invoice

// Upload temporarily
$upload = Teamleader::files()->upload('receipt.jpg', 'temporary');

// POST the content and keep the response β€” the file id comes from here
$ch = curl_init($upload['data']['location']);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => file_get_contents('/local/receipt.jpg'),
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
curl_close($ch);

$fileId = json_decode($body, true)['data']['id'] ?? null;

// Use the file id when creating the receipt
Teamleader::receipts()->add([
    'title'    => 'Team lunch',
    'currency' => ['code' => 'EUR'],
    'total'    => ['tax_inclusive' => ['amount' => 45.0]],
    'file_id'  => $fileId,
]);

Mirror a product's technical attachments

$files = Teamleader::files()->forProduct('product-uuid');

foreach ($files['data'] as $file) {
    $download = Teamleader::files()->download($file['id']);
    $content  = file_get_contents($download['data']['location']);
    file_put_contents("/datasheets/{$file['name']}", $content);
}

Download all files for a company

$files = Teamleader::files()->forCompany('company-uuid');

foreach ($files['data'] as $file) {
    $download = Teamleader::files()->download($file['id']);
    $content  = file_get_contents($download['data']['location']);
    file_put_contents("/exports/{$file['name']}", $content);
}

The API returns no pagination metadata, so a full enumeration means paging until a page comes back shorter than the requested page size. See Usage for the pattern.


Error Handling

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

// No subject filter at all
try {
    Teamleader::files()->list();
} catch (InvalidArgumentException $e) {
    // 'The subject filter is required for files.list. Pass
    //  ['subject' => ['type' => ..., 'id' => ...]], or use one of the
    //  forCompany(), forDeal(), forProduct() helpers.'
}

// Missing subject id on list
try {
    Teamleader::files()->list([
        'subject' => ['type' => 'company'],
    ]);
} catch (InvalidArgumentException $e) {
    // 'subject filter must contain both type and id'
}

// Subject type valid for list but not for upload
try {
    Teamleader::files()->upload('sheet.pdf', 'product', 'product-uuid');
} catch (InvalidArgumentException $e) {
    // 'Invalid subject type: product. files.upload accepts: company, contact,
    //  creditNote, deal, invoice, nextgenProject, temporary, ticket.'
}

// Subject type valid for upload but not for list
try {
    Teamleader::files()->list([
        'subject' => ['type' => 'temporary', 'id' => 'file-uuid'],
    ]);
} catch (InvalidArgumentException $e) {
    // 'Invalid subject type: temporary. files.list accepts: company, contact,
    //  creditNote, deal, invoice, meeting, nextgenProject, product, project, ticket.'
}

// Missing subjectId for non-temporary type
try {
    Teamleader::files()->upload('file.pdf', 'company');
} catch (InvalidArgumentException $e) {
    // 'Subject ID is required for subject type: company'
}

// Unsupported sort field
try {
    Teamleader::files()->forCompany('company-uuid', ['sort' => 'name']);
} catch (InvalidArgumentException $e) {
    // 'Invalid sort field: name. files.list accepts: updated_at.'
}

Related Resources

  • Companies β€” Files can be attached to companies
  • Contacts β€” Files can be attached to contacts
  • Deals β€” Files can be attached to deals
  • Invoices β€” Files can be attached to invoices
  • Products β€” Products carry technical data sheets as attachments (list only)
  • Projects β€” Use forProject() for the current project system
  • Legacy Projects β€” Use forLegacyProject() for accounts on the old system
  • Receipts β€” Receipts accept a file_id on create
  • Incoming Invoices β€” Incoming invoices accept a file_id on create

Clone this wiki locally