Skip to content

Webhooks

MC0RE edited this page Aug 18, 2026 · 3 revisions

Webhooks

Manage webhook registrations for real-time event notifications in Teamleader Focus.

Overview

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 and register() the new ones.

Webhook payload delivers subject.id, not data.id. The entity UUID is at payload['subject']['id'].

URL must be HTTPS β€” HTTP URLs throw InvalidArgumentException before the request.

webhooks.list takes 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 $eventTypes list before the request is sent.

Endpoint

webhooks

Capabilities

Capability Supported
Pagination ❌ Not supported
Filtering ❌ Not supported
Sorting ❌ Not supported
Sideloading ❌ Not supported
Creation βœ… Via register()
Update ❌ Unregister + re-register
Deletion βœ… Via unregister()

Methods

list()

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.


register(string $url, array $types)

Registers the URL for the given event types. Throws InvalidArgumentException if:

  • $url is empty, not a valid URL, or not HTTPS
  • $types is 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.


unregister(string $url, array $types)

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.


Helper Methods

All of these are local β€” they read the SDK's own event type list and make no API call.

getAvailableEventTypes()

Returns the full array of valid event type strings.

$allTypes = Teamleader::webhooks()->getAvailableEventTypes();

getEventTypesByCategory(string $category)

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.*.

Category shortcut helpers

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();

The two project event families

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.*.


Event Types

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 use created. invoice.drafted has no created counterpart. These come from Teamleader, not the SDK β€” build your handler on the exact strings rather than a pattern.


Webhook Payload

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'] β€” not payload['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 like timeTracking.added.

Laravel route example

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.


Usage Examples

Register for all Peppol events

Teamleader::webhooks()->register('https://myapp.com/webhooks/teamleader', [
    'invoice.peppolSubmissionSucceeded',
    'invoice.peppolSubmissionFailed',
    'creditNote.peppolSubmissionSucceeded',
    'creditNote.peppolSubmissionFailed',
]);

Register for multiple resource categories

Teamleader::webhooks()->register(
    'https://myapp.com/webhooks/teamleader',
    array_merge(
        Teamleader::webhooks()->getInvoiceEventTypes(),
        Teamleader::webhooks()->getDealEventTypes(),
        Teamleader::webhooks()->getContactEventTypes(),
    )
);

Reconcile registrations with config

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);
}

Error Handling

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. ...'
}

Related Resources

Clone this wiki locally