Skip to content

Webhooks ​

Webhooks send HTTP POST requests to your URL when task events occur. Use them to integrate with n8n, Home Assistant, Node-RED, or any service that accepts webhooks.

Creating a Webhook ​

bash
curl -X POST https://tasks.example.com/api/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-service.com/webhook",
    "events": ["task.created", "task.completed"]
  }'

The response includes a secret field — save it immediately. The secret is shown once and cannot be retrieved later.

Events ​

EventFires whenPayload
task.createdTask is created{ task: { ... } }
task.updatedTask fields change{ task: { ... }, fields_changed: [...] }
task.completedTask is marked done{ task: { ... } }
task.deletedTask is trashed{ task_id, title }
task.snoozedTask is snoozed{ task: { ... }, previous_due_at }
task.skippedAn occurrence is skipped (POST /api/tasks/{id}/skip-occurrence){ task: { ... } }
task.progressedProgress is logged on a quota (a positive or negative delta, or "did it" on a quota reminder){ task: { ... }, progress_current, progress_target, met }

A skip is not a completion, and neither is logging quota progress: neither sends task.completed. met is true when the count is at or past the target. When a quota's period ends, the server records the period (a met period counts as a completion in history) and resets the count, but it sends no webhook for this.

Payload Format ​

json
{
  "event": "task.completed",
  "timestamp": "2026-02-27T15:00:00.000Z",
  "data": {
    "task": {
      "id": 42,
      "title": "Call dentist",
      "done": true,
      "priority": 2,
      "due_at": "2026-02-27T14:00:00Z",
      "labels": ["medical"],
      "project_id": 1,
      "is_recurring": false
    }
  }
}

Headers ​

HeaderValue
Content-Typeapplication/json
X-OpenTask-EventEvent name (e.g., task.completed)
X-OpenTask-Signaturesha256=<hex> HMAC-SHA256 of body

Verifying Signatures ​

Every delivery is signed with your webhook's secret using HMAC-SHA256:

javascript
// Node.js
const crypto = require('crypto')

function verifySignature(body, signature, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

const signature = req.headers['x-opentask-signature']
const isValid = verifySignature(rawBody, signature, YOUR_WEBHOOK_SECRET)
python
# Python
import hmac, hashlib

def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

Managing Webhooks ​

bash
# List webhooks
curl https://tasks.example.com/api/webhooks \
  -H "Authorization: Bearer $TOKEN"

# Disable (without deleting)
curl -X PATCH https://tasks.example.com/api/webhooks/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'

# Check recent deliveries
curl https://tasks.example.com/api/webhooks/1/deliveries \
  -H "Authorization: Bearer $TOKEN"

# Delete
curl -X DELETE https://tasks.example.com/api/webhooks/1 \
  -H "Authorization: Bearer $TOKEN"

Delivery Behavior ​

  • Timeout: 10 seconds per request
  • Retries: Up to 3 attempts on failure (1s and 5s delays)
  • Success: Any 2xx status code
  • Logging: All deliveries logged, visible via the deliveries endpoint
  • Retention: Delivery logs purged after 7 days
  • Non-blocking: Webhook delivery never blocks task operations