Agent skill

Todoist Webhooks Skill

Receive and verify Todoist webhooks. Use when setting up Todoist webhook handlers, debugging X-Todoist-Hmac-SHA256 signature verification, or handling events like item:added, item:completed, note:added, or project:added.

Install this skill

npx skills add hookdeck/webhook-skills --skill todoist-webhooks


When to Use This Skill

  • How do I receive Todoist webhooks?
  • How do I verify the X-Todoist-Hmac-SHA256 signature?
  • Why is my Todoist webhook signature verification failing?
  • How do I handle item:added, item:completed, note:added or project:added events?
  • How do I deduplicate retried Todoist deliveries with X-Todoist-Delivery-ID?

Verification (core)

Todoist signs the whole raw request body with HMAC-SHA256 and sends the digest as base64 in X-Todoist-Hmac-SHA256. The key is your app's OAuth client_secret (App Management Console) - there is no separate webhook secret. There is no timestamp in the signed content. Todoist has no official webhook verification helper, so verify manually against the raw bytes, before parsing JSON.

Node:

const crypto = require('crypto');

function verify(rawBody, signatureHeader, clientSecret) {
  if (!signatureHeader) return false;
  const expected = crypto.createHmac('sha256', clientSecret).update(rawBody).digest('base64');
  try {
    return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
  } catch {
    return false; // different lengths
  }
}

Python:

import base64, hashlib, hmac

def verify(raw_body: bytes, signature_header: str, client_secret: str) -> bool:
    if not signature_header:
        return False
    expected = base64.b64encode(
        hmac.new(client_secret.encode(), raw_body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(signature_header.encode(), expected.encode())

Tip (Express): read the body with express.raw({ type: '*/*' }) so verification works whatever Content-Type is sent.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload and Idempotency

Body: { event_name, user_id, event_data, initiator, version, triggered_at } (plus optional event_data_extra). Each notification has a unique X-Todoist-Delivery-ID header; failed deliveries are retried (up to 3 times, after 15 minutes) with the same ID, so use it to deduplicate. Respond with HTTP 200 - any other status counts as a failure. Deliveries can arrive late or out of order.

Common Event Types

EventDescription
item:addedA task was added
item:updatedA task was updated
item:completedA task was completed
item:uncompletedA task was uncompleted
item:deletedA task was deleted
note:addedA comment was added
project:addedA project was added
project:archivedA project was archived
section:addedA section was added
label:addedA label was added
filter:addedA filter was added
reminder:firedA reminder has fired

For the full list (including note:updated, project:updated, section:*, label:*, filter:*), see Todoist Webhooks.

Environment Variables

TODOIST_CLIENT_SECRET=your_client_secret   # App Management Console -> your app -> Client Secret

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 todoist --path /webhooks/todoist

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →