Asana webhook handshake failing? Here's what it wants

Asana's webhook creation is a two-request dance that fails in ways no other provider prepares you for. When you call POST /webhooks, Asana — before your create call returns — sends a POST to your target URL carrying an X-Hook-Secret request header. Your endpoint must echo that exact value back as a response header (with a 200 or 204) or the create call fails. Not in the body. Not later. A response header, right now.

The handshake, step by step

  1. You call POST /webhooks with a resource gid and your target URL.
  2. Asana immediately POSTs to your target with header X-Hook-Secret: <random value> and an empty-ish body.
  3. Your endpoint responds 200 with response header X-Hook-Secret: <the same value>.
  4. Only then does your original create call return 201.
  5. That secret is now the HMAC key for every future delivery — and it is never shown again (fetching the webhook object won't return it). Store it during the handshake or lose it.

Why it fails

  1. Your server can't take a request while making one. The handshake arrives while your create call is still blocking. Single-threaded dev servers (one worker, sync frameworks, a REPL script) can't answer the incoming POST until the outgoing request finishes — which never happens, because Asana is waiting on the echo. The create call times out and everyone blames the network.
  2. Echoing in the body instead of a header. The spec is a response header. Frameworks make body-echo easy and header-echo obscure, so this is the most common code bug.
  3. Middleware strips or renames the header. Proxies and serverless platforms that normalize response headers can drop custom ones — verify what actually leaves your edge, not what your handler set.
  4. The endpoint 404s/500s. Deploy the route before creating the webhook; Asana needs a live answer during the create call.

The zero-code way to pass it

A Hookden bin can complete the handshake without any code of yours, because bins support templated custom response headers. In your bin's settings, add one response-header line:

X-Hook-Secret: {{header.x-hook-secret}}

That echoes whatever X-Hook-Secret value arrives straight back as a response header — the handshake passes, the webhook is created, and the handshake request (secret included) sits captured in your dashboard so you can store the secret before it's gone forever. From there, relay deliveries to localhost while you build the real handler.

Verifying deliveries after the handshake

Every later delivery carries X-Hook-Signature: bare hex HMAC-SHA256 of the raw body, keyed with the handshake secret. No sha256= prefix, no timestamp. Set your bin's scheme to generic with header x-hook-signature for live ✓/✗ badges, or use the signature debugger. A realistic payload to poke at: Asana task-changed example (its signature verifies).

The rules that bite you later

Related: Slack URL verification, Zoom CRC and Meta's hub.challenge — the other setup-handshake families (Asana is the only one that answers in a response header) — and the retry-schedule comparison.

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site