Asana Webhook: Example Payload, Headers and Signature

Asana hands you the signing secret instead of asking for one: it arrives once, in a header, during a handshake your endpoint has to answer. Here is that handshake, the compact event list that follows, and how the two fit together.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Asana, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

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

HeaderExampleWhat it is
X-Hook-Signature1d6f0b8f3c2a4e5d7b9a0c1e2f3a4b5c6d7e8f9a0b1…HMAC-SHA256 of the body, hex, with no prefix. On every delivery after the handshake.
X-Hook-Secretb537207f20cbfa02357cf448134da559e8bd39d6159…Sent only on the handshake request: the secret that will sign later deliveries.
Content-Typeapplication/jsonThe 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

  1. Press Create webhook URL at the top of this page and copy the address.
  2. Create a personal access token in Asana's developer console and pick a test project; its gid is the number in the project's URL.
  3. Send POST https://app.asana.com/api/1.0/webhooks with an Authorization: Bearer header and the JSON body {"data":{"resource":"<project gid>","target":"<the address>"}}.
  4. The handshake request appears in the inbox above with its X-Hook-Secret header, and the API call returns the new webhook.
  5. 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 404 from 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

Other webhooks signed with an HMAC of the body

References

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.