Webhooks

Configure webhooks in TaskView to receive real-time HTTP notifications when tasks are created, updated, deleted, or reassigned. Includes HMAC-SHA256 signature verification, automatic retries, and delivery history.

Webhooks let you receive HTTP POST requests when events happen in your projects. Use them to integrate TaskView with external systems - CI/CD pipelines, Slack bots, custom dashboards, or any service that can accept HTTP requests.

Supported events

EventWhen it fires
task.createdA new task is created in the project
task.updatedA task is updated (description, status, priority, deadline, etc.)
task.deletedA task is deleted
task.movedA task is moved to another project. Sent to the webhooks of both projects
task.assigneesChangedTask assignees are added or removed
time-entry.startedA timer is started on a task
time-entry.stoppedA running timer is stopped
time-entry.createdA time entry is created manually
time-entry.updatedA time entry is updated
time-entry.deletedA time entry is deleted
sprint.createdA new sprint is created (draft or planned)
sprint.updatedSprint fields are edited (name, dates, capacity, goal)
sprint.activatedA sprint transitions to the active state
sprint.reviewStartedAn active sprint enters review
sprint.completedA sprint is closed and marked completed
sprint.pausedAn active sprint is paused
sprint.resumedA paused sprint is resumed
sprint.deletedA sprint is deleted (any status, including completed)
task.assignedToSprintA task is added to, moved between, or removed from a sprint
file.uploadedA file is uploaded to the project or straight into a task
file.renamedA file is renamed
file.deletedA file is deleted forever
file.attachedAn existing project file is attached to a task
file.detachedA file is removed from a task (the file itself stays in the project)

Setup

  1. Open a project in TaskView
  2. Right-click the project in the sidebar → "Webhooks"
  3. Click "Add Webhook"
  4. Enter the URL where you want to receive events
  5. Select which events to subscribe to
  6. Click "Add"
  7. Copy the secret and store it securely - it will not be shown again

Permissions

Two project permissions, in the Webhooks group of the role editor, decide who can work with webhooks:

PermissionAllows
webhooks_can_viewSee the project's webhooks with their URLs and events, and the delivery history including payloads
webhooks_can_manageCreate, edit, enable/disable, delete and test webhooks, rotate their secrets and retry deliveries. It does not include viewing: without webhooks_can_view the webhooks list and delivery history stay hidden

The two permissions are independent: to see and change webhooks a member needs both. The project owner has both. Any other member gets them only through a role: neither permission is part of the default roles, so add them to a role in the role editor. A webhook sends every subscribed event of the project to its URL, so grant webhooks_can_manage only to people you trust with all project data, and webhooks_can_view only to people who may see the URLs - many receivers treat the URL itself as a secret.

API tokens and OAuth apps are narrowed as usual: a token restricted to some projects or permissions can only reach the webhooks those allow.

Allowed destinations

A webhook URL must use http or https and point to a public address. URLs that resolve to loopback (127.0.0.1, localhost), private (10/8, 172.16/12, 192.168/16), link-local or cloud-metadata ranges are rejected when you save the webhook, and checked again before every delivery. Redirects are not followed: if your endpoint answers with a 3xx, the delivery is recorded as failed with that status code.

On a self-hosted instance where your receivers (n8n, CI runners, internal bots) live on the same private network, set WEBHOOKS_ALLOW_PRIVATE_URLS=true in the API environment. See environment variables for details.

Payload format

Every webhook delivery is an HTTP POST with Content-Type: application/json:

{
  "event": "task.updated",
  "timestamp": "2026-03-22T12:00:00.000Z",
  "task": {
    "id": 123,
    "goalId": 774,
    "description": "Fix login bug",
    "complete": false,
    "statusId": 5,
    "priorityId": 2,
    "tags": [1, 3],
    "assignedUsers": [10, 22],
    "subtasks": []
  },
  "changes": {
    "statusId": 5
  },
  "initiatorId": 1
}

The changes field is only present on task.updated and sprint.updated events and contains only the fields that changed.

Sprint events carry the sprint payload. Lifecycle events (sprint.activated, sprint.reviewStarted, sprint.completed, sprint.paused, sprint.resumed) include the full sprint object reflecting its state at the time the event fired, plus sprintId, goalId, and initiatorId:

{
  "event": "sprint.completed",
  "timestamp": "2026-03-22T12:00:00.000Z",
  "sprintId": 42,
  "goalId": 774,
  "initiatorId": 1,
  "sprint": {
    "id": 42,
    "goalId": 774,
    "name": "Sprint 7",
    "status": "completed",
    "startDate": "2026-03-08",
    "endDate": "2026-03-22",
    "goalAchieved": true,
    "completedAt": "2026-03-22T12:00:00.000Z"
  }
}

