-
-
Notifications
You must be signed in to change notification settings - Fork 0
Invoices
Manage invoices in Teamleader Focus.
The Invoices resource covers the full invoice lifecycle: drafting, booking, crediting, sending (email and Peppol), payments, and downloads.
Access via Teamleader::invoices().
create()drafts an invoice (posts toinvoices.draft). Thedraft()helper lists draft invoices and is deprecated in favour oflistDrafts()β don't confuse the two.Untitled
grouped_linessections must omit thesectionkey entirely. This is the single most common cause of a400 grouped_lines must be validon this resource. See Grouped lines and untitled sections.
invoices
| Capability | Supported |
|---|---|
| Pagination | β Supported |
| Filtering | β Supported |
| Sorting | β
Supported (invoice_number, invoice_date) |
| Sideloading | β
Supported (late_fees) |
| Creation | β Supported (draft) |
| Update | β Supported |
| Deletion | β Supported |
section is optional on a grouped line. When a group has no title, the key must be omitted entirely β Teamleader rejects both of these with HTTP 400:
['section' => ['title' => null], 'line_items' => [...]] // 400
['section' => ['title' => ''], 'line_items' => [...]] // 400['line_items' => [...]] // β
correct
['section' => ['title' => 'Services'], 'line_items' => [...]] // β
correctThis bites hardest on read-modify-write. invoices.info returns section.title as null for untitled sections, so echoing back exactly what you just read is rejected. Drop the key instead:
$info = Teamleader::invoices()->info($invoiceId);
$groupedLines = [];
foreach ($info['data']['grouped_lines'] as $group) {
$out = ['line_items' => $lineItems];
if (! empty($group['section']['title'])) {
$out['section'] = ['title' => $group['section']['title']];
}
$groupedLines[] = $out;
}
Teamleader::invoices()->update($invoiceId, ['grouped_lines' => $groupedLines]);Untitled sections are common β Teamleader's own UI creates them whenever a user doesn't name a section.
Before v2.2.0 this was impossible through the SDK.
validateGroupedLines()required every group to carry asection.title, so omitting the key threw client-side and including it asnullwas rejected by the API. There was no accepted shape, which madecreate(),update()andupdateBooked()unreachable for any invoice with an untitled section.
Validation now applies these rules, on all four write paths (create(), update(), updateBooked(), creditPartially()):
| Shape | Result |
|---|---|
section omitted |
β Passes |
section.title a non-empty string |
β Passes |
section.title null
|
β Throws β omit the key |
section.title ''
|
β Throws β omit the key |
section present without title
|
β Throws |
section not an array |
β Throws |
line_items missing or not an array |
β Throws |
The SDK is stricter than the API on line items. It requires
unit_priceon every line item; the specification lists onlyquantity,descriptionandtax_rate_idas required. An invoice line without a price is almost always a mistake, so this is deliberate β but worth knowing if you hit it.
The status filter is validated against draft, outstanding, matched (a lone string is coerced to an array).
use McoreServices\TeamleaderSDK\Facades\Teamleader;
$invoices = Teamleader::invoices()->list(['status' => ['outstanding']]);
$invoices = Teamleader::invoices()->list(
['department_id' => 'dept-uuid'],
['sort' => 'invoice_date', 'sort_order' => 'desc', 'page_size' => 50]
);$invoice = Teamleader::invoices()->info('invoice-uuid');
$invoice = Teamleader::invoices()->info('invoice-uuid', 'late_fees');Drafts an invoice (invoices.draft). Required: invoicee (customer.type + customer.id), department_id, payment_term (type), grouped_lines.
Notable optional fields: currency, project_id, quotation_id, purchase_order_number, invoice_date, note, expected_payment_method, custom_fields, delivery_date.
quotation_id(v1.2.8): links the created invoice to a source quotation and its deal, and marks the deal as won.
$invoice = Teamleader::invoices()->create([
'department_id' => 'dept-uuid',
'invoicee' => ['customer' => ['type' => 'company', 'id' => 'company-uuid']],
'payment_term' => ['type' => 'after_invoice_date', 'days' => 30],
'grouped_lines' => [[
'section' => ['title' => 'Services'],
'line_items' => [[
'quantity' => 5, 'description' => 'Consulting',
'unit_price' => ['amount' => 150.0, 'tax' => 'excluding'],
'tax_rate_id' => 'tax-rate-uuid',
]],
]],
'quotation_id' => 'quotation-uuid', // links to quotation + deal, marks deal won
]);A draft without section titles β note the absent section key:
$invoice = Teamleader::invoices()->create([
'department_id' => 'dept-uuid',
'invoicee' => ['customer' => ['type' => 'company', 'id' => 'company-uuid']],
'payment_term' => ['type' => 'cash'],
'grouped_lines' => [[
'line_items' => [[
'quantity' => 1, 'description' => 'Callout fee',
'unit_price' => ['amount' => 75.0, 'tax' => 'excluding'],
'tax_rate_id' => 'tax-rate-uuid',
]],
]],
]);update() edits a draft; updateBooked() edits a booked invoice (only when enabled in Teamleader settings). Both validate expected_payment_method when present (see Expected Payment Method) and grouped_lines when present.
Teamleader::invoices()->update('invoice-uuid', ['note' => 'Updated note']);
Teamleader::invoices()->updateBooked('invoice-uuid', [
'expected_payment_method' => ['method' => 'sepa_direct_debit', 'reference' => 'MND-2026-000123'],
]);Teamleader::invoices()->book('invoice-uuid', '2025-04-01');
$draft = Teamleader::invoices()->copy('invoice-uuid');
Teamleader::invoices()->delete('invoice-uuid'); // draft or last booked only
credit(string $id, string $creditNoteDate) / creditPartially(string $id, string $creditNoteDate, array $groupedLines, ?array $discounts = null)
creditPartially() validates grouped_lines under the same rules as create() β including the untitled-section rule.
Teamleader::invoices()->credit('invoice-uuid', '2025-04-15');
Teamleader::invoices()->creditPartially('invoice-uuid', '2025-04-15', $groupedLines);Valid formats: pdf, ubl/e-fff, ubl/peppol_bis_3, ubl/xrechnung (the last added in v1.2.8). Returns a temporary download URL.
$download = Teamleader::invoices()->download('invoice-uuid', 'ubl/xrechnung');Sends an invoice by email. content requires subject and body (optional mail_template_id). recipients is optional (v1.2.8) β when omitted, the invoice is sent to the invoicee's email. Each recipient in to/cc/bcc must include an email.
Teamleader::invoices()->send('invoice-uuid', [
'subject' => 'Your invoice',
'body' => 'Please find your invoice attached.',
]);Teamleader::invoices()->sendViaPeppol('invoice-uuid');
registerPayment(string $id, array $payment, string $paidAt, ?string $paymentMethodId = null) / removePayments(string $id)
Teamleader::invoices()->registerPayment(
'invoice-uuid',
['amount' => 250.00, 'currency' => 'EUR'],
'2025-04-10',
'payment-method-uuid'
);
Teamleader::invoices()->removePayments('invoice-uuid');| Method | Filter applied |
|---|---|
listDrafts(array $filters = [], array $options = []) |
status: ['draft'] (replaces deprecated draft())
|
outstanding(...) |
status: ['outstanding'] |
matched(...) |
status: ['matched'] |
forCustomer(string $type, string $id, ...) |
customer |
forProject(string $projectId, ...) |
project_id |
forDeal(string $dealId, ...) |
deal_id |
forDepartment(string $departmentId, ...) |
department_id |
search(string $term, ...) |
term |
updatedSince(string $datetime, ...) |
updated_since |
$statuses = Teamleader::invoices()->getValidPeppolStatuses(); // possible peppol_status valuesexpected_payment_method is validated on create(), update() and updateBooked().
Valid method values: direct_debit, credit_card, cash, cheque, bankers_draft, bank_transfer, payment_card, sepa_direct_debit.
When
methodissepa_direct_debit,referenceis required.
// Valid
['method' => 'sepa_direct_debit', 'reference' => 'MND-2026-000123']
['method' => 'credit_card']
// Throws InvalidArgumentException β sepa_direct_debit without reference
['method' => 'sepa_direct_debit']Verified against @teamleader/focus-api-specification.
| Filter | Type | Description |
|---|---|---|
ids |
array | Invoice UUIDs |
term |
string | Invoice number, PO number, payment reference, invoicee |
invoice_number |
string | Full invoice number (fiscal year / number) |
department_id |
string | Department (company entity) |
deal_id |
string | Deal UUID |
project_id |
string | Project UUID |
subscription_id |
string | Subscription UUID |
status |
array |
draft, outstanding, matched (validated) |
updated_since |
string | ISO 8601 datetime |
purchase_order_number |
string | PO number |
payment_reference |
string | Structured payment reference |
invoice_date_after |
string | Date (inclusive, YYYY-MM-DD) |
invoice_date_before |
string | Date (inclusive, YYYY-MM-DD) |
customer |
object | Customer {type, id}
|
| Field | Description |
|---|---|
invoice_number |
Invoice number |
invoice_date |
Invoice date |
Default order is desc.
| Include | Description |
|---|---|
late_fees |
Adds totals.due_incasso_inclusive, totals.fixed_late_fee and totals.interest
|
Sent as includes (plural) in the request body.
-
list()/info()responses includepeppol_status(nullable) β seegetValidPeppolStatuses()for the possible values. -
grouped_lines[].section.titleis nullable on read. See Grouped lines and untitled sections before writing it back. - There is no
metablock. The API returns pagination metadata only when a resource sendsincludes=pagination, and Invoices does not β so there is no total count, and the end of a list is a page shorter than the requested page size.
The round trip that used to be impossible:
$info = Teamleader::invoices()->info('invoice-uuid');
$groupedLines = [];
foreach ($info['data']['grouped_lines'] as $group) {
$lineItems = [];
foreach ($group['line_items'] as $line) {
$lineItems[] = [
'quantity' => $line['quantity'],
'description' => $line['description'],
'unit_price' => ['amount' => $line['unit_price']['amount'], 'tax' => 'excluding'],
'tax_rate_id' => $line['tax']['id'],
];
}
// New line on the first group
if ($groupedLines === []) {
$lineItems[] = [
'quantity' => 1,
'description' => 'Additional callout',
'unit_price' => ['amount' => 75.0, 'tax' => 'excluding'],
'tax_rate_id' => 'tax-rate-uuid',
];
}
$out = ['line_items' => $lineItems];
// Only include section when it has a real title
if (! empty($group['section']['title'])) {
$out['section'] = ['title' => $group['section']['title']];
}
$groupedLines[] = $out;
}
Teamleader::invoices()->update('invoice-uuid', ['grouped_lines' => $groupedLines]);Teamleader::invoices()->book('invoice-uuid', now()->toDateString());
Teamleader::invoices()->send('invoice-uuid', [
'subject' => 'Invoice from MCore Services',
'body' => 'Please find your invoice attached.',
]);$all = [];
$page = 1;
do {
$response = Teamleader::invoices()->list(
['status' => ['outstanding']],
['page_size' => 100, 'page_number' => $page]
);
$all = array_merge($all, $response['data']);
$page++;
} while (count($response['data']) === 100);use InvalidArgumentException;
use McoreServices\TeamleaderSDK\Exceptions\TeamleaderException;
// Untitled section sent as null β omit the key instead
try {
Teamleader::invoices()->update('invoice-uuid', [
'grouped_lines' => [
['section' => ['title' => null], 'line_items' => [$lineItem]],
],
]);
} catch (InvalidArgumentException $e) {
// 'section.title must be a non-empty string. Omit the section key entirely
// for untitled groups β the API rejects both null and "".'
}
// section present but malformed
try {
Teamleader::invoices()->update('invoice-uuid', [
'grouped_lines' => [['section' => [], 'line_items' => [$lineItem]]],
]);
} catch (InvalidArgumentException $e) {
// 'When present, section must contain a title'
}
// Missing line items
try {
Teamleader::invoices()->update('invoice-uuid', [
'grouped_lines' => [['section' => ['title' => 'Services']]],
]);
} catch (InvalidArgumentException $e) {
// 'Each grouped line must have line_items array'
}
// sepa_direct_debit without a reference
try {
Teamleader::invoices()->update('invoice-uuid', [
'expected_payment_method' => ['method' => 'sepa_direct_debit'],
]);
} catch (InvalidArgumentException $e) {
// reference is required for sepa_direct_debit
}
// Server-side rejection, if a bad payload gets past client-side validation
try {
Teamleader::invoices()->update('invoice-uuid', ['grouped_lines' => $groupedLines]);
} catch (TeamleaderException $e) {
// {"errors":[{"code":0,"title":"grouped_lines must be valid","status":400, ...}]}
}-
Credit-Notes β Created via
credit()/creditPartially(); read-only otherwise -
Subscriptions β Can generate invoices;
grouped_linesthere is not section-validated -
Quotations β
quotation_idsource on draft - Deals β Linked deal
-
Payment Terms β
payment_term.typereference -
Tax Rates β
tax_rate_idon line items -
Files β Use
files()->forInvoice()to list attachments - Filtering β Filter and pagination reference
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