Gareth Wilson Gareth Wilson

Guide to Asana Webhooks: Features and Best Practices

Published


Asana webhooks notify your application when work changes: a task is updated, a comment (story) is added, a task is completed. If you're syncing Asana with another system or triggering automation off project activity, webhooks are how you react without polling the API.

This guide covers how Asana webhooks work, the X-Hook-Secret handshake that establishes the shared secret, how to verify the X-Hook-Signature, the compact event batches and heartbeats, and the best practices for running them in production.

What are Asana webhooks?

An Asana webhook watches a resource (a task, project, or higher-level object) and POSTs a batch of events to your target URL when that resource or its children change. The events are compact, telling you what kind of change happened to which resource, so you follow up with an API read for the full details.

Asana establishes a per-webhook shared secret through a handshake at creation, then signs every delivery with an X-Hook-Signature derived from that secret.

Asana webhook features

FeatureDetails
ConfigurationPOST /webhooks with resource + target
HandshakeEcho the X-Hook-Secret header at creation
Signature headerX-Hook-Signature (HMAC-SHA256 hex of the raw body)
PayloadBatch: {"events": [...]}, compact event objects
FiltersRequired for workspace/team/portfolio/goal resources
HeartbeatsEmpty {"events": []} at handshake and every 8 hours
Delivery~1 min average (up to ~10 min); >10s or non-2xx is a failed delivery
Retry / deletionExponential backoff for 24h, then the webhook is deleted
Explicit deleteRespond 410 to have Asana delete the webhook
Limits1,000 webhooks per resource, 10,000 per token

Common events

Asana events describe an action on a resource. The action values are added, changed, removed, deleted, and undeleted, applied to resource types like tasks and stories:

Example eventFires when
changed on a taskA task's field (name, assignee, due date, completion) changes
added on a storyA comment or system story is added to a task
added on a task (in a project)A task is added to a watched project
deleted on a taskA task is deleted

Because events are compact, fetch the full object from the API when you need its current state. For workspace, team, portfolio, and goal resources, filters are required at creation.

Setting up Asana webhooks

The X-Hook-Secret handshake

When you create a webhook (POST /webhooks with a resource and target), Asana immediately POSTs to your target URL with an X-Hook-Secret header and no events. Your server must echo that same X-Hook-Secret back as a response header and return 200, while the create request is still in flight. Store the secret per webhook; you'll use it to verify every subsequent delivery.

const secrets = new Map(); // webhook target -> X-Hook-Secret (persist this)

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  // Handshake: first delivery carries X-Hook-Secret and no body events
  const handshakeSecret = req.headers["x-hook-secret"];
  if (handshakeSecret) {
    secrets.set("asana", handshakeSecret); // store durably in production
    res.setHeader("X-Hook-Secret", handshakeSecret); // echo it back
    return res.sendStatus(200);
  }

  // ... otherwise verify X-Hook-Signature (below)
});

Securing Asana webhooks

After the handshake, every delivery carries an X-Hook-Signature header: a hex-encoded HMAC-SHA256 of the raw request body, keyed with the stored X-Hook-Secret. Recompute it over the exact bytes and compare in constant time.

const crypto = require("crypto");

