Overview

Webhooks allow an application to be notified of changes in Asana.

This is similar to our events resource, but webhooks "push" events via HTTP POST rather than expecting integrations to repeatedly "poll" for them. For services that are already accessible on the internet, this is often more convenient and efficient.

However, webhooks require a server to be accessible over the internet at all times to receive the event. For most simple integrations, events provide much of the same benefits while using a significantly simpler implementation which does not require maintaining an internet-accessible server.

Webhooks and events streams are served from the same infrastructure, where, on average, events are delivered within a minute of occurring, and most should be delivered within 10 minutes of occurring. Review Timing for more information.

This system is designed for at-most-once delivery, meaning in exceptional circumstances we may fail to deliver events. Furthermore, webhooks cannot be replayed once delivered. For these reasons, if your use case requires strong guarantees about processing all changes on a resource and cannot tolerate any missing events, regardless of how rare that might be, we recommend building a fallback polling system that fetches the resource periodically as well. Note that, if your server does not respond to a webhook with a successful HTTP status code within 10 seconds, Asana will try to resend the webhook for up to 24 hours before giving up.


Setting up a webhook

In order to work with Asana webhooks, you must create and host a server that can receive and handle webhook events. This server must expose an HTTP endpoint (i.e., a publicly-accessible URL) to function as the reception and processing point for all incoming events. During the development and testing process, if you set up such a server on your local machine (i.e., a localhost service), you may choose to use a "tunneling" tool such as ngrok to expose that local server to the internet.

📘

Example webhook servers

The following are example webhook servers that demonstrates the features of Asana webhooks. Each example server includes how to handle the webhook handshake as well as how to receive and verify webhook events.

To try out the demo out for yourself, be sure to generate a new personal access token, then follow the instructions in the README for your language of choice:

The following video tutorial walks you through the process of:

  1. Setting up an example webhook server [1:58]
  2. Setting up business logic for the webhook handshake [3:45]
  3. Establishing a webhook on an Asana resource [6:45]
  4. Verifying and receiving webhook events[9:02]

You can find the tutorial source code here.

The webhook handshake

📘

Server requirement

Your server must be capable of handling the outgoing create request while simultaneously receiving and processing incoming requests. A common reason for failed webhook handshakes is that a given server was unable to asynchronously manage the handshake request.

The webhook handshake is a crucial step in establishing a secure connection between your webhook server and Asana. Here is a brief overview of the handshake:

The webhook handshake, summarized

The webhook handshake

Each of the above numbered steps are described in detail:

  1. Before you can start receiving webhook events for an Asana resource, you must first establish a webhook connection.
  2. To verify that your server is ready to accept incoming events, Asana will make a POST request to the user-specified target URI from the webhook creation request. This means that your outgoing webhook creation request (from the previous step) will remain pending until Asana's servers successfully complete this POST request. Only after this "handshake" is successful will your webhook creation request return a successful response (i.e., 201 Created).
  3. Included in the header of that POST request from Asana is an X-Hook-Secret. This value serves as a shared secret that both Asana and the receiving server store. For future webhook events, Asana will use this value to compute a signature over the webhook callback request's body, allowing you to verify the authenticity of the incoming request. We strongly recommend utilizing this security feature and rejecting any webhooks with an invalid signature.
  4. To successfully complete the handshake, your webhook server must echo back an X-Hook-Secret header of its own with the same value. Your webhook server must also return a 200 OK or 204 No Content response status code.
  5. Once the handshake has completed, Asana returns a 201 Created response as the webhook is successfully established. This response body will also include the original X-Hook-Secret. For example:
{
  "data": {
    "gid": "12345",
    "resource_type": "webhook",
    "active": false,
    "resource": {
      "gid": "12345",
      "resource_type": "task",
      "name": "Bug Task"
    },
    "target": "https://example.com/receive-webhook/7654",
    "created_at": "2012-02-22T02:06:58.147Z",
    "last_failure_at": "2012-02-22T02:06:58.147Z",
    "last_failure_content": "500 Server Error\\n\\nCould not complete the request",
    "last_success_at": "2012-02-22T02:06:58.147Z",
    "delivery_retry_count": 3,
    "next_attempt_after": "2012-02-22T02:06:58.147Z",
    "failure_deletion_timestamp": "2012-02-22T02:06:58.147Z",
    "filters": [
      {
        "resource_type": "task",
        "resource_subtype": "milestone",
        "action": "changed",
        "fields": [
          "due_at",
          "due_on",
          "dependencies"
        ]
      }
    ]
  },
  "X-Hook-Secret": "b537207f20cbfa02357cf448134da559e8bd39d61597dcd5631b8012eae53e81"
}

