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

Todoist Laravel Package

socialiteproviders/todoist

View on GitHub
Deep Wiki
Context7

Getting Started

socialiteproviders/todoist is an OAuth 2 driver for Laravel Socialite. It does one job: it lets you complete the Todoist authorization code flow and hands you back the user identity plus an access token you then use against Todoist's own API. It is not a Todoist client — plan on pairing it with Http::withToken(...) or a Todoist SDK.

1. Install

composer require socialiteproviders/todoist

This pulls in socialiteproviders/manager (auto-discovered) and the Socialite version the release targets. Note that 4.3.0 shipped with only a changelog link, so treat it as a routine minor: read the 4.2.0...4.3.0 compare on GitHub before upgrading production, and pin ^4.3 once you've confirmed nothing in the diff touches the provider class.

2. Create the app in Todoist

Go to the Todoist App Management Console, create an app, and copy the Client ID and Client Secret. The Redirect URI must match exactly — scheme, host, path, and trailing slash all count.

3. Config

// config/services.php
'todoist' => [
    'client_id'     => env('TODOIST_CLIENT_ID'),
    'client_secret' => env('TODOIST_CLIENT_SECRET'),
    'redirect'      => env('TODOIST_REDIRECT_URI'),
],

4. Register the listener

This is the step everyone forgets. The driver only exists once the manager event fires.

// Laravel 10 and below: app/Providers/EventServiceProvider.php
protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        \SocialiteProviders\Todoist\TodoistExtendSocialite::class.'@handle',
    ],
];
// Laravel 11/12: app/Providers/AppServiceProvider.php boot()
use Illuminate\Support\Facades\Event;

Event::listen(
    \SocialiteProviders\Manager\SocialiteWasCalled::class,
    \SocialiteProviders\Todoist\TodoistExtendSocialite::class.'@handle'
);

5. First use case — connect a Todoist account

The realistic first feature is "connect my Todoist account" for an already-authenticated user, so your app can read or write their tasks later. That flow is a two-route pair:

Route::get('/connect/todoist', fn () => Socialite::driver('todoist')
    ->scopes(['data:read_write'])
    ->redirect());

Route::get('/connect/todoist/callback', function () {
    $todoist = Socialite::driver('todoist')->user();

    auth()->user()->forceFill([
        'todoist_user_id' => $todoist->getId(),
        'todoist_token'   => $todoist->token, // add an encrypted cast
        'todoist_scopes'  => request('scope'),
    ])->save();

    return redirect()->route('dashboard');
});

Where to look first: dump Socialite::driver('todoist')->redirect()->getTargetUrl() and confirm the client_id, redirect_uri, scope, and state params look right before you write any persistence code. Almost every initial failure is visible in that URL.

Implementation Patterns

Token storage is the real design decision. Todoist access tokens are long-lived and there is no refresh token. Do not model them like typical OAuth2 tokens with expiry/refresh columns you'll never populate. Store access_token, the granted scope string, and todoist_user_id; add a revoked_at or simply delete the row when a call returns 401. Use an encrypted cast on the token column — it's a bearer credential with full account access at data:read_write.

Multi-account / per-user linking. Since Todoist has no password login of its own, most apps use this as an integration link, not a login provider. Keep a todoist_accounts table keyed by (user_id, todoist_user_id) so a single user can connect more than one Todoist account, and so re-running the OAuth flow updates rather than duplicates.

Calling Todoist afterwards. The provider stops at the user object. Everything else is yours:

$response = Http::withToken($user->todoist_token)
    ->acceptJson()
    ->get('https://api.todoist.com/api/v1/tasks');

if ($response->status() === 401) {
    // token revoked or scope missing — force re-consent
}

Wrap this in a small TodoistClient service resolved from the container so tests can swap it out and so retry/backoff lives in one place.

Scope selection per feature. Ask for the narrowest scope that supports the feature: data:read for a read-only dashboard, data:read_write for task creation, data:delete / project:delete only where genuinely needed. Store what was actually granted (the callback receives scope) rather than what you asked for, and gate UI on the stored value.

Stateless flows. For a mobile app or SPA doing a PKCE-less redirect through a backend, use ->stateless() and carry your own signed state in the callback route. For server-rendered web apps, keep Socialite's session-based state check — it's the CSRF protection for this flow.

Re-authorization instead of refresh. Since tokens never expire, the only "refresh" is re-running the OAuth flow. Build a reconnect route that is just the /connect/todoist route again, and link to it from any 401 handling.

Testing. There's no built-in fake for this driver. Two workable approaches:

// A: mock the driver at the Socialite facade
Socialite::shouldReceive('driver->user')->andReturn(
    (new \Laravel\Socialite\Two\User)->map([
        'id' => '123', 'name' => 'Ada', 'email' => '[email protected]',
        'accessToken' => 'tok_abc',
    ])
);

// B: Http::fake() the token endpoint + user endpoint and exercise the real Provider
Http::fake([
    'todoist.com/oauth/access_token' => Http::response(['access_token' => 'tok_abc', 'token_type' => 'Bearer']),
    '*' => Http::response(['id' => '123', 'full_name' => 'Ada', 'email' => '[email protected]']),
]);

Approach B is worth the extra effort: it catches provider-level mapping changes when you bump from 4.2 to 4.3.

Gotchas and Tips

  • Driver [todoist] is not supported means the SocialiteWasCalled listener isn't registered — not that the package is broken. On Laravel 11+ this is the most common failure because EventServiceProvider is gone; register it in AppServiceProvider::boot().
  • Config key must be exactly todoist and must match the string you pass to Socialite::driver(). A typo like 'todoist_app' produces a null-config error far from the cause.
  • php artisan config:cache after changing services.php, or you'll debug a stale redirect URI for twenty minutes. Same for env changes in a container.
  • Redirect URI mismatch is rejected by Todoist at the authorize step and is not always a friendly error page. Copy it from the App Console rather than retyping, and remember staging/production usually need separate Todoist apps.
  • Scope separator. Socialite's default OAuth2 scope separator is a space; Todoist expects comma-separated scopes. If ->scopes(['data:read_write', 'data:delete']) appears to be ignored or the consent screen shows the wrong permissions, either pass a single scope or force the raw param: ->with(['scope' => 'data:read_write,data:delete']). Verify by inspecting the redirect URL — this is the single most likely silent misconfiguration with this driver.
  • No refreshToken, no expiresIn. Don't write logic that branches on them being present; on this provider they come back empty and any if ($user->refreshToken) guard will just be dead code. Handle expiry as "401 → re-consent."
  • Don't treat the Todoist identity as a login. If you use it to sign users in, you have no email-verification guarantee and no password fallback. Prefer linking to an existing authenticated user.
  • Rate limits are per-token and per-endpoint (Todoist's sync-style endpoints are stricter than simple REST reads). Since you'll be making calls on a user's behalf, queue bulk operations and add backoff on 429 rather than retrying inline in a request cycle.
  • Todoist's API surface has moved from the older REST v2 / Sync v9 hosts to the api/v1 endpoints. If you copied snippet code from a tutorial written before the migration, your first call after a successful OAuth handshake will 4xx — that's your integration code, not this package.
  • Extension point: if you need custom field mapping (e.g. to prefer full_name or a workspace context), subclass SocialiteProviders\Todoist\Provider, override mapUserToObject(), and register your own SocialiteWasCalled listener pointing at the subclass. Keeps you off the fork treadmill.
  • Pin the minor version. This provider's release notes are often just a compare link, so composer update can move you between behavior without a readable changelog. Use ^4.3 and review the GitHub compare on each bump; run your Http::fake-based driver test as the tripwire.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect