-
-
Notifications
You must be signed in to change notification settings - Fork 0
Files
Manage file uploads and downloads in Teamleader Focus.
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.listandfiles.uploadaccept different subject types.meeting,productandprojectcan be listed but not uploaded to;temporarycan 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 thefor*()helpers if you don't want to build the filter by hand.Both
projectandnextgenProjectare valid list subject types.projectis the legacy project system,nextgenProjectthe current one.forProject()maps tonextgenProject;forLegacyProject()maps toproject. OnlynextgenProjectis valid for upload.No
update()method β files cannot be renamed or moved after upload.
temporaryuploads do not require asubjectIdβ all other subject types throw without one.
files
| 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 |
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.
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.
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 excepttemporary -
$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
POSTwith the raw binary body, not aPUT, and notmultipart/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);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.
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;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);Throws if $id is empty.
Teamleader::files()->delete('file-uuid');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.
The two endpoints do not accept the same set. Verified against @teamleader/focus-api-specification.
| 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.
| 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.
| Filter | Type | Description |
|---|---|---|
subject |
object |
Required. {type: string, id: uuid} β both validated |
| 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.
// 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');// 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,
]);$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);
}$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.
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.'
}- 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_idon create -
Incoming Invoices β Incoming invoices accept a
file_idon create
Last Updated: August 2026 β’ SDK Version: 2.2.2 β’ Made with β€οΈ by MCore Services
- Departments
- Users
- Teams
- Custom Fields
- Work Types
- Document Templates
- Currencies
- Notes
- Email Tracking
- Closing Days
- Day Off Types
- Days Off
- User Schedules
- Invoices
- Credit Notes
- Subscriptions
- Payment Methods
- Payment Terms
- Tax Rates
- Withholding Tax Rates
- Commercial Discounts
Next Gen Projects
Legacy Projects