The secret should match the value returned in the X-Hook-Secret header from Step 2. We recommend that you verify this by comparing it with the X-Hook-Secret stored by your webhook server (Step 3). This ensures that the signing secret used for the webhook subscription comes from Asana, protecting against potential attacks. Note that X-Hook-Secret is not returned again when fetching the webhook subscription object .

Examples of the webhook handshake

For an example of how all of this might be implemented in code, see our sample webhook server implementation in Node.js and Python.

Filtering

Webhook events will "propagate up" from contained objects through to parent objects. For example, changes to comments will be sent to webhooks on the parent task and to ones on the task's projects. This way, a webhook on a project will be notified of all changes that occur in all of its tasks, subtasks of those tasks, and comments on those tasks and subtasks.

This can be a lot of data, some of which might not be relevant to a particular integration, so Asana's webhooks have a filtering feature which allows integrations to specify only the types of changes that they care about. By specifying the list of webhook filters on webhook creation, an integration can select just the subset of events it wants to receive. That is, when filters are specified on the webhook, events will only be delivered if they pass any of the filters specified when creating the webhook.

To reduce the volume of data to transfer, webhooks created on team, portfolio, goal or workspace must specify filters. In addition, the set of event filters that can be placed on a team-level or workspace-level webhook is more limited than filters for webhooks that are created on lower-level resources:

  • Webhook events from tasks, subtasks, and stories won't be propagated to these higher-level webhooks, so all changes on these resources are automatically filtered out.
  • Webhook events from project resources can be filtered for these actions: added, removed, deleted, undeleted, and changed.
  • Webhook events from team_membership resources can be filtered to actions added and removed.
  • Webhook events from goalresources can be filtered to added and removed.
  • Webhook events from workspace_membership resources can be filtered to added and removed.
  • Webhook events from portfolio resources can be filtered to added.

Actions

Actions define the type of action that was taken on the resource to trigger an event for your webhook. When you receive a webhook event, there will be an associated action in the event response that indicates the action that triggered the event. Additionally, actions are used in webhook filters. You can specify an action and a resource_type in the filters parameter when establishing a webhook so that you will only receive events matching the action specified for the resource in your filter.

The following is a list of actions that we support:

  • added - a new resource was created
  • changed - the resource was modified
  • deleted - the resource itself was deleted
  • removed - the resource was removed from a parent
  • undeleted - the deletion of the resource was undone

Resources and actions

Below is a list of resources and actions that can trigger an event for those resources. This is not an exhaustive list, but should cover the most common use cases.

  • Attachment - deleted, undeleted
  • Goal - added, changed, removed, deleted, undeleted
  • Portfolio - added, deleted, removed
  • Project - added, changed, deleted, removed, undeleted
  • Project Membership - added, removed
  • Project Template Configuration - added, deleted, removed
  • Project Template Configuration Membership - added, removed
  • Section - added, changed, deleted, undeleted
  • Story: type: <all other types> added, removed, undeleted
  • Story: type: comment added, changed, removed, undeleted
  • Tag - added, changed, deleted, undeleted
  • Task - added, changed, deleted, removed, undeleted
  • Team - added, changed, deleted
  • Team Membership - added, removed
  • Time Tracking Entry - added, changed, deleted
  • Workspace - changed
  • Workspace Memberships - added, removed

For example, let's say you establish a webhook for an attachment by providing the GID of an attachment in your resource parameter. This means that based on the resource and action definition for attachment above, a deleted or an undeleted action will trigger your attachment webhook.


Events

Receiving events

