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

laravel-notification-channels/todoist

Laravel notification channel for Todoist, letting you send notifications that create tasks in a Todoist project. Configure the channel and route notifications to your users so alerts, reminders, and app events become actionable Todoist items.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this before you type composer require. The package is archived, its last release was August 2016, and it has zero dependents. That is not automatically disqualifying — the integration surface is one method and one HTTP call — but it changes the setup sequence. You are not installing a dependency, you are adopting a small piece of abandoned source code that you will own.

Step 0 — fork and read it first. The whole package is likely four or five files: a service provider, a TodoistChannel, a TodoistMessage value object, and a config file. Clone it, read the channel class end to end, and find the base URI it posts to. In 2016 that was Todoist's v1 REST/Sync endpoint, which is retired. Budget an hour to confirm what it actually calls before you plan anything else; if the endpoint is dead, you are fixing it on day one, and you should know that before the first standup, not after.

Step 1 — install from your fork, not from Packagist.

composer require laravel-notification-channels/todoist --no-update
# then point the repository at your fork
composer config repositories.todoist vcs [email protected]:yourorg/todoist.git
composer require yourorg/todoist:dev-fix/api-v2

If Composer refuses to solve, that is the real signal. Check the constraint first:

composer why-not laravel-notification-channels/todoist

A 2016-era package will almost certainly pin guzzlehttp/guzzle ^6.0 and illuminate/support ~5.x, either of which deadlocks against a modern lockfile. Relaxing them is usually a one-line edit in your fork — but it means the package is now yours to maintain, so make that a conscious decision with a named owner.

