# Favro Webhooks

## When to Use This Skill

* How do I receive Favro webhooks?
* How do I verify the Favro `X-Favro-Webhook` signature?
* Why is my Favro webhook signature verification failing?
* How do I respond to the Favro setup ping so the webhook validates?
* How do I handle `card.created`, `card.committed`, `card.moved`, `card.updated`, `card.deleted` events?
* How do I handle `comment.created`, `comment.updated`, `comment.deleted` events?

## How Favro Webhooks Work (Read This First)

Favro does not use the [Standard Webhooks](https://www.standardwebhooks.com/)
spec. Its signature scheme is unusual in one critical way: the signed message
is not the request body. From the [Favro developer docs](https://favro.com/developer/#webhook-signatures):

> The header is a base64 digest of an HMAC-SHA1 hash. The hashed content is the
> concatenation of the `payloadId` and the URL exactly as it was provided during
> webhook creation. The key used to sign this text is the secret you entered when
> setting up the webhook.

So verification requires three inputs — and the body is not one of them:

```
X-Favro-Webhook = base64( HMAC-SHA1( key = secret, message = payloadId + webhookUrl ) )
                                                              └─ from body ─┘  └─ from config ─┘

```

Two consequences drive everything below:

1. You must know the exact URL you registered. The `webhookUrl` in the HMAC is
  the `postToUrl` you gave Favro verbatim — same scheme, host, path, trailing
  slash, and query string. Store it as an env var (`FAVRO_WEBHOOK_URL`) and keep
  it byte-identical to what you registered, or every signature will mismatch.
2. The `payloadId` comes from the JSON body. Every delivery (including the
  setup ping) carries a top-level `payloadId` string. Parse it out, concatenate
  `payloadId + webhookUrl`, and HMAC that — not the body bytes.

```
Favro ──POST {"payloadId":"…","action":"…", …}──▶ your endpoint
              X-Favro-Webhook: <base64 HMAC-SHA1>      │  expected = base64(HMAC-SHA1(secret, payloadId + FAVRO_WEBHOOK_URL))
                                                        ▼  timing-safe compare to header
                                          valid? → dispatch on action → 200
                                          ping?  → 200 (validates the webhook)

```

## Verification (core)

Sign `payloadId + webhookUrl` with your webhook secret using HMAC-SHA1,
base64-encode, and compare to the `X-Favro-Webhook` header with a timing-safe
compare. The signed message is not the raw body — do not HMAC the body.

```javascript
const crypto = require('crypto');

// X-Favro-Webhook = base64( HMAC-SHA1( secret, payloadId + webhookUrl ) )
// payloadId comes from the JSON body; webhookUrl is the URL you registered, verbatim.
function verifyFavroWebhook(payloadId, webhookUrl, secret, signature) {
  if (!payloadId || !webhookUrl || !secret || !signature) return false;
  const expected = crypto
    .createHmac('sha1', secret)
    .update(payloadId + webhookUrl, 'utf8')
    .digest('base64');
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  } catch {
    return false; // different lengths => invalid
  }
}

```

There is no official Favro SDK, so verification is manual in every language (the
community Node package [`@bscotch/bravo`](https://github.com/bscotch/bravo)
implements the same scheme). Parse the body only to read `payloadId`; the
signature does not cover the body, so re-serialization is not a concern here.

> For complete handlers with route wiring, event dispatch, ping handling, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/favro-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/favro-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/favro-webhooks/examples/fastapi/)

## The Setup Ping

When a webhook is created, Favro sends a ping to validate the endpoint. Your
handler must return a `2xx` or the webhook stays unvalidated. The ping carries a
`payloadId`, so it is signed with the same scheme — verify it like any other event
and return `200`:

```json
{ "payloadId": "AbCdEf==", "action": "ping", "hookId": "abc123", "hook": { "url": "https://example.com/webhooks/favro" } }

```

If your ping fails verification during setup, the cause is almost always that
`FAVRO_WEBHOOK_URL` does not exactly match the URL you registered.

## Common Event Types

Every payload has a top-level `action` string. The object type is determined by
which object is present (`card`, `comment`, or `hook` for the ping), so handlers
dispatch on the combined `<type>.<action>` key below.

| Event | `action` | Fires When |
| --- | --- | --- |
| `ping` | `ping` | Webhook is created — validate the endpoint (return 2xx) |
| `card.created` | `created` | A card is created |
| `card.committed` | `committed` | A card is committed (moved out of a sheet/backlog into a board) |
| `card.moved` | `moved` | A card moves between columns/boards |
| `card.updated` | `updated` | A card's fields change |
| `card.deleted` | `deleted` | A card is deleted |
| `comment.created` | `created` | A comment is added |
| `comment.updated` | `updated` | A comment is edited |
| `comment.deleted` | `deleted` | A comment is deleted |

> Note: UI-automation-triggered webhooks send partial data with no pre-update
> state. Treat fields as possibly-absent and fetch the full card from the Favro
> API when you need the complete record. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/favro-webhooks/references/overview.md).

## Environment Variables

```bash
FAVRO_WEBHOOK_SECRET=your_webhook_secret        # the secret you entered when creating the webhook
FAVRO_WEBHOOK_URL=https://example.com/webhooks/favro  # the postToUrl you registered, VERBATIM

```

`FAVRO_WEBHOOK_URL` is part of the signed message, so it must be byte-identical to
the URL Favro has on file for this webhook. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/favro-webhooks/references/setup.md).

## Local Development

```bash
# Start tunnel (no account needed) — forwards to your local handler
npx hookdeck-cli listen 3000 favro --path /webhooks/favro

```

Register the resulting public URL as the `postToUrl` when you create the webhook,
and set `FAVRO_WEBHOOK_URL` to that exact same URL.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/favro-webhooks/references/overview.md) - Event types, payload structure, partial-data caveat
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/favro-webhooks/references/setup.md) - Creating a webhook, the secret, and the URL gotcha
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/favro-webhooks/references/verification.md) - X-Favro-Webhook HMAC-SHA1 verification in depth and gotchas