Because multiple events often happen in short succession, a webhook payload is designed to be able to transmit multiple events at once. The schema of these events is described in event.

The HTTP POST that the target receives contains:

  • An X-Hook-Signature header, which allows verifying that the payload is genuine. The signature is a SHA256 HMAC signature computed on the request body using the shared secret transmitted during the handshake . Verification is strongly recommended, as it would otherwise be possible for an attacker to POST malicious payload to the same endpoint.
  • A JSON body with a single key, events, containing an array of the events that have occurred since the last webhook delivery. Note: this list may be empty, as periodically we send a "heartbeat" webhook to verify that the endpoint is still available.

Note that events are "compact" and contain only some basic details of the change, rather than containing the entire resource. We expect integrations to make additional requests to the API to retrieve the latest state from Asana.

Timing

Events are delivered in under a minute on average. Most events are delivered within 10 minutes. In exceptional circumstances, an event may arrive after more than 10 min. If your service relies on real-time processing of Asana webhooks, we recommend that you examine the created_at field on events before processing to ensure the event is still relevant for your needs.

Story consolidation

When a task is updated, Asana may create a story (comment) and append it to its list of activities. However, it's common for a task to be quickly updated several times. If the same action occurs within one hour, the system may consolidate these stories so that only the final result appears in the list of activities. Without story consolidation, a task's activity list would quickly fill with all the in-between states.

For example, if a user moves a task from one section to another, they may see a story appear similar to:

Name moved this task from 'Section 1' to 'Section 2' in Project

However, if they decide a few moments later that it would be better to move the task to another section, the system may replace the previous story with a new story:

Name moved this task from 'Section 1' to 'Section 3' in Project

Story consolidation example: the first story is replaced with the second

Story consolidation example: the first story is replaced with the second


Why it matters

As a task is updated, the system generates webhook events. However, it is important to note that your service may receive an event for a story that no longer exists due to story consolidation. This awareness can help you better understand the system's behavior and adapt your integration logic accordingly.

Errors and retries

If we attempt to send a webhook payload and we receive an error status code, or the request times out, we will retry delivery with exponential backoff. In general, if your servers are not available for an hour, you can expect it to take no longer than approximately an hour after they come back before the paused delivery resumes. However, if we are unable to deliver a message for 24 hours, the webhook will be deleted.

Heartbeat events

Webhooks keep track of the last time that delivery succeeded, and this time is updated with each success (i.e, last_success_at). A delivery succeeds when your webhook server responds to a webhook event with a 200 OK or 204 No Content response code. To help facilitate this delivery succeeded tracking, webhooks have a “heartbeat” that will deliver an empty payload to your webhook server at the initial handshake , and then at every eight hours. This way, even if there is no activity on the resource, the last success time (i.e., last_success_at) will still be updated continuously.

Note that if we do not receive a response to a "heartbeat" after 24 hours, we will delete that webhook connection. This means that specific webhook route will not receive future events from Asana. If you make a request for that specific webhook (i.e., GET /webhooks/{webhook_gid}), that webhook will no longer be available.

Additionally, when a webhook's resource is deleted, Asana will automatically delete the webhook in 72 hours.

Scenario 1: successful heartbeat (after initial handshake)

Scenario 2: failed heartbeat (after initial handshake)


Security

In order to receive webhook events, your webhook endpoint needs to be accessible over the internet. As such, because your webhook endpoint is publicly accessible over the internet, it is vulnerable to receiving events that are not from Asana. To ensure that your webhook endpoint receives events from Asana, it is important to verify each request.

Request verification can be done by comparing an HMAC signature generated from the X-Hook-Secret and request body to the X-Hook-Signature. When you first establish a webhook with Asana, we send your webhook endpoint an X-Hook-Secret. Once received, this X-Hook-Secret should be stored by your webhook server.

When a webhook event is triggered, Asana sends along a X-Hook-Signature in the request header. At this point, your webhook server should extract the body from the request and use it along with the stored X-Hook-Secret to generate an HMAC signature. HMAC signatures can be generated using a HMAC library in your language of development and passing in the X-Hook-Secret as the secret and in the request body as the message. The generated HMAC signature should match the X-Hook-Signature sent in the request header. If these signatures differ, it can indicate that the event received was not from Asana.

