Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Toggl Laravel Package

ixudra/toggl

ixudra/toggl is a Laravel package offering a fluent client for the Toggl API, so you can manage time entries, projects, clients, workspaces, and reports directly from your PHP application. Includes a service provider, facade, and config for quick integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal path from zero to a logged time entry:

  1. Install.

    composer require ixudra/toggl
    

    On Laravel 5.5+ the Ixudra\Toggl\TogglServiceProvider is auto-discovered. On older versions register it in config/app.php and add the alias:

    'Toggl' => Ixudra\Toggl\Facades\Toggl::class,
    
  2. Publish the config so you can point the client at the right API version and workspace:

    php artisan vendor:publish --provider="Ixudra\Toggl\TogglServiceProvider"
    

    Then set TOGGL_API_KEY (from the bottom of your Toggl profile page) and usually TOGGL_WORKSPACE_ID in .env. Never commit the token — use config('toggl.api_key') only on the server.

  3. Look first at config/toggl.php (api key, workspace id, api url) and the facade's method list. The facade proxies 1:1 onto Toggl REST endpoints, so the Toggl API docs are your real reference — the package method names track them closely.

  4. First useful call — sanity-check auth and discover your workspace:

    $me = Toggl::currentUser();          // confirms the token works
    $workspaces = Toggl::workspaces();   // grab the id you'll pass everywhere else
    
  5. First real use case — log a completed entry:

    Toggl::createTimeEntry([
        'wid'         => config('toggl.workspace_id'),
        'start'       => now()->toIso8601String(),
        'duration'    => 3600,
        'description' => 'Code review',
        'pid'         => $projectId,
    ]);
    
    // Or start/stop a running timer:
    Toggl::startTimeEntry(['wid' => $wid, 'description' => 'Working']);
    Toggl::stopTimeEntry($entryId);
    

Implementation Patterns

Wrap the facade in your own service. The facade is a thin transport. Give it a home so you can mock it in tests and centralise your wid/pid defaults:

class TogglService
{
    public function __construct(private int $workspaceId) {}

    public function log(string $description, int $seconds, ?int $projectId = null): array
    {
        return Toggl::createTimeEntry([
            'wid'         => $this->workspaceId,
            'start'       => now()->subSeconds($seconds)->toIso8601String(),
            'duration'    => $seconds,
            'description' => $description,
            'pid'         => $projectId,
        ]);
    }
}

Build a config-driven defaults layer. Nearly every endpoint needs a workspace id, and many need a project id. Keep a small resolver (ProjectResolver, WorkspaceResolver) that maps your app's own entities to Toggl ids, cached in Redis or a toggl_projects table. This turns "hardcoded magic ids" into one lookup point.

Sync rather than query-on-read. For anything dashboard- or reporting-oriented, run an artisan command (scheduled) that pulls Toggl::clients($wid), Toggl::projects($wid), and Toggl::timeEntries([...]) into local tables keyed by Toggl id. Your UI then reads from your own DB — far faster than hitting a rate-limited remote on every page load, and robust when Toggl is slow.

Model the start/stop lifecycle explicitly. Running entries have a negative duration (-1 * unix_start). If you mirror that state locally, store an is_running flag rather than trying to interpret the negative number in your views. Always call Toggl::stopTimeEntry($id) before starting a new one — Toggl allows only one running entry per user, and a silent conflict returns a bare 4xx rather than a helpful exception.

Payload arrays over builders. The package favours plain associative arrays for create/update payloads, which makes it trivial to build them from Eloquent models with $model->only([...]) and to test with array comparisons. Treat the expected shape as your contract and validate it in a FormRequest before it reaches the facade.

Reports are a different world. The Toggl Reports API lives on a different host, uses a different token-in-query auth scheme, and requires a user agent. The package's reporting coverage (if present) is limited — for summaries, either build your own Guzzle client against toggl.com/reports/api/v2, or aggregate from synced timeEntries locally. Don't assume Toggl::reports() mirrors the tracking endpoints.

Testing. Bind a fake to the container in tests rather than hitting the network:

$this->swap(\Ixudra\Toggl\TogglFactory::class, $mock);

or mock your own TogglService wrapper — the wrapper exists precisely so you never need to mock the facade.

Gotchas and Tips

  • API version drift. Toggl v8 and v9 differ in response shapes and some endpoints. Pin the base URL in config/toggl.php and re-check after any composer update. If calls mysteriously change shape, this is the first place to look.
  • The workspace id is mandatory on nearly every endpoint and is not inferred from your token. Forgetting it produces 4xx errors that look like auth failures but aren't.
  • Timezone handling is on you. Toggl stores UTC but reports/exports use the workspace timezone. start values you send are interpreted in your account's timezone setting, not necessarily UTC. Be explicit in your payloads and consistent in your sync logic or your daily totals will drift by an hour at DST boundaries.
  • No pagination helpers. timeEntries() accepts a since/date range and returns what the API returns. For long histories, page manually with successive start_date/end_date windows — don't assume one call returns everything.
  • Rate limits are real. Toggl limits to roughly one request per second per token. Bulk-syncing hundreds of projects or entries will get you throttled or temporarily blocked. Add usleep(1_000_000) (or a queued job with a rate limiter) in any loop that iterates remote calls.
  • Error handling is thin. The client typically returns decoded arrays or throws on HTTP failure; there's no rich domain exception layer. Wrap calls at your boundary and translate non-2xx into your own exceptions — don't let a raw Guzzle exception bubble into a user-facing controller.
  • The token is effectively a password. It grants full account access. Rotate it if leaked, never log request payloads containing it, and remember that Toggl tokens are per-user — if you're building a multi-user integration, each user supplies their own token, so the shared config('toggl.api_key') singleton is only suitable for a single-account setup. For multi-tenant, resolve the client per-user rather than relying on the facade default.
  • Facade is a static singleton under the hood. That makes it convenient but stateful if you try to swap credentials at runtime. Prefer instantiating the underlying client (Ixudra\Toggl\TogglFactory) for multi-tenant work instead of mutating config mid-request.
  • Deletion and updates are soft on some endpoints. Deleting a client may fail or cascade unexpectedly if it still has projects; the API will refuse rather than cascade cleanly. Handle deletion ordering yourself (time entries → projects → clients).
  • Facade alias collision. Toggl is a common name — if you have your own class with that name, alias the facade explicitly ('TogglApi' => ...) to avoid silent resolution to your own class in some Laravel versions.
  • Extension point: the package is a thin wrapper. If an endpoint isn't exposed, you can either subclass the client or, more simply, drop to Guzzle for that one call while keeping the package for everything else. Don't fork the library over a missing method.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource