When a team member assigns a task to you in Asana, you need to know immediately—not when your polling script checks again in 5 minutes. Asana webhooks solve this problem by sending real-time HTTP notifications to your server the moment events occur, enabling you to automate task assignments, sync project updates to external systems, trigger workflows when tasks are completed, and keep your team informed through custom notifications.
Asana webhooks are HTTP callbacks that deliver instant notifications when tasks, projects, stories, and other resources change within your Asana workspace. Unlike traditional API polling which repeatedly queries for updates, webhooks "push" event data to your server immediately after changes occur, reducing latency and API quota consumption.
In this comprehensive guide, you'll learn how to set up Asana webhooks from scratch, understand the unique handshake process required to establish connections, implement secure signature verification with HMAC-SHA256, handle various event types including task changes and project updates, build production-ready webhook endpoints with proper error handling, and test your integration effectively using our Webhook Payload Generator tool.
What Are Asana Webhooks?
Asana webhooks are real-time event notifications sent via HTTP POST requests from Asana's servers to your application whenever monitored resources change. Unlike the Events API which requires continuous polling, webhooks provide an event-driven architecture where Asana "pushes" updates to your endpoint automatically.
The webhook architecture follows this flow:
[Asana Event Occurs] → [Asana Webhook Service] → [Your Webhook Endpoint] → [Your Application Logic]
When you create a webhook subscription for an Asana resource (such as a task or project), Asana monitors that resource and all contained resources. For example, a webhook on a project will receive events for all tasks within that project, subtasks of those tasks, comments (stories) added to tasks, and even changes to custom fields. This "bubbling up" behavior means you can monitor an entire project hierarchy with a single webhook subscription.
Key Benefits of Asana Webhooks:
- Real-time updates: Events delivered within one minute on average, most within 10 minutes
- Reduced API calls: No need to poll the API repeatedly, saving on rate limits
- Hierarchical monitoring: Events propagate up from subtasks to tasks to projects
- Selective filtering: Subscribe only to specific event types and resource changes
- Efficient infrastructure: Shared with Asana's production event streaming system
Prerequisites for Using Asana Webhooks:
- Active Asana account with API access
- Personal Access Token (PAT) or OAuth token for authentication
- Publicly accessible HTTPS endpoint to receive webhook events
- Resource GID (globally unique identifier) for the task, project, or workspace to monitor
- Server capable of handling the webhook handshake process
Asana webhooks differ from standard webhook implementations in two significant ways. First, they require an initial handshake process using the X-Hook-Secret header to verify your endpoint is ready before sending events. Second, webhook payloads contain compact "lightweight" event data rather than full resource details, meaning you'll need to make additional API calls to fetch complete resource information.
Setting Up Asana Webhooks
Setting up Asana webhooks requires both server-side code to handle incoming webhooks and an API call to establish the webhook subscription with Asana. Follow these steps to configure your first webhook:
Step 1: Prepare Your Webhook Endpoint
First, create an HTTPS endpoint on your server that will receive webhook events. This endpoint must be publicly accessible on the internet. For local development, use a tunneling tool like ngrok.
Your webhook endpoint URL format should be:
https://yourdomain.com/webhooks/asana
For local development with ngrok:
# Start your local server
node server.js # Port 3000
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL in your webhook setup
https://abc123.ngrok.io/webhooks/asana
Step 2: Implement the Handshake Logic
Before Asana sends events, your endpoint must complete a handshake process. Your server needs to handle the initial X-Hook-Secret header and echo it back:
// Node.js handshake example
app.post('/webhooks/asana', (req, res) => {
const hookSecret = req.headers['x-hook-secret'];
// If X-Hook-Secret header exists, this is a handshake request
if (hookSecret) {
console.log('Handshake received, echoing secret back');
// Store the secret for future signature verification
process.env.ASANA_HOOK_SECRET = hookSecret;
// Echo the secret back in response header
res.setHeader('X-Hook-Secret', hookSecret);
return res.status(200).send('OK');
}
// Otherwise, handle webhook event (covered later)
// ... event handling code
});
Step 3: Create the Webhook via API
With your endpoint ready to handle the handshake, make a POST request to Asana's webhook creation endpoint. You'll need:
- Resource GID: The ID of the task, project, or workspace to monitor
- Target URL: Your webhook endpoint URL
- Access Token: Your Asana Personal Access Token
Here's how to create a webhook using cURL:
curl -X POST https://app.asana.com/api/1.0/webhooks \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "1234567890123456",
"target": "https://yourdomain.com/webhooks/asana"
}
}'
Node.js example:
const axios = require('axios');
async function createAsanaWebhook(resourceGid, targetUrl, accessToken) {
try {
const response = await axios.post(
'https://app.asana.com/api/1.0/webhooks',
{
data: {
resource: resourceGid,
target: targetUrl
}
},
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
console.log('Webhook created:', response.data);
return response.data;
} catch (error) {
console.error('Failed to create webhook:', error.response?.data || error.message);
throw error;
}
}
// Usage
createAsanaWebhook(
'1234567890123456', // Resource GID
'https://yourdomain.com/webhooks/asana',
process.env.ASANA_ACCESS_TOKEN
);
Step 4: Add Webhook Filters (Optional)
To reduce noise and only receive specific events, add filters when creating the webhook:
const response = await axios.post(
'https://app.asana.com/api/1.0/webhooks',
{
data: {
resource: resourceGid,
target: targetUrl,
filters: [
{
resource_type: 'task',
action: 'changed',
fields: ['assignee', 'due_at', 'completed']
},
{
resource_type: 'story',
action: 'added'
}
]
}
},
// ... headers
);
This filter configuration ensures you only receive events when task assignees change, due dates are modified, tasks are completed, or new comments are added.
Step 5: Verify the Handshake Succeeded
When the webhook creation API call succeeds, you'll receive a 201 Created response containing the X-Hook-Secret:
{
"data": {
"gid": "9876543210987654",
"resource_type": "webhook",
"active": true,
"resource": {
"gid": "1234567890123456",
"resource_type": "task",
"name": "Complete project documentation"
},
"target": "https://yourdomain.com/webhooks/asana",
"created_at": "2025-01-24T10:30:00.000Z"
},
"X-Hook-Secret": "b537207f20cbfa02357cf448134da559e8bd39d61597dcd5631b8012eae53e81"
}
Pro Tips:
- Store the
X-Hook-Secretsecurely—you'll need it to verify all future webhook events - Use environment variables for secrets, never commit them to version control
- Webhooks on higher-level resources (workspaces, portfolios) require filters to be specified
- Your handshake handler must respond within 10 seconds or the connection will timeout
- Test the handshake locally first before creating production webhooks
- Store the webhook
gidto delete or manage the webhook later
Common Mistakes to Avoid:
- Not implementing the handshake handler before calling the API (webhook creation will fail)
- Using HTTP instead of HTTPS (Asana requires secure endpoints)
- Forgetting to echo back the exact
X-Hook-Secretvalue received - Blocking the handshake response while waiting for the API call to complete (causing circular dependency)
Asana Webhook Events & Payloads
Asana webhooks can deliver events for numerous resource types and actions. Understanding the event structure and available event types is crucial for building robust integrations.
Overview of Event Types
| Event Type | Resource Type | Description | Common Use Case |
|---|---|---|---|
task.added | Task | New task created or added to project | Trigger onboarding workflows |
task.changed | Task | Task fields modified (assignee, due date, status) | Send notifications on assignments |
task.removed | Task | Task removed from project | Clean up external references |
task.deleted | Task | Task permanently deleted | Archive related data |
task.undeleted | Task | Deleted task restored | Restore archived data |
story.added | Story | Comment added to task | Real-time chat notifications |
story.changed | Story | Comment edited | Update notification history |
project.added | Project | New project created | Initialize external tracking |
project.changed | Project | Project details updated | Sync project metadata |
section.added | Section | New section added to project | Update workflow stages |
tag.added | Tag | Tag applied to task | Categorize and route tasks |
Event Payload Structure
All Asana webhook events follow this structure:
{
"events": [
{
"user": {
"gid": "12345",
"resource_type": "user",
"name": "Sarah Johnson"
},
"resource": {
"gid": "67890",
"resource_type": "task",
"name": "Update API documentation"
},
"action": "changed",
"parent": null,
"created_at": "2025-01-24T14:22:30.147Z",
"change": {
"field": "assignee",
"action": "changed",
"new_value": {
"gid": "54321",
"resource_type": "user"
}
}
}
]
}
The events array can contain multiple events delivered in a single request. Empty arrays indicate heartbeat events sent every 8 hours to verify endpoint availability.
Detailed Event Examples
Event: task.changed - Task Assigned
Description: Triggered when a task's assignee field is modified.
Payload Structure:
{
"events": [
{
"user": {
"gid": "98765432101234",
"resource_type": "user",
"name": "Michael Chen"
},
"resource": {
"gid": "11223344556677",
"resource_type": "task",
"name": "Review pull request #342"
},
"action": "changed",
"parent": null,
"created_at": "2025-01-24T15:45:12.234Z",
"change": {
"field": "assignee",
"action": "changed",
"new_value": {
"gid": "55667788990011",
"resource_type": "user"
}
}
}
]
}
Key Fields:
user.gid- The user who made the change (not necessarily the new assignee)resource.gid- The task that was modifiedaction- Always "changed" for field modificationschange.field- The specific field that changed ("assignee")change.action- How the field was modified ("changed", "added", or "removed")change.new_value- The new assignee's compact representation
Use Case: Send Slack notification to the new assignee when tasks are assigned.
Event: task.changed - Custom Field Updated
Description: Triggered when a custom field value is modified on a task.
Payload Structure:
{
"events": [
{
"user": {
"gid": "12345678901234",
"resource_type": "user",
"name": "Alex Rodriguez"
},
"resource": {
"gid": "99887766554433",
"resource_type": "task",
"name": "Bug fix: Login timeout"
},
"action": "changed",
"parent": null,
"created_at": "2025-01-24T16:20:44.891Z",
"change": {
"field": "custom_fields",
"action": "changed",
"new_value": {
"gid": "11111111111111",
"resource_type": "custom_field"
}
}
}
]
}
Key Fields:
change.field- "custom_fields" indicates a custom field was modifiedchange.new_value.gid- The custom field definition ID (not the value itself)
Important Note: The webhook payload does not include the actual custom field value. You must make a follow-up API call to fetch the complete task data:
// Fetch full task details after receiving event
const task = await axios.get(
`https://app.asana.com/api/1.0/tasks/${resource.gid}`,
{
params: { opt_fields: 'custom_fields,custom_fields.name,custom_fields.display_value' },
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
Use Case: Track priority changes via custom fields and escalate high-priority tasks.
Event: story.added - Comment Added
Description: Triggered when a comment (story) is added to a task.
Payload Structure:
{
"events": [
{
"user": {
"gid": "33445566778899",
"resource_type": "user",
"name": "Jennifer Liu"
},
"resource": {
"gid": "22334455667788",
"resource_type": "story",
"name": null
},
"action": "added",
"parent": {
"gid": "11223344556677",
"resource_type": "task",
"name": "Review pull request #342"
},
"created_at": "2025-01-24T17:05:22.456Z",
"change": null
}
]
}
Key Fields:
resource.resource_type- "story" indicates a comment eventaction- "added" for new commentsparent.gid- The task that received the commentchange- null for added/removed events
Note: The comment text is not included in the webhook payload. Fetch the story resource to retrieve content:
const story = await axios.get(
`https://app.asana.com/api/1.0/stories/${resource.gid}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
console.log('Comment text:', story.data.data.text);
Use Case: Feed task comments into a team chat system or notification service.
Event: task.added - Task Added to Project
Description: Triggered when a task is added to a project or created within a project.
Payload Structure:
{
"events": [
{
"user": {
"gid": "77889900112233",
"resource_type": "user",
"name": "David Park"
},
"resource": {
"gid": "44556677889900",
"resource_type": "task",
"name": "Implement user authentication"
},
"action": "added",
"parent": {
"gid": "99001122334455",
"resource_type": "project",
"name": "Q1 2025 Development Sprint"
},
"created_at": "2025-01-24T18:12:55.789Z",
"change": null
}
]
}
Key Fields:
action- "added" indicates the task was newly addedparent- The project where the task was addedchange- Always null for add/remove events
Use Case: Automatically create corresponding tickets in external project management tools when new tasks are added.
Event: task.changed - Task Completed
Description: Triggered when a task's completion status changes.
Payload Structure:
{
"events": [
{
"user": {
"gid": "11223344556677",
"resource_type": "user",
"name": "Emma Watson"
},
"resource": {
"gid": "22334455667788",
"resource_type": "task",
"name": "Write unit tests for API endpoints"
},
"action": "changed",
"parent": null,
"created_at": "2025-01-24T19:30:18.123Z",
"change": {
"field": "completed",
"action": "changed",
"new_value": true
}
}
]
}
Key Fields:
change.field- "completed" indicates completion status changedchange.new_value- Boolean true for completed, false for reopened
Use Case: Trigger automated deployment pipelines or send completion notifications to stakeholders.
Understanding Event Propagation
Events "bubble up" through the resource hierarchy. If you subscribe to a project webhook, you'll receive events for:
- Tasks directly in the project
- Subtasks of those tasks
- Comments (stories) on tasks and subtasks
- Custom field changes on any contained task
- Attachments added to tasks
This hierarchical propagation means a single webhook on a project can monitor an entire team's workflow.
Webhook Signature Verification
Signature verification is critical for security. Without it, malicious actors could send fake events to your endpoint, potentially triggering unauthorized actions or corrupting data. Asana uses HMAC-SHA256 signatures to ensure webhook authenticity.
Why Signature Verification Matters
When your webhook endpoint is publicly accessible, anyone who discovers the URL could theoretically send POST requests to it. Signature verification ensures that:
- Events genuinely come from Asana's servers
- The payload hasn't been tampered with during transit
- Replay attacks using old valid payloads are detectable via timestamps
- Your integration can safely process events without additional verification
Asana's Signature Method
Asana's webhook signature system uses:
- Algorithm: HMAC-SHA256 (Hash-based Message Authentication Code with SHA-256)
- Signature Header:
X-Hook-Signature(sent with every event) - Secret Source:
X-Hook-Secret(received during handshake) - Signed Data: The complete raw request body
- Encoding: Hexadecimal string representation
Step-by-Step Verification Process
- Extract the signature from the
X-Hook-Signatureheader in the incoming request - Retrieve the webhook secret stored during the handshake (
X-Hook-Secret) - Get the raw request body as a string or buffer (do NOT parse JSON first)
- Compute the expected signature using HMAC-SHA256 with the secret and body
- Compare signatures using constant-time comparison to prevent timing attacks
- Validate timestamp (optional but recommended) to prevent replay attacks
Implementation Examples
Node.js / Express
const crypto = require('crypto');
const express = require('express');
const app = express();
// Store the webhook secret (set during handshake)
const WEBHOOK_SECRET = process.env.ASANA_HOOK_SECRET;
// IMPORTANT: Use raw body parser for signature verification
app.use('/webhooks/asana', express.raw({type: 'application/json'}));
app.post('/webhooks/asana', (req, res) => {
// Handle handshake first
const hookSecret = req.headers['x-hook-secret'];
if (hookSecret) {
// This is the handshake - echo secret back
res.setHeader('X-Hook-Secret', hookSecret);
// Store the secret for future verifications
process.env.ASANA_HOOK_SECRET = hookSecret;
return res.status(200).send('OK');
}
// Verify signature for webhook events
const signature = req.headers['x-hook-signature'];
if (!signature) {
console.error('Missing X-Hook-Signature header');
return res.status(401).send('Unauthorized');
}
// Compute expected signature from raw body
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('Invalid signature - potential spoofing attempt');
return res.status(401).send('Unauthorized');
}
// Signature verified - safe to parse payload
const payload = JSON.parse(req.body.toString());
// Process webhook events
console.log(`Received ${payload.events.length} events`);
payload.events.forEach(event => {
console.log(`Event: ${event.action} on ${event.resource.resource_type} ${event.resource.gid}`);
});
// Return 200 immediately
res.status(200).send('Webhook received');
// Process async (recommended)
processWebhookAsync(payload);
});
function processWebhookAsync(payload) {
// Your business logic here
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Python / Flask
import hmac
import hashlib
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
# Store the webhook secret (set during handshake)
WEBHOOK_SECRET = None
@app.route('/webhooks/asana', methods=['POST'])
def asana_webhook():
global WEBHOOK_SECRET
# Handle handshake first
hook_secret = request.headers.get('X-Hook-Secret')
if hook_secret:
# This is the handshake - echo secret back
WEBHOOK_SECRET = hook_secret
os.environ['ASANA_HOOK_SECRET'] = hook_secret
response = jsonify({'status': 'ok'})
response.headers['X-Hook-Secret'] = hook_secret
return response, 200
# Verify signature for webhook events
signature = request.headers.get('X-Hook-Signature')
if not signature:
print('Missing X-Hook-Signature header')
return 'Unauthorized', 401
if not WEBHOOK_SECRET:
print('Webhook secret not set - handshake may have failed')
return 'Unauthorized', 401
# Get raw request body
payload = request.get_data()
# Compute expected signature
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Use constant-time comparison to prevent timing attacks
if not hmac.compare_digest(signature, expected_signature):
print('Invalid signature - potential spoofing attempt')
return 'Unauthorized', 401
# Signature verified - safe to parse payload
data = request.get_json()
# Process webhook events
print(f"Received {len(data['events'])} events")
for event in data['events']:
print(f"Event: {event['action']} on {event['resource']['resource_type']} {event['resource']['gid']}")
# Return 200 immediately
return 'Webhook received', 200
PHP
<?php
// Store webhook secret in environment or config
$webhookSecret = getenv('ASANA_HOOK_SECRET');
// Get raw POST body
$payload = file_get_contents('php://input');
// Handle handshake
$hookSecret = $_SERVER['HTTP_X_HOOK_SECRET'] ?? null;
if ($hookSecret) {
// This is the handshake - echo secret back
header('X-Hook-Secret: ' . $hookSecret);
// Store secret for future use
putenv("ASANA_HOOK_SECRET=$hookSecret");
http_response_code(200);
echo 'OK';
exit;
}
// Get signature from header
$signature = $_SERVER['HTTP_X_HOOK_SIGNATURE'] ?? null;
if (!$signature) {
error_log('Missing X-Hook-Signature header');
http_response_code(401);
die('Unauthorized');
}
if (!$webhookSecret) {
error_log('Webhook secret not set - handshake may have failed');
http_response_code(401);
die('Unauthorized');
}
// Compute expected signature
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
// Use constant-time comparison to prevent timing attacks
if (!hash_equals($signature, $expectedSignature)) {
error_log('Invalid signature - potential spoofing attempt');
http_response_code(401);
die('Unauthorized');
}
// Signature verified - safe to parse payload
$data = json_decode($payload, true);
// Process webhook events
error_log(sprintf("Received %d events", count($data['events'])));
foreach ($data['events'] as $event) {
error_log(sprintf(
"Event: %s on %s %s",
$event['action'],
$event['resource']['resource_type'],
$event['resource']['gid']
));
}
// Return 200 immediately
http_response_code(200);
echo 'Webhook received';
?>
Common Verification Errors
-
❌ Parsing JSON before verification: The body is modified when parsed, breaking the signature
- ✅ Solution: Always verify using the raw body, parse JSON only after verification passes
-
❌ Using wrong secret: Test secret used in production or vice versa
- ✅ Solution: Store secrets per environment and verify you're using the correct one
-
❌ Not using constant-time comparison: Opens vulnerability to timing attacks
- ✅ Solution: Use
crypto.timingSafeEqual()(Node.js),hmac.compare_digest()(Python), orhash_equals()(PHP)
- ✅ Solution: Use
-
❌ Missing the handshake handler: Webhook creation fails because server can't echo secret
- ✅ Solution: Implement handshake logic before attempting to create webhooks via API
-
❌ Encoding issues: Using wrong character encoding when computing hash
- ✅ Solution: Ensure consistent UTF-8 encoding for both secret and payload
Testing Asana Webhooks
Testing webhooks during development presents unique challenges since Asana's servers need to reach your endpoint over the internet. Here are proven strategies for testing effectively.
Local Development Challenges
The primary obstacle when testing Asana webhooks locally:
- Asana servers cannot reach
localhostor127.0.0.1 - Your development machine isn't publicly accessible
- You need HTTPS for security (Asana requires it)
- The handshake process requires bidirectional communication
Solution 1: Using ngrok
ngrok creates a secure tunnel from a public URL to your local development server:
# Install ngrok (macOS)
brew install ngrok
# Or download from ngrok.com for other platforms
# Start your local webhook server
node server.js # Listening on port 3000
# In another terminal, start ngrok tunnel
ngrok http 3000
# Output shows your public URL:
# Forwarding https://abc123def456.ngrok.io -> http://localhost:3000
Use the ngrok HTTPS URL when creating your webhook:
createAsanaWebhook(
'1234567890123456', // Resource GID
'https://abc123def456.ngrok.io/webhooks/asana', // ngrok URL
process.env.ASANA_ACCESS_TOKEN
);
Benefits of ngrok:
- ✅ Full end-to-end testing with real Asana events
- ✅ Tests handshake process and signature verification
- ✅ Handles HTTPS automatically
- ✅ Inspects request/response via web interface (http://localhost:4040)
Considerations:
- Free tier URLs change with each restart
- Requires keeping ngrok running during development
- Network latency for each request
Solution 2: Webhook Payload Generator Tool
For testing without external dependencies, use our Webhook Payload Generator:
Step-by-step testing process:
-
Visit the tool: Navigate to our Webhook Payload Generator
-
Select provider: Choose "Asana" from the provider dropdown
-
Choose event type: Select an event like "task.changed" or "story.added"
-
Customize payload: Modify field values to match your test scenario:
- Task GID
- User information
- Changed field (assignee, due_at, completed)
- Custom field values
-
Generate signature: The tool automatically generates a valid
X-Hook-Signatureusing your webhook secret -
Send to local endpoint: Copy the generated payload and send it to your local server:
curl -X POST http://localhost:3000/webhooks/asana \
-H "Content-Type: application/json" \
-H "X-Hook-Signature: generated-signature-here" \
-d @payload.json
Benefits of the Webhook Payload Generator:
- ✅ No tunneling or public exposure required
- ✅ Test signature verification logic independently
- ✅ Customize payload values for edge case testing
- ✅ Test error handling without triggering real Asana events
- ✅ Rapid iteration during development
What it can't test:
- The actual handshake process (use ngrok for this)
- Event propagation and filtering
- Asana's retry behavior
Testing the Handshake Process
The handshake is the most common failure point. Test it explicitly:
// Test handshake handler
const request = require('supertest');
const app = require('./server'); // Your Express app
describe('Asana Webhook Handshake', () => {
it('should echo X-Hook-Secret in response', async () => {
const testSecret = 'test-secret-12345';
const response = await request(app)
.post('/webhooks/asana')
.set('X-Hook-Secret', testSecret)
.expect(200);
expect(response.headers['x-hook-secret']).toBe(testSecret);
});
});
Testing Checklist
Before deploying to production, verify:
- Handshake handler echoes
X-Hook-Secretcorrectly - Signature verification passes with valid signatures
- Signature verification rejects invalid signatures
- Endpoint returns 200 status within 10 seconds
- Handles empty events array (heartbeat events)
- Processes multiple events in single payload
- Implements idempotency (handles duplicate events)
- Error handling for malformed payloads
- Async processing doesn't block response
- Logging captures all events for debugging
Using Asana's Webhook Inspector
After creating a webhook, monitor its health via the API:
// Check webhook status
const webhook = await axios.get(
`https://app.asana.com/api/1.0/webhooks/${webhookGid}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
console.log('Last success:', webhook.data.data.last_success_at);
console.log('Last failure:', webhook.data.data.last_failure_at);
console.log('Failure content:', webhook.data.data.last_failure_content);
console.log('Retry count:', webhook.data.data.delivery_retry_count);
These fields help diagnose delivery issues and verify your endpoint is responding correctly.
Implementation Example
Building a production-ready Asana webhook endpoint requires more than just signature verification. Here's a complete implementation with queue-based processing, idempotency, error handling, and proper logging.
Requirements for Production Webhooks
Your webhook endpoint must:
- Respond within 10 seconds (Asana's timeout)
- Return 200 or 204 status code for successful receipt
- Process events asynchronously to avoid blocking
- Handle retries gracefully (Asana retries for 24 hours)
- Implement idempotency to prevent duplicate processing
- Log all events for debugging and audit trails
- Respond to heartbeat events every 8 hours
Complete Node.js Implementation
const express = require('express');
const crypto = require('crypto');
const Queue = require('bull'); // npm install bull redis
const axios = require('axios');
const { PrismaClient } = require('@prisma/client');
const app = express();
const prisma = new PrismaClient();
const webhookQueue = new Queue('asana-webhooks', process.env.REDIS_URL);
// Store webhook secret
let WEBHOOK_SECRET = process.env.ASANA_HOOK_SECRET;
const ASANA_ACCESS_TOKEN = process.env.ASANA_ACCESS_TOKEN;
// Raw body parser for signature verification
app.use('/webhooks/asana', express.raw({type: 'application/json'}));
// Asana webhook endpoint
app.post('/webhooks/asana', async (req, res) => {
try {
// Step 1: Handle handshake
const hookSecret = req.headers['x-hook-secret'];
if (hookSecret) {
console.log('Webhook handshake initiated');
WEBHOOK_SECRET = hookSecret;
process.env.ASANA_HOOK_SECRET = hookSecret;
// Store secret in database for persistence
await prisma.config.upsert({
where: { key: 'asana_webhook_secret' },
update: { value: hookSecret },
create: { key: 'asana_webhook_secret', value: hookSecret }
});
res.setHeader('X-Hook-Secret', hookSecret);
return res.status(200).send('OK');
}
// Step 2: Verify signature
const signature = req.headers['x-hook-signature'];
if (!signature) {
console.error('Missing X-Hook-Signature header');
return res.status(401).json({ error: 'Missing signature' });
}
if (!WEBHOOK_SECRET) {
console.error('Webhook secret not configured');
return res.status(500).json({ error: 'Server configuration error' });
}
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
// Timing-safe comparison
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('Invalid signature - possible spoofing attempt');
return res.status(401).json({ error: 'Invalid signature' });
}
// Step 3: Parse payload after verification
const payload = JSON.parse(req.body.toString());
// Handle heartbeat (empty events array)
if (payload.events.length === 0) {
console.log('Heartbeat received - webhook is healthy');
return res.status(200).json({ received: true, heartbeat: true });
}
// Step 4: Queue events for async processing
const queuedEvents = [];
for (const event of payload.events) {
const eventId = generateEventId(event);
// Check for duplicate (idempotency)
const exists = await prisma.webhookEvent.findUnique({
where: { eventId }
});
if (exists) {
console.log(`Event ${eventId} already processed, skipping`);
continue;
}
// Queue event for processing
await webhookQueue.add({
eventId,
event,
receivedAt: new Date()
});
queuedEvents.push(eventId);
}
// Step 5: Return 200 immediately
res.status(200).json({
received: true,
queued: queuedEvents.length
});
console.log(`Queued ${queuedEvents.length} events for processing`);
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to prevent retries for our errors
res.status(200).json({ received: true, error: true });
}
});
// Process webhooks from queue
webhookQueue.process(async (job) => {
const { eventId, event, receivedAt } = job.data;
try {
console.log(`Processing event ${eventId}: ${event.action} on ${event.resource.resource_type}`);
// Mark as processing
await prisma.webhookEvent.create({
data: {
eventId,
status: 'processing',
resourceType: event.resource.resource_type,
resourceGid: event.resource.gid,
action: event.action,
payload: event,
receivedAt
}
});
// Route to appropriate handler based on resource type and action
await routeEvent(event);
// Mark as completed
await prisma.webhookEvent.update({
where: { eventId },
data: {
status: 'completed',
completedAt: new Date()
}
});
console.log(`Successfully processed event ${eventId}`);
} catch (error) {
console.error(`Failed to process event ${eventId}:`, error);
// Mark as failed
await prisma.webhookEvent.update({
where: { eventId },
data: {
status: 'failed',
error: error.message,
failedAt: new Date()
}
});
throw error; // Will trigger queue retry
}
});
// Route events to specific handlers
async function routeEvent(event) {
const { resource, action } = event;
switch (resource.resource_type) {
case 'task':
await handleTaskEvent(event);
break;
case 'story':
await handleStoryEvent(event);
break;
case 'project':
await handleProjectEvent(event);
break;
default:
console.warn(`Unhandled resource type: ${resource.resource_type}`);
}
}
// Handle task events
async function handleTaskEvent(event) {
const { resource, action, change } = event;
if (action === 'changed' && change?.field === 'assignee') {
// Task was assigned - fetch full details
const task = await fetchAsanaResource('tasks', resource.gid);
// Send notification to new assignee
if (task.assignee) {
await sendAssignmentNotification(task);
}
}
if (action === 'changed' && change?.field === 'completed') {
// Task was completed
const task = await fetchAsanaResource('tasks', resource.gid);
if (change.new_value === true) {
await handleTaskCompletion(task);
}
}
if (action === 'added') {
// New task added to project
const task = await fetchAsanaResource('tasks', resource.gid);
await syncTaskToExternalSystem(task);
}
}
// Handle story (comment) events
async function handleStoryEvent(event) {
const { resource, action, parent } = event;
if (action === 'added' && parent?.resource_type === 'task') {
// New comment added to task
const story = await fetchAsanaResource('stories', resource.gid);
if (story.type === 'comment') {
await notifyTeamOfComment(story, parent.gid);
}
}
}
// Handle project events
async function handleProjectEvent(event) {
const { resource, action } = event;
if (action === 'changed') {
// Project details updated
const project = await fetchAsanaResource('projects', resource.gid);
await syncProjectMetadata(project);
}
}
// Fetch full resource details from Asana API
async function fetchAsanaResource(resourceType, gid) {
const response = await axios.get(
`https://app.asana.com/api/1.0/${resourceType}/${gid}`,
{
headers: {
'Authorization': `Bearer ${ASANA_ACCESS_TOKEN}`
}
}
);
return response.data.data;
}
// Business logic: Send assignment notification
async function sendAssignmentNotification(task) {
console.log(`Sending notification: Task "${task.name}" assigned to ${task.assignee.name}`);
// Example: Send Slack message
await axios.post(process.env.SLACK_WEBHOOK_URL, {
text: `New task assigned: *${task.name}*`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${task.assignee.name}* was assigned to:\n*${task.name}*`
}
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Due: ${task.due_on || 'No due date'} | <https://app.asana.com/0/${task.gid}|View in Asana>`
}
]
}
]
});
}
// Business logic: Handle task completion
async function handleTaskCompletion(task) {
console.log(`Task completed: "${task.name}"`);
// Example: Update external database
await prisma.externalTask.update({
where: { asanaGid: task.gid },
data: {
status: 'completed',
completedAt: new Date()
}
});
// Example: Trigger downstream workflow
if (task.custom_fields.some(cf => cf.name === 'Triggers Deployment' && cf.display_value === 'Yes')) {
await triggerDeploymentPipeline(task);
}
}
// Business logic: Sync task to external system
async function syncTaskToExternalSystem(task) {
console.log(`Syncing new task to external system: "${task.name}"`);
await prisma.externalTask.create({
data: {
asanaGid: task.gid,
name: task.name,
assignee: task.assignee?.name,
dueDate: task.due_on,
status: task.completed ? 'completed' : 'in_progress'
}
});
}
// Business logic: Notify team of comment
async function notifyTeamOfComment(story, taskGid) {
console.log(`New comment on task ${taskGid}: "${story.text}"`);
// Example: Send email notification
// await sendEmail({
// to: '[email protected]',
// subject: `New comment on task`,
// body: story.text
// });
}
// Business logic: Sync project metadata
async function syncProjectMetadata(project) {
console.log(`Syncing project metadata: "${project.name}"`);
await prisma.externalProject.upsert({
where: { asanaGid: project.gid },
update: {
name: project.name,
dueDate: project.due_on,
status: project.archived ? 'archived' : 'active'
},
create: {
asanaGid: project.gid,
name: project.name,
dueDate: project.due_on,
status: project.archived ? 'archived' : 'active'
}
});
}
// Generate consistent event ID for idempotency
function generateEventId(event) {
// Combine resource GID, action, and timestamp for unique ID
const idString = `${event.resource.gid}-${event.action}-${event.created_at}`;
return crypto.createHash('sha256').update(idString).digest('hex');
}
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Asana webhook server listening on port ${PORT}`);
});
Key Implementation Details
- Raw body parsing: The
express.raw()middleware preserves the exact body for signature verification - Timing-safe comparison:
crypto.timingSafeEqual()prevents timing attack vulnerabilities - Idempotency check: Database lookup ensures events are processed exactly once
- Queue-based processing: Bull queue with Redis ensures fast response times
- Error handling: Always returns 200 to Asana, logs errors for internal debugging
- Heartbeat handling: Responds to empty events array to maintain webhook health
- Resource fetching: Webhook payloads are compact, full details fetched via API
Best Practices
Following these best practices ensures your Asana webhook integration is secure, reliable, and maintainable.
Security
- ✅ Always verify signatures: Never process events without signature verification
- ✅ Use HTTPS endpoints only: Asana requires HTTPS; use SSL certificates from Let's Encrypt or similar
- ✅ Store secrets in environment variables: Never commit
X-Hook-Secretto version control - ✅ Validate timestamp fields: Check
created_atto reject old events and prevent replay attacks - ✅ Rate limit webhook endpoints: Protect against abuse even with signature verification
- ✅ Implement IP whitelisting: Asana doesn't publish IP ranges, but consider reverse proxy filtering
- ✅ Rotate secrets periodically: Delete and recreate webhooks with new secrets quarterly
- ✅ Log security events: Track all signature verification failures for security monitoring
Performance
- ✅ Respond within 10 seconds: Asana's timeout is strict; acknowledge receipt immediately
- ✅ Return 200 immediately, process async: Use queues (Redis, RabbitMQ, AWS SQS) for background processing
- ✅ Batch API calls: When fetching multiple resources, use Asana's batch endpoint
- ✅ Implement exponential backoff: When calling external services, retry with increasing delays
- ✅ Monitor webhook processing times: Track queue depth and processing latency
- ✅ Cache frequently accessed data: Store user mappings, custom field definitions locally
- ✅ Limit concurrent processing: Prevent overwhelming downstream systems with parallel processing limits
Reliability
- ✅ Implement idempotency: Always track event IDs to prevent duplicate processing
- ✅ Handle duplicate webhooks gracefully: Asana may deliver the same event multiple times
- ✅ Implement retry logic for failures: Use exponential backoff when business logic fails
- ✅ Don't rely solely on webhooks: Run periodic reconciliation jobs to catch missed events
- ✅ Log all webhook events: Maintain audit trail for debugging and compliance
- ✅ Handle heartbeat events: Respond to empty events arrays to keep webhook alive
- ✅ Monitor webhook health: Check
last_success_atandlast_failure_atvia API - ✅ Set up alerting: Get notified when delivery_retry_count increases or failures occur
Monitoring
- ✅ Track webhook delivery success rate: Monitor via Asana API's webhook inspection
- ✅ Alert on signature verification failures: Immediate notification for security issues
- ✅ Monitor processing queue depth: Alert when queue backs up beyond threshold
- ✅ Log event IDs for traceability: Enable debugging by tracking events end-to-end
- ✅ Set up health checks: Expose endpoint returning webhook status and last event time
- ✅ Dashboard key metrics: Track events/hour, processing time, error rate
- ✅ Monitor rate limit usage: Track API calls made while processing webhooks
Asana-Specific Best Practices
- ✅ Handle story consolidation: Be aware that rapid changes may result in consolidated stories in Asana's UI, but you'll receive individual webhook events
- ✅ Fetch full resource details: Webhook payloads are compact; always make API calls for complete data
- ✅ Use webhook filters: Reduce noise by filtering at webhook creation time
- ✅ Understand event propagation: Events bubble up from subtasks to tasks to projects
- ✅ Respect the 10,000 webhook limit: Per access token, plan your webhook architecture accordingly
- ✅ Handle deleted resources gracefully: Webhooks are auto-deleted 72 hours after resource deletion
- ✅ Test handshake extensively: Most webhook setup failures occur during handshake
- ✅ Return 410 Gone to delete: If you no longer need a webhook, return 410 status to auto-delete it
Common Issues & Troubleshooting
Issue 1: Webhook Creation Fails During Handshake
Symptoms:
- API call to create webhook times out or returns error
- Error message: "Failed to establish webhook"
- Webhook never appears in GET /webhooks list
Causes & Solutions:
- ❌ Handshake handler not implemented: Server doesn't respond to
X-Hook-Secret- ✅ Solution: Implement handshake logic before calling webhook creation API
// Must handle X-Hook-Secret BEFORE creating webhook
app.post('/webhooks/asana', (req, res) => {
const hookSecret = req.headers['x-hook-secret'];
if (hookSecret) {
res.setHeader('X-Hook-Secret', hookSecret);
return res.status(200).send('OK');
}
// ... rest of handler
});
-
❌ Server not responding fast enough: Handshake times out after 10 seconds
- ✅ Solution: Ensure handshake handler isn't blocked by slow operations
-
❌ Server not publicly accessible: Using localhost or private IP
- ✅ Solution: Use ngrok for development or deploy to public server
-
❌ SSL certificate issues: Invalid or self-signed certificate
- ✅ Solution: Use valid SSL certificate from trusted CA
-
❌ Circular dependency: Server waits for webhook creation while Asana waits for handshake
- ✅ Solution: Handle handshake synchronously in web server, create webhook in separate process
Issue 2: Signature Verification Failing
Symptoms:
- 401 errors being returned to Asana
- "Invalid signature" errors in application logs
- Webhook shows failures in
last_failure_content
Causes & Solutions:
- ❌ Using wrong secret: Test secret used in production environment
- ✅ Solution: Verify you're using the correct
X-Hook-Secretfor the environment
- ✅ Solution: Verify you're using the correct
// Check which secret is being used
console.log('Using webhook secret:', WEBHOOK_SECRET?.substring(0, 8) + '...');
- ❌ Parsing JSON before verification: Body is modified, breaking signature
- ✅ Solution: Always use raw body parser for webhook routes
// Correct: raw body
app.use('/webhooks/asana', express.raw({type: 'application/json'}));
// Wrong: JSON parser modifies body
app.use(express.json()); // Don't use this for webhook routes
- ❌ Not using constant-time comparison: Vulnerable to timing attacks
- ✅ Solution: Use timing-safe comparison functions
// Correct
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
// Wrong - vulnerable to timing attacks
signature === expectedSignature
- ❌ Character encoding issues: Using wrong encoding for signature computation
- ✅ Solution: Ensure UTF-8 encoding consistently
# Python - ensure utf-8 encoding
hmac.new(secret.encode('utf-8'), payload, hashlib.sha256)
Issue 3: Webhook Timeouts
Symptoms:
- Asana webhook delivery logs show timeout errors
delivery_retry_countincreasinglast_failure_contentshows timeout message
Causes & Solutions:
- ❌ Slow database queries blocking response: Synchronous DB operations
- ✅ Solution: Move all processing to async queue
// Wrong - blocks response
app.post('/webhooks/asana', async (req, res) => {
await processEvent(event); // Blocks for seconds
res.status(200).send('OK');
});
// Correct - immediate response
app.post('/webhooks/asana', async (req, res) => {
await queue.add(event); // Fast queue operation
res.status(200).send('OK'); // Returns immediately
// Processing happens in background
});
-
❌ External API calls blocking: Waiting for third-party services
- ✅ Solution: Return 200 first, call external APIs in background worker
-
❌ Complex business logic: Synchronous processing takes too long
- ✅ Solution: Use background jobs with timeout monitoring
-
❌ Large payload processing: Parsing or validating large events synchronously
- ✅ Solution: Stream processing or chunk processing in background
Issue 4: Duplicate Event Processing
Symptoms:
- Same action executed multiple times
- Database constraints violated
- Duplicate notifications sent
Causes & Solutions:
- ❌ No idempotency check: Events processed every time received
- ✅ Solution: Track processed event IDs in database
const eventId = generateEventId(event);
const exists = await db.webhookEvents.findUnique({ where: { eventId } });
if (exists) {
console.log('Event already processed, skipping');
return res.status(200).json({ received: true, duplicate: true });
}
-
❌ Network retries from Asana: Endpoint times out, Asana retries
- ✅ Solution: Implement idempotent operations that can safely run multiple times
-
❌ Using non-unique identifiers: Resource GID alone isn't unique for change events
- ✅ Solution: Combine resource GID, action, and timestamp for unique ID
Issue 5: Webhook Automatically Deleted
Symptoms:
- GET /webhooks/{gid} returns 404
- Events stop arriving unexpectedly
- No notification of deletion
Causes & Solutions:
- ❌ Not responding to heartbeat events: Empty events array ignored
- ✅ Solution: Always respond with 200 to heartbeat events
if (payload.events.length === 0) {
console.log('Heartbeat received');
return res.status(200).send('OK'); // Critical!
}
-
❌ 24-hour delivery failure: Endpoint down for extended period
- ✅ Solution: Monitor uptime, set up automatic webhook recreation
-
❌ Resource deleted: Parent task/project was deleted
- ✅ Solution: Webhooks auto-delete 72 hours after resource deletion; recreate if needed
-
❌ Access token revoked: PAT used to create webhook was deleted
- ✅ Solution: Use service account tokens with longer lifetime
Issue 6: Missing Webhook Events
Symptoms:
- Expected events not arriving
- Gaps in event sequence
- Changes visible in Asana UI but no webhook received
Causes & Solutions:
- ❌ Event filters too restrictive: Filter configuration blocks events
- ✅ Solution: Review filter configuration, test with broader filters
// Too restrictive - only assignee changes on milestones
filters: [{
resource_type: 'task',
resource_subtype: 'milestone',
action: 'changed',
fields: ['assignee']
}]
// Better - all task changes
filters: [{
resource_type: 'task',
action: 'changed'
}]
-
❌ Wrong resource subscribed: Webhook on wrong task/project
- ✅ Solution: Verify resource GID is correct
-
❌ At-most-once delivery: Rare event loss (Asana's documented behavior)
- ✅ Solution: Implement reconciliation polling as fallback
-
❌ Event outside filter scope: Higher-level webhooks don't receive all events
- ✅ Solution: Workspace webhooks don't receive task-level events; use project webhooks
Debugging Checklist
When troubleshooting webhook issues:
- Check Asana webhook delivery logs via API (GET /webhooks/{gid})
- Verify webhook endpoint is publicly accessible (test with curl)
- Test signature verification with known-good payload from our Webhook Payload Generator
- Check application logs for errors and signature verification results
- Verify SSL certificate is valid and not expired
- Confirm webhook secret matches what was received during handshake
- Test with empty events array to verify heartbeat handling
- Check
last_failure_contentfield for Asana's error messages - Monitor
delivery_retry_countto see if retries are occurring - Verify resource hasn't been deleted (webhooks auto-delete 72 hours later)
Frequently Asked Questions
Q: How often does Asana send webhooks?
A: Asana sends webhooks immediately when events occur, typically within one minute on average. Most events arrive within 10 minutes. In exceptional circumstances, events may be delayed beyond 10 minutes. If delivery fails, Asana retries with exponential backoff for up to 24 hours before deleting the webhook. Additionally, Asana sends heartbeat events (empty payloads) every 8 hours to verify endpoint availability.
Q: Can I receive webhooks for past events?
A: No, Asana webhooks only deliver events that occur after the webhook is created. You cannot receive webhooks for historical events. To retrieve past data, use the Asana API's GET endpoints to fetch resources and their history. The Events API (/events endpoint) provides events for the last 24 hours with sync tokens, but webhooks themselves don't support historical replay.
Q: What happens if my endpoint is down?
A: Asana will retry failed webhook deliveries with exponential backoff for up to 24 hours. After 24 hours of continuous failures, the webhook will be automatically deleted. You can monitor retry attempts via the delivery_retry_count field when fetching webhook details. Asana also sends heartbeat events every 8 hours; if your endpoint doesn't respond to heartbeats for 24 hours, the webhook is deleted even if there are no active events.
Q: Do I need different endpoints for test and production?
A: Yes, it's strongly recommended to use separate webhook URLs for development/test and production environments with different webhook secrets. This allows you to test changes safely without affecting production. Use different Asana workspaces or projects for testing, and ensure your test endpoint handles the same event types as production. You can use the same codebase with environment-specific configuration.
Q: How do I handle webhook event ordering?
A: Asana does not guarantee webhook events will arrive in the order they occurred. Events are delivered as quickly as possible, but network conditions and processing delays can cause reordering. Best practice: always use the created_at timestamp field on events to determine true chronological order. Design your event handlers to be idempotent and handle events regardless of arrival order. If strict ordering is critical, implement a time-based queue that processes events in timestamp order.
Q: Can I filter which events I receive?
A: Yes, when creating a webhook you can specify filters to only receive specific event types. Filters support resource_type (e.g., "task", "story"), resource_subtype (e.g., "milestone"), action (e.g., "changed", "added"), and fields (specific field changes when action is "changed"). Webhooks on higher-level resources like workspaces and portfolios must include filters. Use our Webhook Payload Generator to test how different filter configurations affect event delivery.
Q: Why am I receiving events for resources I'm not subscribed to?
A: This is due to Asana's event propagation behavior. Events "bubble up" from child resources to parent resources. For example, a webhook on a project receives events for all tasks in that project, subtasks of those tasks, comments on tasks and subtasks, and custom field changes. Similarly, a webhook on a workspace receives events for contained projects (if filtered appropriately). This is by design and allows you to monitor entire hierarchies with a single webhook.
Q: What's the difference between webhooks and the Events API?
A: Both use the same infrastructure but differ in delivery method. Webhooks "push" events to your server automatically, requiring a publicly accessible endpoint but providing real-time notifications. The Events API requires you to "poll" for events by making repeated GET requests with sync tokens, is easier to implement (no server needed), and works from localhost. Events are available via the Events API for 24 hours after occurring. For production integrations requiring real-time response, webhooks are preferred.
Q: Can I replay or retrieve old webhook events?
A: No, webhook events are delivered once and cannot be replayed. Once delivered, the event is not stored by Asana for retrieval. However, the same events are available through the Events API endpoint for 24 hours after they occur. After 24 hours, sync tokens expire. If you need an audit trail or event replay capability, store all incoming webhook events in your own database with the complete payload.
Q: How many webhooks can I create?
A: Asana has two webhook limits: (1) 10,000 webhooks per access token, and (2) 1,000 webhooks per resource. Note that Events API streams also count toward the per-resource limit. For large-scale integrations, consider using workspace or project-level webhooks with filters instead of creating webhooks on individual tasks. If you reach limits, audit your webhooks and delete unused ones via DELETE /webhooks/{webhook_gid}.
Next Steps & Resources
Now that you understand Asana webhooks, here's how to put your knowledge into practice:
Try It Yourself:
- Set up an Asana webhook following the step-by-step guide above
- Test your implementation with our Webhook Payload Generator to validate signature verification
- Deploy your webhook endpoint to production with proper monitoring and alerting
- Implement idempotency and error handling based on the production example
Additional Resources:
- Asana Official Webhook Documentation
- Asana Webhooks Guide (Setup and Handshake)
- Asana Events API Reference
- Asana API Explorer
- Our Complete Webhooks Guide
Related Guides:
- How to Verify Webhook Signatures: Complete Guide
- Testing Webhooks Locally with ngrok
- Webhook Security Best Practices
Asana Developer Resources:
- Asana Developer Console - Manage apps and tokens
- Asana Developer Forum - Community support
- Asana API Status Page - Service health monitoring
- Asana GitHub Examples - Official code samples
Need Help Testing?
- Use our Webhook Payload Generator to create test events with valid signatures
- Test handshake process locally before deploying to production
- Verify signature verification logic with known-good payloads
- Validate error handling for malformed or invalid webhook events
Conclusion
Asana webhooks provide a powerful way to build real-time integrations that respond instantly to task changes, project updates, and team collaboration. By following this guide, you now know how to:
- ✅ Set up Asana webhooks with proper handshake handling
- ✅ Verify webhook signatures securely using HMAC-SHA256
- ✅ Implement a production-ready webhook endpoint with queuing and error handling
- ✅ Handle common issues including duplicate events and webhook timeouts
- ✅ Test webhooks effectively with ngrok and our payload generator tool
Remember the key principles for production Asana webhook integrations:
- Always verify signatures for security using the
X-Hook-Secretfrom the handshake - Respond within 10 seconds to prevent Asana from timing out and retrying
- Process asynchronously using queues for reliability and fast response times
- Implement idempotency to handle duplicate events gracefully
- Handle heartbeat events to keep your webhook active and monitored by Asana
The unique handshake process with X-Hook-Secret ensures your endpoint is ready before events arrive, while compact event payloads keep delivery fast but require follow-up API calls for complete data. Leverage event propagation to monitor entire project hierarchies with minimal webhooks, and use filters to reduce noise and focus on events that matter to your integration.
Start building with Asana webhooks today, and use our Webhook Payload Generator to test your integration thoroughly before deploying to production.
Have questions or run into issues? Drop a comment below or contact us for personalized assistance with your Asana webhook integration.