-
-
Notifications
You must be signed in to change notification settings - Fork 0
Webhooks
Manage webhook registrations for real-time event notifications in Teamleader Focus.
Webhooks push event notifications to your HTTPS endpoint when something changes in Teamleader. You register a URL + list of event types; Teamleader sends a POST to your URL each time a matching event fires.
Access via Teamleader::webhooks().
No update method. To change which events a URL subscribes to,
unregister()the old types andregister()the new ones.Webhook payload delivers
subject.id, notdata.id. The entity UUID is atpayload['subject']['id'].URL must be HTTPS β HTTP URLs throw
InvalidArgumentExceptionbefore the request.
webhooks.listtakes no request body. Passing filters, sorting or pagination throws since v2.2.0; before that the arguments were silently discarded.All event types are validated against the SDK's
$eventTypeslist before the request is sent.
webhooks
| Capability | Supported |
|---|---|
| Pagination | β Not supported |
| Filtering | β Not supported |
| Sorting | β Not supported |
| Sideloading | β Not supported |
| Creation | β
Via register()
|
| Update | β Unregister + re-register |
| Deletion | β
Via unregister()
|
Returns all registered webhooks ordered by URL. Takes no arguments.
use McoreServices\TeamleaderSDK\Facades\Teamleader;
$webhooks = Teamleader::webhooks()->list();
foreach ($webhooks['data'] as $webhook) {
echo "{$webhook['url']} β " . implode(', ', $webhook['types']) . "\n";
}Any argument throws:
Teamleader::webhooks()->list([], ['page_size' => 10]);
// InvalidArgumentException: webhooks.list does not support pagination. ...There is no info() endpoint β list() returns everything.
Registers the URL for the given event types. Throws InvalidArgumentException if:
-
$urlis empty, not a valid URL, or not HTTPS -
$typesis empty - Any type string is not in the known event type list
Teamleader::webhooks()->register(
'https://myapp.com/webhooks/teamleader',
['invoice.booked', 'invoice.paymentRegistered', 'deal.won']
);
// Register all invoice-related events in one call
$types = Teamleader::webhooks()->getInvoiceEventTypes();
Teamleader::webhooks()->register('https://myapp.com/webhooks/teamleader', $types);The API answers HTTP 204. The SDK converts that into:
['success' => true, 'status_code' => 204, 'message' => 'Operation completed successfully', 'headers' => [...]]There is no data key on a 204 response β check success rather than looking for an id.
Removes the given event types from the URL. Same validation as register(). To fully remove a webhook, pass all its currently subscribed types.
// Remove specific types
Teamleader::webhooks()->unregister(
'https://myapp.com/webhooks/teamleader',
['invoice.booked']
);
// Remove all types (effectively deletes the webhook)
$webhooks = Teamleader::webhooks()->list();
foreach ($webhooks['data'] as $webhook) {
if ($webhook['url'] === 'https://myapp.com/webhooks/teamleader') {
Teamleader::webhooks()->unregister($webhook['url'], $webhook['types']);
break;
}
}Also returns the 204 success array described above.
All of these are local β they read the SDK's own event type list and make no API call.
Returns the full array of valid event type strings.
$allTypes = Teamleader::webhooks()->getAvailableEventTypes();Returns all types whose prefix matches $category.
$receiptTypes = Teamleader::webhooks()->getEventTypesByCategory('receipt');
// ['receipt.added', 'receipt.approved', 'receipt.bookkeepingSubmissionFailed', ...]Matching is on the exact prefix before the dot, so getEventTypesByCategory('project') returns only project.* β not nextgenProject.*.
| Method | Returns types for |
|---|---|
getInvoiceEventTypes() |
invoice.* + incomingInvoice.*
|
getCreditNoteEventTypes() |
creditNote.* + incomingCreditNote.*
|
getDealEventTypes() |
deal.* |
getContactEventTypes() |
contact.* |
getCompanyEventTypes() |
company.* |
getProjectEventTypes() |
project.* + nextgenProject.*
|
getTaskEventTypes() |
task.* + nextgenTask.*
|
getTicketEventTypes() |
ticket.* + ticketMessage.*
|
getTimeTrackingEventTypes() |
timeTracking.* |
$invoiceTypes = Teamleader::webhooks()->getInvoiceEventTypes();
$projectTypes = Teamleader::webhooks()->getProjectEventTypes();
$timeTrackTypes = Teamleader::webhooks()->getTimeTrackingEventTypes();project.* and nextgenProject.* are different systems, not old and new names for the same events:
| Events | System | SDK resource |
|---|---|---|
project.created, project.updated, project.deleted
|
Legacy projects | Teamleader::legacyProjects() |
nextgenProject.created, nextgenProject.updated, nextgenProject.closed, nextgenProject.deleted
|
Current ("nextgen") projects | Teamleader::projects() |
Note that the naming runs opposite to the SDK method names β projects() is the resource that fires nextgenProject.* events. See Projects and Legacy Projects.
getProjectEventTypes() returns both families, which is usually what you want: an account only fires one set, so subscribing to both means your integration keeps working through a migration. If you need to know which system an account is on, ask directly rather than inferring it from which events arrive:
Teamleader::accounts()->getProjectsVersion(); // 'projects-v2' or 'legacy'The same split applies to task.* versus nextgenTask.*.
95 event types, verified complete against @teamleader/focus-api-specification v1.197.0 β exact match in both directions.
| Category | Events |
|---|---|
| account |
deactivated, deleted
|
| call |
added, completed, deleted, updated
|
| company |
added, deleted, updated
|
| contact |
added, deleted, linkedToCompany, unlinkedFromCompany, updatedLinkToCompany, updated
|
| creditNote |
booked, deleted, peppolSubmissionFailed, peppolSubmissionSucceeded, sent, updated
|
| deal |
created, deleted, lost, moved, updated, won
|
| incomingCreditNote |
added, approved, bookkeepingSubmissionFailed, bookkeepingSubmissionSucceeded, deleted, refused, updated
|
| incomingInvoice |
added, approved, bookkeepingSubmissionFailed, bookkeepingSubmissionSucceeded, deleted, refused, updated
|
| invoice |
booked, deleted, drafted, paymentRegistered, paymentRemoved, peppolSubmissionFailed, peppolSubmissionSucceeded, sent, updated
|
| meeting |
completed, created, deleted, updated
|
| milestone |
created, updated
|
| nextgenProject |
closed, created, deleted, updated
|
| nextgenTask |
completed, created, deleted, updated
|
| product |
added, deleted, updated
|
| project |
created, deleted, updated
|
| receipt |
added, approved, bookkeepingSubmissionFailed, bookkeepingSubmissionSucceeded, deleted, refused, updated
|
| subscription |
added, deactivated, deleted, updated
|
| task |
completed, created, deleted, updated
|
| ticket |
closed, created, deleted, reopened, updated
|
| ticketMessage | added |
| timeTracking |
added, deleted, updated
|
| user | deactivated |
Note the inconsistent verbs: CRM entities use
added, deals and projects usecreated.invoice.draftedhas nocreatedcounterpart. These come from Teamleader, not the SDK β build your handler on the exact strings rather than a pattern.
When an event fires, Teamleader POSTs JSON to your endpoint:
{
"type": "invoice.booked",
"subject": {
"type": "invoice",
"id": "invoice-uuid"
},
"account": {
"type": "account",
"id": "account-uuid"
}
}
payload['subject']['id']β notpayload['data']['id'].The payload carries the entity id only, not the entity itself. Your handler will need a follow-up
info()call to get the record, which counts against your rate limit budget β worth remembering if you subscribe to a high-volume event liketimeTracking.added.
Route::post('/webhooks/teamleader', function (Request $request) {
$type = $request->input('type');
$id = $request->input('subject.id'); // entity UUID
match ($type) {
'invoice.booked' => handleInvoiceBooked($id),
'invoice.peppolSubmissionFailed' => handlePeppolFailure($id),
'deal.won' => handleDealWon($id),
default => null,
};
return response()->json(['status' => 'received']);
});Queue the work rather than doing it inline. If your handler makes SDK calls and the event is high-volume, a synchronous handler can hit the rate limiter and time out β and Teamleader will retry, compounding the problem.
Teamleader::webhooks()->register('https://myapp.com/webhooks/teamleader', [
'invoice.peppolSubmissionSucceeded',
'invoice.peppolSubmissionFailed',
'creditNote.peppolSubmissionSucceeded',
'creditNote.peppolSubmissionFailed',
]);Teamleader::webhooks()->register(
'https://myapp.com/webhooks/teamleader',
array_merge(
Teamleader::webhooks()->getInvoiceEventTypes(),
Teamleader::webhooks()->getDealEventTypes(),
Teamleader::webhooks()->getContactEventTypes(),
)
);Since there is no update method, syncing means diffing:
$url = config('app.url').'/webhooks/teamleader';
$wanted = config('teamleader.webhook_events', ['invoice.booked', 'deal.won']);
$current = [];
foreach (Teamleader::webhooks()->list()['data'] as $webhook) {
if ($webhook['url'] === $url) {
$current = $webhook['types'];
break;
}
}
$toAdd = array_values(array_diff($wanted, $current));
$toRemove = array_values(array_diff($current, $wanted));
if ($toAdd !== []) {
Teamleader::webhooks()->register($url, $toAdd);
}
if ($toRemove !== []) {
Teamleader::webhooks()->unregister($url, $toRemove);
}use InvalidArgumentException;
use McoreServices\TeamleaderSDK\Exceptions\TeamleaderException;
// Non-HTTPS URL
try {
Teamleader::webhooks()->register('http://myapp.com/webhooks', ['invoice.booked']);
} catch (InvalidArgumentException $e) {
// 'Webhook URL must use HTTPS protocol'
}
// Invalid event type
try {
Teamleader::webhooks()->register('https://myapp.com/webhooks', ['invoice.created']); // doesn't exist
} catch (InvalidArgumentException $e) {
// 'Invalid event type: invoice.created. Use getAvailableEventTypes() to see all valid types.'
}
// Empty types array
try {
Teamleader::webhooks()->register('https://myapp.com/webhooks', []);
} catch (InvalidArgumentException $e) {
// 'At least one event type is required'
}
// Arguments the list endpoint cannot honour
try {
Teamleader::webhooks()->list([], ['page_size' => 10]);
} catch (InvalidArgumentException $e) {
// 'webhooks.list does not support pagination. ...'
}-
Invoices β
invoice.*events -
Deals β
deal.*events -
Contacts β
contact.*events -
Companies β
company.*events -
Projects β fires
nextgenProject.*, despite the method name -
Legacy Projects β fires
project.* -
Accounts β
getProjectsVersion()tells you which project system an account uses -
Subscriptions β
subscription.*events -
Receipts β
receipt.*events -
Incoming Invoices β
incomingInvoice.*events -
Incoming Credit Notes β
incomingCreditNote.*events
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