Skip to main content
All posts
Developers7 min read

How to Send an Asana Task Completion Alert to Your Team's Phones

Verify an Asana project webhook, check the task's latest completion state, and send one privacy-minimal alert to a shared PingRoom Room.

Two PingRoom avatars move one plain task tile into a completion tray while Pingo watches from the project table.

To send an Asana task-completion alert to a shared group of phones, subscribe a small server-side relay to one Asana project, verify Asana's webhook signature, fetch the task's current completed state, and post fixed copy to a PingRoom incoming webhook. Keep the task name, description, assignee, project name, and access token out of the phone alert.

This is a developer webhook recipe, not a native Asana integration. It is useful when one completion state should reach a small Room that includes people who do not work in Asana.

Use Asana notifications first when they fit

Asana already has an Inbox and mobile apps. Its Inbox guidance covers per-project notification defaults, email, browser and push controls, filters, and quiet periods. The mobile-app guide shows how people can open a notification and act on the task in Asana.

Use that route when recipients already work in Asana and need the task context there.

NeedBetter starting point
Assigned people and followers need normal task contextAsana Inbox and mobile notifications
One selected completion should reach a separate shared attention channelAsana project webhook → trusted relay → PingRoom Room
Every change must be accounted for in a durable recordApplication-owned reconciliation and audit system
The event is urgent, safety-critical, or regulatedThe approved pager, record, and escalation path

Asana documents its webhook stream as at most once. A Ping is an attention signal, not an audit ledger or proof that the work was completed correctly. Asana remains the task record.

Why this event needs a relay

Asana's webhook guide describes three boundaries that matter here:

  • Creating a webhook starts a synchronous X-Hook-Secret handshake with a public HTTPS endpoint.
  • Later deliveries include X-Hook-Signature, an HMAC-SHA256 signature over the full raw request body.
  • Events are compact change notices. Integrations are expected to fetch the current resource state before acting.

A PingRoom incoming webhook does not implement Asana's handshake or signature protocol. Put a relay between them. The relay verifies the sender, selects only completion events, reads the latest task state, and forwards one bounded sentence.

Create the destination Room

Create a private PingRoom Room for the people who need the completion signal. The Room owner needs PingRoom Pro for incoming webhooks.

In the PingRoom browser app, open the Room's Settings → Connections → From another app, add an incoming webhook, and store its full URL as a protected server-side secret. Do not place it in Asana, browser code, source control, screenshots, or logs.

The incoming-webhook guide explains the request and recipient outcome. The webhook reference carries the current field limits, cooldown behavior, idempotency rules, and error responses.

Use fixed copy for the first version:

  • Title: Task completed
  • Message: One Asana task is complete. Open Asana for details.

That message gives the Room a useful state without exposing a client name, task title, project, assignee, or link on a lock screen.

Deploy a signature-verifying receiver

Store these values in server-side configuration:

  • ASANA_ACCESS_TOKEN: a token that can read tasks in the selected project
  • ASANA_HOOK_SECRET: the secret returned during webhook creation
  • PINGROOM_WEBHOOK_URL: the complete secret PingRoom URL

For a personal experiment, Asana calls a personal access token the quickest authentication option. Its authentication guide says the token has the same Asana access as the person who created it. Use OAuth for an app used by other people, keep tokens opaque and secret, and grant only the access the integration needs.

This is the core of the receiver we tested:

import crypto from "node:crypto";

export async function handleAsanaWebhook(request, env) {
  const handshakeSecret = request.headers.get("x-hook-secret");
  if (handshakeSecret) {
    return new Response(null, {
      status: 200,
      headers: { "X-Hook-Secret": handshakeSecret },
    });
  }

  const raw = await request.text();
  const signature = request.headers.get("x-hook-signature") ?? "";
  const expected = crypto
    .createHmac("sha256", env.ASANA_HOOK_SECRET)
    .update(raw)
    .digest("hex");

  if (!safeEqual(signature, expected)) {
    return Response.json({ error: "invalid_signature" }, { status: 401 });
  }

  const payload = JSON.parse(raw);
  for (const event of payload.events ?? []) {
    if (
      event.resource?.resource_type !== "task" ||
      event.action !== "changed" ||
      event.change?.field !== "completed"
    ) {
      continue;
    }

    const taskResponse = await fetch(
      `https://app.asana.com/api/1.0/tasks/${encodeURIComponent(event.resource.gid)}?opt_fields=completed,completed_at`,
      { headers: { Authorization: `Bearer ${env.ASANA_ACCESS_TOKEN}` } },
    );
    if (!taskResponse.ok) throw new Error("Asana task lookup failed");

    const { data: task } = await taskResponse.json();
    if (!task.completed || !task.completed_at) continue;

    const eventKey = crypto
      .createHash("sha256")
      .update(`${event.resource.gid}\n${task.completed_at}`)
      .digest("hex");

    const pingResponse = await fetch(env.PINGROOM_WEBHOOK_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": `asana-${eventKey}`,
      },
      body: JSON.stringify({
        title: "Task completed",
        message: "One Asana task is complete. Open Asana for details.",
        correlation_id: `asana:${eventKey}`,
      }),
    });
    if (!pingResponse.ok) throw new Error("PingRoom rejected the event");
  }

  return Response.json({ accepted: true });
}

