Asana Integration
Receive and route Asana webhooks for tasks, projects, stories, and team changes.
Setup
1. Create a Source in Hookbase
Create it without a signing secret. Asana only issues the secret once the webhook exists, so it is added in step 3.
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Asana Production",
"slug": "asana",
"provider": "asana"
}'Until the secret is set, the source shows Awaiting Secret (awaitingSigningSecret: true in the
API): events are accepted but not verified.
Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/asana2. Create the Asana Webhook
Asana webhooks are created through its API, not through the Asana UI.
POST https://app.asana.com/api/1.0/webhooks- Set
resourceto the GID of the project, task or workspace you want to watch - Set
targetto your Hookbase ingest URL - Asana immediately sends a handshake request to
targetcarrying anX-Hook-Secretheader. Hookbase answers it automatically by echoing the header back, and Asana then creates the webhook - The
POST /webhooksresponse body includes that sameX-Hook-Secret. Copy it now: Asana webhooks have no dashboard, and later reads of the webhook do not return it
Info
Hookbase only echoes the handshake; it never saves the value from it. Anyone who knows an ingest URL can send a handshake-shaped request, so the secret is taken from Asana's API response, which only you receive.
See Asana's webhooks guide for the handshake and
the resource values it accepts.
3. Save the Secret on the Source
Paste the X-Hook-Secret value into the source (dashboard: Edit → X-Hook-Secret), or:
curl -X PATCH https://api.hookbase.app/api/sources/src_... \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"signingSecret": "your-x-hook-secret-value"}'From then on every delivery's X-Hook-Signature is verified.
4. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Task Sync", "slug": "asana-task-sync", "url": "https://api.myapp.com/webhooks/asana"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Asana to Task Sync", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
Asana signs webhooks with HMAC-SHA256 over the raw request body. The hex digest is sent in the
X-Hook-Signature header, with no prefix:
X-Hook-Signature: 1d1bd2b0f34c3b49d3e4e52b0d2d0a5f7f8e9c0b1a2d3e4f5061728394a5b6c7Hookbase verifies this automatically once the source has both fields set:
{
"provider": "asana",
"signingSecret": "your-x-hook-secret-value"
}The secret is the X-Hook-Secret value Asana issued for that webhook (returned by POST /webhooks) — not an
Asana personal access token, and not an OAuth token. There is no timestamp in this scheme, so nothing
expires and nothing needs a clock tolerance.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
Common Events
| Event | Description |
|---|---|
task.changed | A task was changed |
task.added | A task was added to a project |
task.removed | A task was removed from a project |
task.deleted | A task was deleted |
project.changed | A project was changed |
story.added | A comment or story was added |
Asana batches events: one request carries an events array, and each entry describes one change.
Task Changed
{
"events": [
{
"user": {
"gid": "12345",
"resource_type": "user"
},
"resource": {
"gid": "67890",
"resource_type": "task",
"name": "Implement auth flow"
},
"parent": {
"gid": "11111",
"resource_type": "project",
"name": "Sprint 24"
},
"action": "changed",
"type": "task",
"created_at": "2024-01-15T10:30:00.000Z",
"change": {
"field": "completed",
"action": "changed",
"new_value": {
"completed": true
}
}
}
]
}Task Added
{
"events": [
{
"user": {
"gid": "12345",
"resource_type": "user"
},
"resource": {
"gid": "22222",
"resource_type": "task",
"name": "Write API docs"
},
"parent": {
"gid": "11111",
"resource_type": "project",
"name": "Sprint 24"
},
"action": "added",
"type": "task",
"created_at": "2024-01-15T11:00:00.000Z"
}
]
}Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
One Row Per Event
function transform(payload) {
return (payload.events || []).map(e => ({
action: e.action,
resource_type: e.type,
resource_gid: e.resource ? e.resource.gid : null,
resource_name: e.resource ? e.resource.name : null,
parent_gid: e.parent ? e.parent.gid : null,
actor_gid: e.user ? e.user.gid : null,
changed_field: e.change ? e.change.field : null,
created_at: e.created_at
}));
}Slack Notification for Completed Tasks
function transform(payload) {
const completed = (payload.events || []).filter(
e => e.change && e.change.field === "completed"
);
return {
text: completed.length
? `${completed.length} Asana task(s) updated`
: "Asana update",
blocks: completed.map(e => ({
type: "section",
text: {
type: "mrkdwn",
text: `*${e.resource ? e.resource.name : e.resource_gid}* in ${e.parent ? e.parent.name : "a project"}`
}
}))
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Task Events Only
{
"name": "Tasks Only",
"logic": "AND",
"conditions": [
{
"field": "events.0.type",
"operator": "equals",
"value": "task"
}
]
}One Project
{
"name": "Sprint 24 Only",
"logic": "AND",
"conditions": [
{
"field": "events.0.parent.gid",
"operator": "equals",
"value": "11111"
}
]
}Info
Because Asana batches changes into one events array, a condition on events.0.* tests the first
entry only. When a single request can mix resource types, filter downstream in a transform or in your
destination handler instead.
Headers
| Header | Description |
|---|---|
X-Hook-Signature | Hex HMAC-SHA256 of the raw body, no prefix |
X-Hook-Secret | Sent on the one-time handshake request only, never on event deliveries |
Hookbase does not forward these headers to your destination. A delivery is a fresh request:
Hookbase sets Content-Type, User-Agent: Hookbase/1.0, X-Delivery-ID and X-Event-ID, then
adds the headers and auth you configured on the destination. Nothing Asana sent reaches your
handler as a header — read what you need out of the body, or set it on the destination yourself.
Of the incoming headers, only content-type, user-agent, x-github-event, x-gitlab-event and
stripe-signature are stored on the event, so those are the only ones visible later in the
dashboard or the API.
Troubleshooting
The Webhook Never Gets Created
Asana refuses to create a webhook whose target does not echo X-Hook-Secret on the handshake.
Hookbase echoes it only on a source whose provider is asana, so check that first. Also check
the source is active and not IP-filtered, so that Asana's request is not refused before the
handshake is answered.
Signature Verification Failed
- Confirm the source's
providerisasana— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, and Asana sendsX-Hook-Signature - Confirm
signingSecretis theX-Hook-Secretvalue from thePOST /webhooksresponse for this webhook — each Asana webhook gets its own - Recreating a webhook issues a new secret; update the source when you do
Duplicate Events
Hookbase has no provider event-id extractor for Asana, so the default auto dedup strategy falls back
to hashing the payload. Identical batches inside the dedup window collapse into one; batches differing
by even a timestamp do not.