# Teamwork Webhooks

Covers **Teamwork.com Projects** webhooks, **Version 2** only. Teamwork Desk and Teamwork Spaces webhooks are documented separately and are not covered here.

## When to Use This Skill

* How do I receive Teamwork Projects webhooks?
* How do I verify the Teamwork `X-Projects-Signature` header?
* How do I handle `TASK.CREATED`, `TASK.COMPLETED`, or `PROJECT.CREATED` events?
* Why is my Teamwork webhook signature verification failing?
* Why does my Teamwork webhook have no signature header?

## Verification (core)

When a webhook has a **token**, Teamwork computes an HMAC-SHA256 of the **raw POST body** keyed with that token (as UTF-8 bytes) and sends it **hex-encoded** in the `X-Projects-Signature` header (no prefix, no timestamp). The token is optional in Teamwork, so you must choose **Version 2** and set one, or no signature is sent.

Node (check the header is exactly 64 hex chars before `Buffer.from(x, 'hex')`, because invalid hex silently truncates):

```javascript
const crypto = require('crypto');

function verifyTeamworkWebhook(rawBody, signature, token) {
  if (!signature || !token || !/^[0-9a-f]{64}$/i.test(signature)) return false;
  const expected = crypto.createHmac('sha256', token).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}

```

Python (`compare_digest` on bytes, so a non-ASCII header gives 401, not 500):

```python
import hmac, hashlib

def verify_teamwork_webhook(raw_body: bytes, signature: str | None, token: str | None) -> bool:
    if not signature or not token:
        return False
    expected = hmac.new(token.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/teamwork-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/teamwork-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/teamwork-webhooks/examples/fastapi/).

Teamwork has no official SDK helper for webhook verification, so all three examples verify manually. Express examples read the body with `express.raw({ type: '*/*' })` so any content type verifies.

## Common Event Types

The event name arrives in the `X-Projects-Event` header in uppercase `ENTITY.ACTION` form. The body has no event field, only an `eventCreator` object and the entity (for example `task`, `project`, `comment`).

| Event | Triggered When |
| --- | --- |
| `TASK.CREATED` | A task is created |
| `TASK.UPDATED` | A task is updated |
| `TASK.COMPLETED` | A task is completed |
| `TASK.DELETED` | A task is deleted |
| `PROJECT.CREATED` | A project is created |
| `COMMENT.CREATED` | A comment is added |
| `MILESTONE.COMPLETED` | A milestone is completed |
| `TIME.CREATED` | Time is logged |

All delete events share one payload: `{ "id", "projectId", "eventCreator" }`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/teamwork-webhooks/references/overview.md) for the full entity/action list.

## Important Headers and Fields

| Item | Description |
| --- | --- |
| `X-Projects-Signature` header | Hex HMAC-SHA256 of the raw body, keyed with the webhook token |
| `X-Projects-Event` header | Event name, e.g. `TASK.CREATED` |
| `eventCreator` (body) | `id`, `firstName`, `lastName`, `avatar` of the user who triggered the event |

Respond `200` within 5 seconds. Any other status (including 3xx) counts as a failure: Teamwork retries hourly, up to 3 attempts, and deactivates the webhook after three discarded events. Queue work and acknowledge immediately.

## Environment Variables

```bash
TEAMWORK_WEBHOOK_TOKEN=your_webhook_token   # the token set on the Version 2 webhook

```

## Local Development

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

```

Use the Hookdeck URL as the webhook URL in Teamwork.

### Hookdeck setup

Only Teamwork.com Projects webhooks are supported. Use **Version 2** webhooks and **set a token**; Teamwork signs with it. Enter the same token as the signing secret on the Hookdeck source. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/teamwork-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/teamwork-webhooks/references/overview.md) - Teamwork webhook concepts, event list, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/teamwork-webhooks/references/setup.md) - Create a Version 2 webhook with a token, Hookdeck setup
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/teamwork-webhooks/references/verification.md) - Signature verification details and gotchas