Step 2 — token and config. Add a token to config/services.php (this family of packages reads services.<name>.token by convention — verify against your fork's provider):

'todoist' => [
    'token' => env('TODOIST_TOKEN'),
],

Use a dedicated bot account's API token, not a personal one. Todoist tokens are account-scoped with full read/write across every project; you do not want an app-owned token living in a .env that can also delete someone's personal tasks.

Step 3 — first use case: failed jobs. The highest-value, lowest-risk first integration is operational, not customer-facing — a failed queue job becomes a task in a "Ops" project. It fires rarely, the failure mode is a lost notification rather than a lost order, and it proves the token, endpoint, and project ID all work. Do that before you touch anything a customer can see.

Then the notification itself, in the standard Laravel shape:

class JobFailed extends Notification implements ShouldQueue
{
    public function via($notifiable): array
    {
        return [TodoistChannel::class];
    }

    public function toTodoist($notifiable)
    {
        return TodoistMessage::create()
            ->project(config('services.todoist.ops_project_id'))
            ->priority(4)
            ->content('Job failed: '.$this->job->getName());
    }
}

Where to look first, in order: the channel class (what it posts and where) → the message class (which builder methods exist and what the payload looks like) → the service provider (is it auto-discovered, is the config published) → composer.json (constraints, PSR-4, extra.laravel.providers).

Implementation Patterns

Treat the package as an adapter, not an API. The package gives you exactly one method name — toTodoist() — and everything else is an implementation detail you may end up replacing. So do not let TodoistMessage::create()->project()->label()->due() spread across forty notification classes. Wrap it:

namespace App\Notifications\Concerns;

trait CreatesTodoistTasks
{
    protected function todoistTask(string $content): TodoistMessage
    {
        return TodoistMessage::create()
            ->project(config('services.todoist.inbox_project'))
            ->content($content);
    }
}

Now every notification depends on your trait, and the day you replace the archived package with a hand-rolled channel or a hosted integration, you edit one file. This is the single most valuable pattern in this whole assessment — the package's integration surface is shallow precisely because there is one seam, so keep it at one seam.

Routing patterns. Two options, and the choice matters more than it looks.

  • via() returns TodoistChannel::class and the message builder supplies the project. Simple, but hardcodes the destination into the notification class.
  • Implement RouteNotificationForTodoist() on the notifiable, or resolve the project from config. Better when the same notification fires for multiple tenants/projects.

Because the channel is a stateless outbound HTTP call, there is no per-user routing concept here — tasks land where the message says, not where the notifiable lives. Do not build a mental model where $notifiable implies a destination.

Queue everything. The channel is a synchronous HTTP request inside send(). A slow or rate-limited Todoist API call will block whatever dispatched the notification — including a web request. implements ShouldQueue on the notification class is not optional here; it is the difference between a 200ms page load and a 10s one. If you cannot queue (no worker running), you cannot safely use this package in a request path.

Fan-out for triage. The genuinely good use of a Todoist channel is turning events into a human work queue. Patterns that earn their keep:

  • Per-project triage. Support form submissions → a "Support" Todoist project; sales inquiries → "Leads". One notification class per event, project resolved from config.
  • Escalation with due dates. Overdue invoices → task with a due date, so it surfaces in someone's Today view rather than dying in an inbox.
  • Priority as a routing signal. Failures that need same-day attention get high priority; the ops team filters on it. Note that Todoist's priority scale is inverted relative to intuition — see Gotchas.
  • Labels for cross-cutting state. blocked, needs-customer-reply. Labels must already exist in the account; the API creates tasks referencing label names or IDs depending on version, and it will silently drop unknown ones in some versions.

Testing. Laravel's Notification::fake() intercepts before the channel runs, so it will assert your notification would have sent without ever exercising the HTTP call:

Notification::fake();

Notification::route('todoist', 'inbox')->notify(new JobFailed($job));

Notification::assertSentOnDemand(JobFailed::class, function ($n, $channels, $notifiable) {
    return in_array(TodoistChannel::class, $channels);
});

That tests your wiring, not the package's HTTP layer. Because the package is unmaintained, also write one integration test that hits the real channel against Todoist's API in a sandbox project — that is the only thing that will tell you when the endpoint drifts or the token expires. Put it behind a CI flag so it does not run on every push.

Feature-flag the whole thing. A TODOIST_ENABLED config key checked at the top of your trait, returning a no-op message or a fallback channel. When the API breaks at 2am, you want to flip a config value and have notifications degrade to Slack or a log line, not deploy a hotfix.

Gotchas and Tips

The API version is the whole ballgame. This package was written against Todoist's 2016 REST/Sync API. Todoist has since moved to v2 (2021) and then renamed surfaces again. The old endpoints are sunset — expect 404, 410, or a redirect that Guzzle follows into a malformed response. Before you write any notification code, confirm the base URI in the fork's channel class against Todoist's current docs and repoint it. If the payload shape changed too (task content field, due-date object, priority encoding), you are rewriting the message serialization, not just an endpoint — at which point a ~40-line hand-written channel using Laravel's Http facade is genuinely the cheaper option, and it is what I would default to.

Composer constraints will fight you before the code does. Guzzle ^6 and illuminate/* ~5.x are the likely pins. On Laravel 10/11 with Guzzle 7, the solver will refuse. This is a five-minute fix in a fork and a permanent maintenance obligation — record it.

The package may predate auto-discovery. Laravel 5.4 introduced extra.laravel.providers. A 2016 package sometimes has it, sometimes not. If the channel class is never resolved, check composer.json for extra.laravel.providers and add the service provider to config/app.php manually. Symptom: BindingResolutionException or a channel that silently no-ops.

Silent failure is the default mode. Notification channels that catch exceptions and log them will make the integration look alive when it is dead. The task simply never appears and nobody notices for a week. Mitigations: log the HTTP status and response body on non-2xx (patch the fork), send one smoke-test task per deploy in staging, and never place this channel as the sole recipient of anything important — always pair it with a log or a Slack fallback.

Priority is inverted. Todoist's API priority 4 is the highest (UI "p1"), and 1 is the lowest (UI "p4"). The number you pass is the API number, which reads backwards from the UI badge. If your ops team says "make it p1" and you pass 1, you have created the quietest possible task. Verify the mapping against your pinned API version — this is a classic silent-wrong-value bug that only surfaces weeks later when someone asks why nothing urgent is ever in the todo list.

Projects and labels must exist, and IDs beat names. Resolving a project by name is fragile (renames, duplicates, shared projects). Store project IDs in config, look them up once, and fail loudly in tests if a configured ID is missing. Deleting a project in the Todoist UI does not error your notification — it just starts dropping tasks.

Rate limits are account-wide. Todoist's documented limits are on the order of a few hundred requests per 15 minutes per user token. A retry storm, a notification loop, or a batch job that fires per-row will burn that fast and then get your token throttled for all your integrations. Guard against accidental fan-out: never send a Todoist notification inside a loop over a large collection, and consider a small token-bucket before the channel if your volume is nontrivial.

Queue retries without idempotency create duplicate tasks. ShouldQueue with retries means a timeout where the task was created but the response was lost will produce a second task on retry. Todoist has no natural idempotency key you can set. Either accept duplicates (fine for ops alerts) or include a request ID in the content and dedupe upstream.

Debugging workflow. In order: (1) Notification::fake() to confirm the channel is in via() and the message builds; (2) php artisan tinker and call the channel directly against a scratch project — this bypasses all your notification plumbing and tests the package in isolation; (3) Http::fake() is not going to help if the package uses its own Guzzle client, so log at the Guzzle handler level or patch the fork to Log::debug() the request/response; (4) only then look at your notification class. Ninety percent of "the task didn't appear" issues are token scope, project ID, or a silently swallowed non-2xx — not your code.

Extension points are, effectively, the fork. There is no plugin surface, no events, no driver abstraction. If you need due-date timezones, sections, assignees, or subtasks, you edit the message class. Keep your diff small and rebased on master so future-you can tell what you changed.

Plan the exit before you need it. The honest recommendation for a 3-star archived package: write your own channel. It is roughly this much code —

class TodoistChannel
{
    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toTodoist($notifiable);

        Http::withToken(config('services.todoist.token'))
            ->post('https://api.todoist.com/rest/v2/tasks', $message->toArray())
            ->throw();
    }
}

— and it removes the fork, the Guzzle constraint fight, the dead endpoint, and the abandonment risk in one commit. If your only goal is turning events into tasks, the package's value over that snippet is close to zero, and its maintenance cost is nonzero. Adopt it only if you have already read the source and confirmed it works as-is; otherwise the snippet is the better day-to-day decision.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package