# How to Send a Todoist Task Completion Alert to a Shared Room

> Verify Todoist's completion webhook, filter one project, and send one fixed privacy-minimal alert to a shared PingRoom Room.

- Published: 2026-10-27
- Author: PingRoom
- Category: Developers
- Topics: Todoist task completed notification, Todoist webhook, Todoist push notification
- Canonical: https://pingroom.io/blog/todoist-task-completion-push-notifications
- Cover image: https://pingroom.io/blog/todoist-task-completion-push-notifications-cover.webp
- Cover description: Two PingRoom avatars move a plain task tile into a finished tray while Pingo watches beside a sealed alert tube.

**To send one Todoist task-completion alert to a shared group of phones, receive Todoist's `item:completed` webhook in a small server-side relay, verify the raw-body signature, filter one project, and post fixed copy to a PingRoom incoming webhook.** Keep the task name, description, assignee, labels, and Todoist user data out of the phone alert.

This is a developer webhook recipe, not a native Todoist integration. It fits one selected completion that matters to people outside the Todoist project. Todoist remains the task record.

## Start with Todoist's own notifications

Todoist already supports email, push, web, and desktop notifications. Its [notification guide](https://www.todoist.com/help/articles/manage-your-notifications-in-todoist-QxQGXkMu) says a creator or assigner can be notified when a shared-project task is completed.

Use that first when the right people already work in Todoist and need the task context there.

| Need                                                                   | Better starting point                                   |
| ---------------------------------------------------------------------- | ------------------------------------------------------- |
| Creator or assigner needs normal task context                          | Todoist notifications                                   |
| One selected completion should reach a separate shared attention group | Todoist webhook → trusted relay → PingRoom Room         |
| A no-code personal device alert is enough                              | An automation service such as IFTTT                     |
| Every completion must be accounted for                                 | Todoist plus an application-owned reconciliation record |

IFTTT publishes a [Todoist completed-task notification recipe](https://ifttt.com/applets/Tu3hm5gF-get-an-ifttt-notification-when-a-todoist-task-is-completed). Its trigger polls every hour on Free, every 15 minutes on Starter, and every five minutes on Pro or Pro+. That can be simpler than running a relay. The direct Todoist webhook route below is for a server-side shared-Room workflow with sender verification and fixed copy.

## Understand the Todoist webhook boundary

Todoist's [webhook reference](https://developer.todoist.com/api/v1/#tag/Webhooks) documents four details that shape this recipe:

- `item:completed` carries the completed task object.
- A webhook starts only after the app user completes Todoist's OAuth flow. Creating an app does not activate its owner's webhook by itself.
- `X-Todoist-Hmac-SHA256` is a base64 HMAC-SHA256 over the entire raw request body, keyed with the app's client secret.
- `X-Todoist-Delivery-ID` identifies one event. Todoist keeps that value when it retries a failed delivery after 15 minutes, up to three times.

The payload contains more task detail than a lock-screen message needs. A PingRoom incoming webhook also does not verify Todoist's signature. Put a relay between the services: verify first, select the event and project, discard the rest, then forward one bounded sentence.

Todoist warns that webhooks can be delayed, arrive out of order, or fail to arrive. Treat this as an attention path, not a complete audit feed.

## Create the destination Room

Create a private PingRoom Room for the people who need the completion signal. The Room owner needs **PingRoom Pro** for incoming webhooks.

In the PingRoom browser app, open **Room Settings → Connections → From another app**, add an incoming webhook, and store its complete URL as a server-side secret. The [incoming-webhook guide](/blog/webhook-to-push-notification) covers setup and recipient behavior; the [webhook reference](/documents/webhooks) carries the current request limits, idempotency rules, cooldown behavior, and errors.

Use fixed copy for the first version:

- Title: **Task completed**
- Message: **One selected Todoist task is complete. Open Todoist for details.**

That tells the Room what changed without placing the client, task, project, assignee, or deep link on a phone screen.

## Deploy a signature-verifying receiver

Store these three values in protected server-side configuration:

- `TODOIST_CLIENT_SECRET`: the secret for the Todoist app that owns the webhook
- `TODOIST_PROJECT_ID`: the one project allowed to create a shared alert
- `PINGROOM_WEBHOOK_URL`: the complete secret PingRoom URL

This is the core of the dependency-free Node receiver we tested:

```js
import crypto from "node:crypto";

export async function handleTodoistWebhook(request, env) {
  const raw = await request.text();
  const supplied = request.headers.get("x-todoist-hmac-sha256") ?? "";
  const expected = crypto
    .createHmac("sha256", env.TODOIST_CLIENT_SECRET)
    .update(raw)
    .digest("base64");

  if (!safeEqual(supplied, expected)) {
    return Response.json({ error: "invalid_signature" }, { status: 401 });
  }

  const deliveryId = request.headers.get("x-todoist-delivery-id");
  const payload = JSON.parse(raw);
  if (
    !deliveryId ||
    payload.event_name !== "item:completed" ||
    String(payload.event_data?.project_id ?? "") !== env.TODOIST_PROJECT_ID
  ) {
    return Response.json({ accepted: true, forwarded: false });
  }

  const eventKey = crypto
    .createHash("sha256")
    .update(`${deliveryId}\n${payload.event_data.id}`)
    .digest("hex");

  const response = await fetch(env.PINGROOM_WEBHOOK_URL, {
    method: "POST",
    redirect: "error",
    headers: {
      "Content-Type": "application/json",
      "Idempotency-Key": `todoist-${eventKey}`,
    },
    body: JSON.stringify({
      title: "Task completed",
      message:
        "One selected Todoist task is complete. Open Todoist for details.",
      correlation_id: `todoist:${eventKey}`,
    }),
  });

  if (!response.ok) {
    return Response.json({ error: "destination_rejected" }, { status: 502 });
  }
  return Response.json({ accepted: true, forwarded: true });
}

function safeEqual(left, right) {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

The recorded version also rejects oversized bodies, malformed JSON, missing task IDs, invalid delivery IDs, missing configuration, network failures, and redirects. It never logs the Todoist payload or a secret-bearing PingRoom response.

The idempotency key hashes Todoist's delivery ID with the task ID. Todoist reuses its delivery ID for a retry, so an exact redelivery produces the same PingRoom key and body. Completing the task again later is a new Todoist event with a new delivery ID.

## Register and activate the Todoist app

In Todoist's App Management Console, create or open an app, set the webhook callback to your deployed HTTPS receiver, and subscribe only to `item:completed`. Todoist requires HTTPS and does not allow a port in the callback URL.

Then complete the OAuth flow for the Todoist account whose events should activate the webhook. For an app used by other people, implement the normal authorization-code flow and show clear consent. For a personal server-side experiment, follow Todoist's documented personal-use OAuth activation steps. Do not put the client secret or access token in browser code, a repository, screenshots, or shared shell history.

Todoist says an app webhook begins receiving events for all app users who authorize it. The project filter in the receiver is therefore a required data-minimization boundary, not an optional convenience. If this app will serve more than one account, bind each authorized account to its allowed project and destination in server-side data instead of one global environment variable.

![Two PingRoom avatars watch one sealed source token pass through a narrow verification gate toward a plain Room token while Pingo stands beside the closed project box.](/blog/todoist-task-completion-push-notifications-1.webp)

_These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer. They are not Todoist or PingRoom screenshots and do not depict customers._

Pingo is inspecting the verification gate with the solemnity he usually reserves for crumbs that have failed a background check. He has no Todoist account, client secret, private-Room access, or authority to complete tasks or send Pings. People choose the project, recipients, copy, and fallback.

## Test one harmless completion

Use a test project and a generic task with no private content.

1. Authorize the Todoist app and confirm the callback is active.
2. Complete the test task and confirm exactly one **Task completed** Ping appears.
3. Replay the same signed request with the same delivery ID and confirm the same idempotency key and body are reused.
4. Change the project ID and confirm the relay returns success without calling PingRoom.
5. Send `item:updated` and confirm it is ignored.
6. Send a bad signature and confirm the relay returns `401` before parsing or forwarding.
7. Inspect the outgoing request and confirm it contains no task title, description, assignee, label, project name, user ID, or Todoist token.
8. Rotate the Todoist client secret and PingRoom webhook secret separately; confirm the retired values stop working.

For this article, representative signed events passed through the recorded receiver and a local mock of PingRoom's documented contract. The checks covered valid forwarding, invalid signatures, unrelated events, a different project, malformed JSON, exact replay, destination rejection, and privacy-minimal output. No live Todoist workspace, PingRoom Room, phone, or notification receipt was used.

## Keep the record and failure path honest

Return HTTP 200 only after the event has been sent or placed in a durable queue. A non-200 response asks Todoist to retry, but its retry limit is not a durable delivery guarantee. If every completion matters, reconcile against Todoist on a schedule and keep a separate application-owned record.

PingRoom's HTTP 200 means the event was accepted and entered its normal path. It does not prove that every Room member saw it. Connectivity, device notification permissions, Focus modes, operating-system behavior, and Room mute settings still apply.

For a large project, one alert per completed task becomes noise. Filter a genuinely meaningful project or milestone, queue bursts, and send a digest when several completions belong together. Keep safety-critical, regulated, or contractual work in its approved record and escalation system.

If one selected Todoist completion deserves a separate shared signal, [create the private Room and incoming webhook](/app/rooms), deploy the relay with generic test data, and keep all task details in Todoist.

_Todoist is a trademark of its owner. PingRoom is not affiliated with or endorsed by Todoist._
