Asana Integration
Receive and route Asana webhooks for tasks, projects, stories, and team changes.
Setup
1. Create a Source in Hookbase
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",
"signingSecret": "your-asana-webhook-secret"
}'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 the URL that will receive the events - Asana immediately sends a handshake request to
targetcarrying anX-Hook-Secretheader, and creates the webhook only if the response echoes that same header back - The value from the handshake is the secret every later request is signed with — store it on your
Hookbase source as
signingSecret
Warning
Hookbase's ingest endpoint does not echo X-Hook-Secret. There is no handler for that header
anywhere in the ingest path, and the ingest response carries only Hookbase's own headers. Pointing
Asana's target straight at a Hookbase ingest URL therefore fails at step 4 — Asana never finishes
creating the webhook, so no events are ever sent.
The handshake has to be answered by an endpoint you control. Once you hold the secret from that handshake, everything below applies: Hookbase verifies Asana's signature from it exactly as Asana computes it.
See Asana's webhooks guide for the handshake and
the resource values it accepts.
3. 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-asana-webhook-secret"
}The secret is the value Asana sent in the X-Hook-Secret handshake header for that webhook — 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. See the
warning under Create the Asana Webhook — Hookbase's ingest endpoint does not answer that
handshake.
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 the handshake secret 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.