task.assignedToSprint carries taskId, sprintId (the new sprint, or null when removed), prevSprintId, goalId, and initiatorId. sprint.deleted carries sprintId, goalId, and initiatorId only, since the sprint no longer exists. initiatorId may be null for sprints generated automatically by a cadence.

task.moved carries task (the task after the move), taskIds (the moved task and its subtasks), fromGoalId, toGoalId and initiatorId. When the user may not delete tasks in the source project, the task is copied instead and the target project receives task.created for every copied task.

File events share one payload shape:

{
  "event": "file.attached",
  "timestamp": "2026-10-05T12:00:00.000Z",
  "goalId": 1,
  "taskIds": [42],
  "initiatorId": 7,
  "file": {
    "id": "6f1c2a8e-3b1d-4c9e-9a0b-2d5e7f8a1b3c",
    "goalId": 1,
    "name": "report.pdf",
    "originalName": "report.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 248512,
    "uploaderId": 7,
    "uploaderEmail": "[email protected]",
    "createdAt": "2026-10-05T11:58:00.000Z",
    "linkedTaskIds": [17, 42]
  }
}

taskIds lists the tasks the event concerns: every task the file is attached to for file.uploaded, file.renamed and file.deleted, the single task for file.attached and file.detached. file.linkedTaskIds is the state after the change; for file.deleted it is the state just before deletion. Payloads carry metadata only - no download link, so the file content never leaves TaskView through a webhook. Fetch the content through the API with a token that has the file_can_view permission.

Signature verification

Every request includes an X-Webhook-Signature header with an HMAC-SHA256 signature of the request body:

X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592...

Always verify the signature before processing the payload. Example in Node.js:

const crypto = require('crypto')

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

// In your HTTP handler:
const body = req.body // raw string, not parsed JSON
const signature = req.headers['x-webhook-signature']
const isValid = verifySignature(body, signature, YOUR_SECRET)
Never process webhook payloads without verifying the signature. Without verification, anyone who knows your webhook URL can send fake events.

Retries

If your server responds with a non-2xx status code or doesn't respond within 10 seconds, TaskView retries the delivery:

AttemptDelay
1st retry~10 seconds
2nd retry~20 seconds

After 3 total attempts (1 original + 2 retries), the delivery is marked as failed.

Auto-deactivation

If a webhook accumulates 10 consecutive failed deliveries (after all retries are exhausted), it is automatically deactivated. A single successful delivery resets the failure counter.

To reactivate a webhook, toggle it back on from the webhooks page. The failure counter is not reset automatically - the next successful delivery will reset it.

Delivery history

The webhooks page shows delivery history for each webhook:

  • Event - which event was delivered
  • Status - success, failed, or pending
  • HTTP code - response status code from your server
  • Attempts - how many attempts were made
  • Payload - click to view the full JSON payload

Failed deliveries can be retried manually from the delivery history.

Managing webhooks

From the webhooks page you can:

  • Toggle webhooks on/off
  • Edit the URL and subscribed events
  • Test - sends a test payload to verify connectivity
  • Rotate secret - generates a new secret (the old one stops working immediately)
  • Delete - removes the webhook and all delivery history
  • View deliveries - see delivery history with status filter

Secret rotation

If your secret is compromised, rotate it:

  1. Click the key icon on the webhook
  2. Confirm that you want to rotate
  3. Copy the new secret
  4. Update the secret in your receiving application

The old secret stops working immediately. Any in-flight deliveries signed with the old secret will fail signature verification on your end.

Testing locally

You can use a simple Node.js script to test webhook deliveries:

const http = require('http')
const crypto = require('crypto')

const PORT = 4545
const SECRET = 'your-secret-here'

const server = http.createServer((req, res) => {
  const chunks = []
  req.on('data', (chunk) => chunks.push(chunk))
  req.on('end', () => {
    const body = Buffer.concat(chunks).toString()
    const signature = req.headers['x-webhook-signature'] || ''
    const expected = 'sha256=' + crypto
      .createHmac('sha256', SECRET)
      .update(body)
      .digest('hex')

    console.log(signature === expected ? 'Valid' : 'INVALID')
    console.log(JSON.stringify(JSON.parse(body), null, 2))

    res.writeHead(200)
    res.end('OK')
  })
})

server.listen(PORT, () => console.log(`Listening on :${PORT}`))

Run with node webhook-receiver.js and set the webhook URL to http://localhost:4545. Loopback addresses are refused by default, so for this local test start the API with WEBHOOKS_ALLOW_PRIVATE_URLS=true, or expose the receiver through a tunnel (ngrok, Cloudflare Tunnel) and use its public URL.