function safeEqual(left, right) {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The full receiver should also reject oversized or malformed bodies, log destination failures without logging secrets, and keep the task lookup and PingRoom response statuses. The version recorded with this article includes those checks.

The idempotency key uses the task GID and Asana's current completed_at value. An exact replay for the same completion produces the same key and body. Reopening and completing the task later gives it a new completion time and a new event identity.

Create one project-scoped subscription

Deploy the receiver before creating the subscription. Asana's create-webhook reference requires a public target that can answer the handshake while the creation request is still open. localhost is not a valid target.

Replace the two placeholders below and run the request from a trusted shell. Do not paste a real token into shared history.

curl --fail-with-body --silent --show-error \
  -X POST "https://app.asana.com/api/1.0/webhooks" \
  -H "Authorization: Bearer $ASANA_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "resource": "<PROJECT_GID>",
      "target": "https://<YOUR_RELAY>/asana",
      "filters": [
        {
          "resource_type": "task",
          "action": "changed",
          "fields": ["completed"]
        }
      ]
    }
  }'

The response includes X-Hook-Secret. Put that value into your relay's secret store as ASANA_HOOK_SECRET, then restart or redeploy the receiver before changing a test task. Do not print it in application logs.

The filter keeps unrelated field changes out of the subscription. The receiver still checks the event shape and fetches the task's latest state, because a compact change event is not the authoritative task snapshot.

Two PingRoom avatars watch a sealed project box stay closed while one verified signal token passes a miniature receiver gate beside Pingo.

These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer. They are not Asana or PingRoom screenshots and do not depict customers.

Pingo is watching the project box remain closed. He has no Asana token, private-Room access, or authority to complete a task or send a Ping. The human team chooses the project, receiver, secrets, recipients, and copy.

Test the boundary with a harmless task

Use a test project and a generic task with no private content.

  1. Create the webhook and confirm the relay echoes the X-Hook-Secret handshake.
  2. Complete the test task and confirm exactly one Task completed Ping appears.
  3. Replay the same signed event and confirm the same idempotency key and response are reused.
  4. Reopen the task and confirm the receiver sends nothing for the incomplete state.
  5. Send an invalid signature and confirm the relay returns 401 without calling Asana or PingRoom.
  6. Send an empty events array and confirm the heartbeat returns success without creating a Ping.
  7. Inspect the outgoing PingRoom request and confirm it contains no task name, description, assignee, project, user, or Asana token.
  8. Rotate the Asana hook secret, Asana access token, and PingRoom webhook secret separately; confirm old values stop working.

For this article, a representative signed Asana event passed through the recorded receiver and a local mock of PingRoom's documented contract. The check covered the handshake, valid and invalid signatures, current completion-state lookup, exact replay, changed-body conflict, reopened-task suppression, heartbeat handling, and the privacy-minimal outbound body. It used no live Asana workspace, PingRoom Room, phone, or notification receipt.

Preserve the record and the failure path

Asana batches events and can send an empty heartbeat. It retries failed webhook deliveries, but its documented event system is still at most once. If every completion must be accounted for, reconcile against Asana on a schedule and keep an application-owned durable record. Do not describe the Room as complete coverage.

Return a successful response only after you have either sent the Ping or placed the event in a durable queue. Keep the Asana task and your relay logs as the operational record. If the alert is missing, inspect the Asana webhook status, receiver logs, task state, and PingRoom response rather than treating Room silence as proof that no task was completed.

PingRoom also applies webhook cooldown and rate limits. If many tasks can complete together, queue them, send only chosen milestones, or use a digest. A Room alert for every checklist item becomes another feed to ignore.

HTTP 200 from PingRoom means the event was accepted and entered PingRoom's normal path. It does not guarantee what a particular phone displayed under its current connectivity, Focus mode, notification permission, or Room settings.

If one project completion deserves a separate shared signal, create the private Room and incoming webhook, deploy the receiver with generic test data, and keep the work details in Asana.

Asana is a trademark of its owner. PingRoom is not affiliated with or endorsed by Asana.

PingRoom

Short-form communication.

View all posts