# Asana Webhooks

## When to Use This Skill

* How do I receive Asana webhooks?
* How do I implement the Asana `X-Hook-Secret` handshake?
* How do I verify Asana webhook signatures (`X-Hook-Signature`)?
* How do I handle task, project, or story events (`added`, `changed`, `removed`, `deleted`, `undeleted`)?
* Why is my Asana webhook signature verification failing?

## How Asana Webhooks Work

Asana webhooks have two phases that both POST to your `target` URL:

1. Handshake (once, at creation). When you call `POST /webhooks`, Asana sends a
  request carrying an `X-Hook-Secret` header and no `X-Hook-Signature`. Your
  endpoint must echo that same `X-Hook-Secret` back as a response header and
  return `200`. Store the secret — it is the key for verifying every future
  delivery. This secret is shown only during the handshake.
2. Event deliveries (ongoing). Every later request carries an
  `X-Hook-Signature` header — a hex HMAC-SHA256 of the raw request body,
  keyed with the stored secret. The body is a batch: `{"events": [...]}`.
  Heartbeats arrive as `{"events": []}`.

## Verification (core)

Distinguish the handshake from a normal delivery by which header is present, then
HMAC the raw body and compare timing-safe.

Node:

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

function verifyAsanaSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, 'hex'),
      Buffer.from(expected, 'hex')
    );
  } catch {
    return false; // wrong length / malformed hex
  }
}

// Handshake: echo X-Hook-Secret, store it, return 200.
// Delivery: verifyAsanaSignature(rawBody, req.headers['x-hook-signature'], storedSecret)

```

Python:

```python
import hmac, hashlib

def verify_asana_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

```

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

## Event Actions

Each event in the `events` array is compact — it names what changed, not the full
object. Fetch full details with a follow-up API call using the resource `gid`.

| Action | Triggered When |
| --- | --- |
| `added` | A resource is created or added to a parent (e.g. task added to a project) |
| `changed` | A field on a resource changes (e.g. task name, due date, completed) |
| `removed` | A resource is removed from a parent (still exists elsewhere) |
| `deleted` | A resource is deleted (trashed) |
| `undeleted` | A previously deleted resource is restored |

Event object fields: `action`, `resource` (`{ gid, resource_type }`), `parent`,
`user`, `created_at`, and (with filters) `change`.

> For the full event reference, see [Asana Webhooks Guide](https://developers.asana.com/docs/webhooks-guide).

## Important Headers

| Header | Direction | Description |
| --- | --- | --- |
| `X-Hook-Secret` | request → response | Sent by Asana during the handshake; echo it back and store it |
| `X-Hook-Signature` | request | Hex HMAC-SHA256 of the raw body on every event delivery |

## Environment Variables

```bash
# The X-Hook-Secret captured during the handshake for this webhook.
# In production, store one secret per webhook (keyed by webhook gid), not a single env var.
ASANA_WEBHOOK_SECRET=your_stored_x_hook_secret

# Optional: Personal Access Token used to create webhooks and fetch full resource details.
ASANA_ACCESS_TOKEN=your_personal_access_token

```

## Local Development

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

```

Create the webhook against the tunnel URL:

```bash
curl -X POST https://app.asana.com/api/1.0/webhooks \
  -H "Authorization: Bearer $ASANA_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"data": {"resource": "<PROJECT_GID>", "target": "https://<your-tunnel>/webhooks/asana"}}'

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/asana-webhooks/references/overview.md) - Asana webhook concepts, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/asana-webhooks/references/setup.md) - Creating webhooks via the API, the handshake
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/asana-webhooks/references/verification.md) - Signature verification details and gotchas