How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Asana webhook?
An Asana webhook is an HTTP POST sent to a target URL when something changes inside a resource you subscribed to, most often a project, a portfolio or a single task. Webhooks are created through the API with a personal access token or an OAuth token; there is no screen for them in the Asana app. Each one watches a single resource and can carry filters that narrow it to certain resource types, actions and fields.
The body is a list of compact events. Each says who did something, when, what kind of action it was (added, changed, removed, deleted or undeleted) and which resource it happened to, identified by gid and type. For a change it also names the field. What it does not contain is the resource itself: you read the task or story from the API when you need its content.
What a Asana webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Hook-Signature: 1d6f0b8f3c2a4e5d7b9a0c1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
X-Hook-Secret: b537207f20cbfa02357cf448134da559e8bd39d61597dcd5631b8012eae53e81
Content-Type: application/json
{
"events": [
{
"user": {
"gid": "1199999999999991",
"resource_type": "user"
},
"created_at": "2026-01-01T12:00:00.000Z",
"action": "changed",
"resource": {
"gid": "1201234567890123",
"resource_type": "task",
"resource_subtype": "default_task"
},
"parent": null,
"change": {
"field": "due_on",
"action": "changed",
"new_value": "2026-01-09"
}
},
{
"user": {
"gid": "1199999999999991",
"resource_type": "user"
},
"created_at": "2026-01-01T12:00:04.000Z",
"action": "added",
"resource": {
"gid": "1201234567890456",
"resource_type": "story",
"resource_subtype": "comment_added"
},
"parent": {
"gid": "1201234567890123",
"resource_type": "task",
"resource_subtype": "default_task"
}
}
]
}
Identifiers are gid strings; treat them as opaque text even though they look numeric. parent is set for actions that relate two resources, such as a comment (a story) being added to a task. Several events usually arrive together, and a body of {"events":[]} is not an error: it is a heartbeat, sent periodically to check that the endpoint is still there.
Headers Asana sends
| Header | Example | What it is |
|---|---|---|
X-Hook-Signature | 1d6f0b8f3c2a4e5d7b9a0c1e2f3a4b5c6d7e8f9a0b1… | HMAC-SHA256 of the body, hex, with no prefix. On every delivery after the handshake. |
X-Hook-Secret | b537207f20cbfa02357cf448134da559e8bd39d6159… | Sent only on the handshake request: the secret that will sign later deliveries. |
Content-Type | application/json | The body is JSON. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Asana signs a webhook
The secret comes from Asana. While your API call to create the webhook is still in progress, Asana posts to the target with an X-Hook-Secret header, and the endpoint must store that value: it is never sent again. Every later delivery carries X-Hook-Signature, the hex HMAC-SHA256 of the request body keyed with that secret.
Because the secret is delivered rather than configured, losing it means deleting the webhook and creating it afresh. Keep one secret per webhook, since each handshake issues a different one. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// hookSecret: the X-Hook-Secret value stored during this webhook's handshake
// rawBody: Buffer or string of the body exactly as received
function verifyAsana(rawBody, header, hookSecret) {
const expected = crypto.createHmac('sha256', hookSecret)
.update(rawBody).digest('hex');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The handshake, heartbeats and retries
Creating a webhook is a conversation between three requests. You call the API; before it answers, Asana posts to your target with X-Hook-Secret; your endpoint must reply 200 or 204 with the same header and value in its response; only then does your original API call return success. If the echo is missing or slow, the creation call fails. The webhook URLs created on this site return the header for you, and the handshake request stays in the inbox, so you can read the secret from its headers and use it to check later signatures.
After that, Asana expects a 2xx for each delivery and retries failures with a growing delay. Per its documentation at the time of writing, a webhook whose deliveries keep failing for about a day is deleted, and heartbeats with an empty event list arrive every few hours so that a dead endpoint is noticed even when nothing is changing. Events are delivered at least once and not strictly in order.
Catch a real Asana webhook
- Press Create webhook URL at the top of this page and copy the address.
- Create a personal access token in Asana's developer console and pick a test project; its
gidis the number in the project's URL. - Send
POST https://app.asana.com/api/1.0/webhookswith anAuthorization: Bearerheader and the JSON body{"data":{"resource":"<project gid>","target":"<the address>"}}. - The handshake request appears in the inbox above with its
X-Hook-Secretheader, and the API call returns the new webhook. - Change a task in the project and read the events. Delete the webhook with
DELETE /webhooks/<webhook gid>when you have finished.
The creation request can be sent from the request console on this site. Anyone who holds the inbox URL can read the handshake and therefore the secret, which is one more reason to do this with a throwaway project.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- The echo goes in a response header, not the body. Returning the secret as JSON does not complete the handshake.
- One change produces several events. Completing a task fires a change on the task and adds a story; without filters a busy project is noisy.
- Events are pointers. By the time you fetch the resource it may have changed again, or be gone; handle a
404from the follow-up request.
api-tester.com is independent and is not affiliated with Asana. Providers change header names, limits and retry schedules; treat the official Asana documentation as the source of truth.
Related examples and tools
Okta Webhook Example
An Okta event hook delivers System Log events to your endpoint, authenticated by a header and secret you choose rather than by a signature.
Stripe Webhook Example
What Stripe actually sends to your endpoint — the event envelope, the Stripe-Signature header and the retry behaviour — with a webhook URL you can create here to catch a real one.
GitHub Webhook Example
The headers and JSON GitHub posts when a repository event fires, how X-Hub-Signature-256 is computed, and what happens when your endpoint is down.
PayPal Webhook Example
The notification PayPal posts when a payment event happens — its envelope, the five PAYPAL- headers, and the certificate-based signature that works differently from everyone else's HMAC.
Shopify Webhook Example
What Shopify posts when a store event fires — the topic and shop headers, the resource JSON, and a base64 HMAC that trips up people expecting hex.
Bearer Tokens
How the Authorization header used to create the webhook works.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official Asana documentation — The provider's own guide to the requests described on this page.
- Asana API reference: establish a webhook — The request that starts the handshake.
- Asana API reference: webhooks — The webhook object and its endpoints.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Asana webhook FAQ
What does an Asana webhook payload look like?
A JSON object with an events array. Each event has the user who acted, created_at, an action such as changed or added, the resource as a gid and resource_type, an optional parent, and for changes a change object naming the field.
How does the Asana webhook handshake work?
When you create a webhook, Asana sends a POST to the target with an X-Hook-Secret header. The endpoint must respond with status 200 or 204 and the same X-Hook-Secret header, and should store the value for verifying signatures.
How do I verify the X-Hook-Signature header?
Compute an HMAC-SHA256 of the raw request body using the X-Hook-Secret value received during the handshake, hex-encode it and compare it with the header in constant time.
Why does Asana send a webhook with no events?
It is a heartbeat. Asana periodically posts an empty events array to confirm the endpoint still responds. Answer it with a 2xx like any other delivery.
How do I create an Asana webhook?
Send POST /webhooks to the Asana API with an access token and a body containing the resource gid to watch and the target URL, optionally with filters for resource types, actions and fields.