function verify(rawBody, signature) {
  const secret = secrets.get("asana");
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const handshakeSecret = req.headers["x-hook-secret"];
  if (handshakeSecret) {
    secrets.set("asana", handshakeSecret);
    res.setHeader("X-Hook-Secret", handshakeSecret);
    return res.sendStatus(200);
  }

  if (!verify(req.body, req.headers["x-hook-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  const { events } = JSON.parse(req.body);
  for (const event of events) processQueue.add(event); // async
});

The same verification in Python:

import hashlib
import hmac


def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

Heartbeats (empty {"events": []} deliveries) also carry a signature, so verify them the same way; just don't treat the empty batch as work.

Asana webhook limitations and pain points

The handshake must complete in-flight

The Problem: The X-Hook-Secret echo has to happen synchronously while the POST /webhooks create request is still open. If your endpoint is slow to respond, or handles the handshake asynchronously, webhook creation fails.

Why It Happens: Asana confirms endpoint ownership and the shared secret before it finishes creating the webhook.

Workarounds:

  • Detect the X-Hook-Secret header, echo it, and return 200 immediately, before any other logic.
  • Persist the secret durably (not just in memory) so restarts don't lose it.

How Hookdeck Can Help: Hookdeck can complete the Asana handshake and verify signatures at the edge, so your application receives pre-verified events and never has to race the in-flight create request.

Missed heartbeats delete the webhook

The Problem: Asana sends a heartbeat at handshake and every 8 hours. If no delivery succeeds for 24 hours, the webhook is deleted, and a slow endpoint (over 10 seconds) or non-2xx responses count as failures that lead there.

Why It Happens: Asana prunes webhooks whose endpoints look dead.

Workarounds:

  • Acknowledge every delivery, including heartbeats, with a fast 2xx.
  • Monitor for missing heartbeats and be ready to recreate a deleted webhook.
  • Don't respond 410 unless you actually want the webhook deleted.

How Hookdeck Can Help: Hookdeck always accepts deliveries from Asana and absorbs downstream failures itself, keeping the webhook alive while it retries to your service, so a brief outage doesn't cost you the whole subscription.

Compact events require follow-up reads and dedup

The Problem: Events are minimal and batched, and the same change can appear more than once. Handlers that expect full objects, or that assume one event per request, break.

Why It Happens: Asana optimizes the event stream for size and delivers batches at-least-once.

Workarounds:

  • Iterate the events array and fetch full objects from the API when needed.
  • Dedupe on the event's resource plus action plus timestamp.

How Hookdeck Can Help: Hookdeck deduplicates deliveries and can split batches, so your app processes one clean event at a time. See our guide to webhook idempotency.

Filters are mandatory for some resources

The Problem: For workspace, team, portfolio, and goal resources, webhook creation requires filters. Omit them and creation fails.

Why It Happens: Broad resources would otherwise generate overwhelming event volume.

Workarounds:

  • Provide filters at creation for these resource types.
  • Scope filters to the resource and action types you actually handle.

How Hookdeck Can Help: Hookdeck's filters let you narrow the stream further downstream, so your handler only sees the events it cares about regardless of what Asana sends.

Best practices

Handle the handshake first and persist the secret

Echo X-Hook-Secret and return 200 before anything else, and store the secret durably per webhook.

Verify every delivery, including heartbeats

Recompute the hex HMAC-SHA256 over the raw body with the stored secret and compare in constant time. Heartbeats are signed too.

Acknowledge within ten seconds, process asynchronously

Return 2xx fast and defer work to a queue so you never approach the 10-second failure threshold or the 24-hour deletion. See why to process webhooks asynchronously.

Process the batch and dedupe

Iterate events, fetch full objects when needed, and dedupe so at-least-once delivery doesn't double-process.

Provide filters and mind the limits

Supply required filters for workspace/team/portfolio/goal resources, and stay within 1,000 webhooks per resource and 10,000 per token.

Make Asana webhooks production-ready

Hookdeck completes the handshake, verifies X-Hook-Signature, deduplicates, and keeps your webhook from being deleted

Conclusion

Asana webhooks establish a shared secret through an in-flight X-Hook-Secret handshake, then sign every delivery (heartbeats included) with an X-Hook-Signature you verify against the raw body. The delivery model has real edges: compact batched events, 8-hour heartbeats, and deletion after 24 hours of failed delivery, so a slow or flaky endpoint can lose the subscription entirely.

That puts the handshake, signature verification, fast acknowledgement, batch handling, and deduplication on you. Hookdeck completes the handshake, verifies signatures, deduplicates, and keeps your webhook alive by absorbing downstream failures, so your app only ever processes verified, unique events.

Get started with Hookdeck for free and handle Asana webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.