Guide to Favro Webhooks: Features and Best Practices
Favro webhooks notify your systems about board activity: a card is created, moved, or updated, a comment is added. If you're building on Favro, webhooks are how you react to project changes without polling.
This guide covers how Favro webhooks work, the events you'll handle, its unusual verification (the signed message isn't the body), the ping handshake, and the best practices for production.
What are Favro webhooks?
Favro webhooks are HTTP POSTs delivered to a URL you configure per board/widget. The verification is unusual: the signature in the X-Favro-Webhook header is a base64 HMAC-SHA1 where the signed message is payloadId + webhookUrl (the payload's payloadId concatenated with the webhook's own target URL), keyed with the webhook secret, not the raw body, and SHA1 rather than SHA256.
Favro webhook features
| Feature | Details |
|---|---|
| Configuration | Per board/widget: POST create-webhook (name, widgetCommonId, postToUrl, secret) or UI automations |
| Signature header | X-Favro-Webhook |
| Signature scheme | base64 HMAC-SHA1 over payloadId + webhookUrl, keyed with the webhook secret |
| Setup handshake | A Ping event on creation, return 2xx to validate |
| Events | action field (card and comment actions) |
| Caveat | UI-automation-triggered webhooks send partial data (no pre-update state) |
| SDK | None (community @bscotch/bravo implements verification) |
Common events
Favro payloads carry an action field:
| Action | Fires when |
|---|---|
Card created | A card is created |
Card committed | A card is committed |
Card moved | A card is moved |
Card updated | A card is updated |
Card deleted | A card is deleted |
Comment created / updated / deleted | A comment changes |
Branch on the action value.
Setting up Favro webhooks
Create webhooks per board/widget via the create-webhook API (name, widgetCommonId, postToUrl, secret) or through Favro's UI automations. On creation, Favro sends a Ping event, return a 2xx to validate the setup.
Securing Favro webhooks
The X-Favro-Webhook header is base64(HMAC-SHA1(secret, payloadId + webhookUrl)). To verify, concatenate the payload's payloadId with the webhook's own target URL (the postToUrl you registered), HMAC-SHA1 it with the webhook secret, base64-encode, and compare. The webhook URL is a required input, which is why you must know it at verification time.
const crypto = require("crypto");
const SECRET = process.env.FAVRO_WEBHOOK_SECRET;
const WEBHOOK_URL = process.env.FAVRO_WEBHOOK_URL; // exactly as registered (postToUrl)
function verify(payloadId, signature) {
const message = payloadId + WEBHOOK_URL; // NOT the raw body
const expected = crypto.createHmac("sha1", SECRET).update(message).digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.json(), (req, res) => {
// Favro sends a Ping on creation; accept it to validate setup
if (req.body && req.body.hook === "ping") {
return res.sendStatus(200);
}
if (!verify(req.body.payloadId, req.headers["x-favro-webhook"])) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast
processQueue.add(req.body); // branch on action, async
});
The same check in Python:
import base64
import hashlib
import hmac
import os
SECRET = os.environ["FAVRO_WEBHOOK_SECRET"].encode()
WEBHOOK_URL = os.environ["FAVRO_WEBHOOK_URL"]
def verify(payload_id: str, signature: str) -> bool:
message = (payload_id + WEBHOOK_URL).encode() # NOT the raw body
expected = base64.b64encode(hmac.new(SECRET, message, hashlib.sha1).digest()).decode()
return hmac.compare_digest(expected, signature or "")
The header name (X-Favro-Webhook) comes from the community SDK; Favro's live doc is JS-rendered and has shown inconsistent header strings, so confirm the exact header against a real delivery.
Favro webhook limitations and pain points
The signed message is payloadId + webhookUrl, not the body
The Problem: Verification is over the payloadId concatenated with the webhook's own URL, not the raw body, and it's SHA1, not SHA256. Standard "HMAC the raw body with SHA256" logic finds nothing that matches.
Why It Happens: Favro signs a small identifier plus the destination URL, with SHA1.
Workarounds:
- Concatenate
payloadId + webhookUrl, HMAC-SHA1, base64, and compare, using the exact registered URL.
How Hookdeck Can Help: Hookdeck can verify provider signatures at the edge, so your app receives pre-verified events without reconstructing the payloadId-plus-URL message.
The header name is inconsistent in the docs
The Problem: Favro's live doc (JS-rendered) has shown different header strings; the trusted value is X-Favro-Webhook (from the community SDK). Checking the wrong header name misses the signature.
Why It Happens: The documentation is inconsistent.
Workarounds:
- Use
X-Favro-Webhook, and confirm against a real delivery.
How Hookdeck Can Help: Hookdeck verifies at the edge, so your app isn't coupled to the header name.
The Ping handshake
The Problem: Favro sends a Ping on creation that isn't a real event; a handler that tries to verify or process it as a card/comment event errors out.
Why It Happens: Favro validates the endpoint with a Ping.
Workarounds:
- Detect the Ping and return 2xx without verifying it as a data event.
How Hookdeck Can Help: Hookdeck can accept the Ping and forward only real events to your handler.
UI-automation webhooks send partial data
The Problem: Webhooks triggered by UI automations send partial data with no pre-update state, so diff-based logic breaks.
Why It Happens: UI-automation triggers carry a reduced payload.
Workarounds:
- Don't assume full or pre-update state; fetch current state via the API when you need it.
How Hookdeck Can Help: Hookdeck durably queues events so your worker can enrich them via the API at its own pace.
Best practices
Verify payloadId + webhookUrl with HMAC-SHA1
Concatenate payloadId with the exact registered webhook URL, HMAC-SHA1 with the secret, base64, and compare against X-Favro-Webhook in constant time.
Handle the Ping
Detect the Ping on creation and return 2xx without treating it as a data event.
Acknowledge fast, branch on action
Return 200 quickly, defer work to a queue, and branch on the action field. See why to process webhooks asynchronously.
Don't assume full data
Fetch current state via the API for UI-automation webhooks that send partial data.
Make Favro webhooks production-ready
Hookdeck verifies X-Favro-Webhook, handles the Ping, deduplicates, and durably queues every board event
Conclusion
Favro webhooks verify with an X-Favro-Webhook base64 HMAC-SHA1 over payloadId + webhookUrl, not the raw body, so you need the registered URL at verification time. Handle the Ping on creation, branch on the action field, confirm the header name against a real delivery, and don't assume full data from UI-automation webhooks.
Hookdeck verifies the signature, handles the Ping, deduplicates, and durably queues every event at the edge, so your app only ever processes verified board events.
Get started with Hookdeck for free and handle Favro webhooks reliably in minutes.