To read more about our X-Hook-Signature see receiving events. For examples of servers configured to verify HMAC signatures, see our example webhook servers.

Permissions

Understanding webhooks permissions is essential for effective webhook management.

Webhook subscriptions can only be created by users who have access to the specific object (e.g., task). If a user loses this access—such when the user leaves the domain—the existing webhook will remain but will stop receiving events. No events will be received until access is restored.

Webhooks are tied to the authorization token (e.g., personal access token, or PAT) used for their creation. If this token is deactivated or deleted, the associated webhook will also be removed, ensuring that authorization is properly maintained. Furthermore, if the user or PAT that established the webhook is deleted, the webhook will be trashed and will no longer count against the domain's webhook limit.


Limits

Webhooks have two different limits:

  • 1,000 webhooks per resource. For example, if 10 apps/users each have 100 webhooks watching the same resource, no additional webhooks can be established on that particular resource. Note that /events streams also count toward this limit
  • 10,000 webhooks per token. That is, a single token can have 10,000 active webhooks at any one time

📘

Managing webhooks limits

If you no longer require a webhook to be active, please send a HTTP 410 response when you receive a webhook event or webhook heartbeat event. This lets us know that you no longer want to receive webhook events for this particular webhook and we will delete this webhook.

Alternatively, you may send a (DELETE /webhooks/{webhook_gid}) request at anytime.


Troubleshooting

Webhook stopped receiving events

This can happen when your registered webhook endpoint ignores incoming heartbeat events. We send periodic heartbeat events to your webhook endpoint every 8 hours to keep track of the last time that delivery succeeded. If we receive no response to heartbeat events after 24 hours, we will delete the registered webhook connection.

To fix this, you will need to modify your webhook endpoint code to respond to heartbeat events and re-establish a webhook connection.

Computed webhook signature differs from X-Hook-Signature

When computing your SHA256 HMAC signature, make sure to utilize the X-Hook-Secret and the full body of the request. The X-Hook-Secret can be found in the header of the initial request sent to your webhook endpoint during the initial handshake process. The full body of the request should also be used, not just a portion of the data inside the request body.

Additionally, check the documentation of the library you are using to compute the SHA256 HMAC signature, as it may require you to convert the request body from an object into a string.

Webhook delivery status indicators

When you make a request to GET /webhooks/{webhook_gid}, the 200 response includes information to help you monitor and manage webhook delivery attempts. Understanding these properties can greatly assist in troubleshooting and ensuring the reliability of your webhook integrations:

PropertyDescription
last_failure_atThe timestamp when the webhook last received an error when sending an event to the target.
last_failure_contentThe contents of the last error response sent to the webhook when attempting to deliver events to the target.
last_success_atThe timestamp when the webhook last successfully sent an event to the target.
delivery_retry_countThe number of times the webhook has retried delivery of events to the target (resets after a successful attempt).
failure_deletion_timestampThe timestamp when the webhook will be deleted if there is no successful attempt to deliver events to the target
next_attempt_afterThe timestamp after which the webhook will next attempt to deliver an event to the target.

Sample 200 response:

{
  "data": {
    "gid": "12345",
    "resource_type": "webhook",
    "active": false,
    "resource": {
      "gid": "12345",
      "resource_type": "task",
      "name": "Bug Task"
    },
    "target": "https://example.com/receive-webhook/7654",
    "created_at": "2012-02-22T02:06:58.147Z",
    "last_failure_at": "2012-02-22T02:06:58.147Z",
    "last_failure_content": "500 Server Error\\n\\nCould not complete the request",
    "last_success_at": "2012-02-22T02:06:58.147Z",
    "delivery_retry_count": 3,
    "next_attempt_after": "2012-02-22T02:06:58.147Z",
    "failure_deletion_timestamp": "2012-02-22T02:06:58.147Z",
    "filters": [
      {
        "resource_type": "task",
        "resource_subtype": "milestone",
        "action": "changed",
        "fields": [
          "due_at",
          "due_on",
          "dependencies"
        ]
      }
    ]
  }
}