Agent skill
Metabase Webhooks Skill
Receive and authenticate Metabase alert webhooks. Use when setting up a Metabase webhook endpoint, checking the HTTP Basic Authorization header, debugging 401s, or handling alert payloads with question results (alert_id, question_url, raw_data, base64 PNG visualization).
Install this skill
npx skills add hookdeck/webhook-skills --skill metabase-webhooks
Metabase can send the results of a saved question to a webhook when an alert fires. The request is a POST with a JSON body. Webhooks are not signed: there is no signature header and no timestamp. Instead you choose an authentication method on the webhook, and this skill covers HTTP Basic, where Metabase sends Authorization: Basic base64(username:password) on every request, up front, and your endpoint compares it with the credentials you chose.
When to Use This Skill
- How do I receive Metabase webhooks (alert webhooks)?
- How do I check the Basic
Authorizationheader from Metabase? - Does Metabase sign webhook payloads? (No: use Basic authentication)
- How do I handle the
alertpayload,raw_dataand the base64 PNGvisualization? - Why does my Metabase webhook endpoint return 401?
Setup note
Choose Basic authentication on the Metabase webhook. (Bearer, API key and query-param options are not covered here.) Use the same username and password in METABASE_ and METABASE_.
Verification (core)
const crypto = require('crypto');
const sha = (b) => crypto.createHash('sha256').update(b).digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(sha(a), sha(b)); // equal-length digests
function verifyMetabaseRequest(headers, username, password) {
if (!username || !password) return false; // never accept when unconfigured
const m = /^basic +([A-Za-z0-9+/]+={0,2})$/i.exec((headers['authorization'] || '').trim());
if (!m) return false; // strict base64 alphabet only
const decoded = Buffer.from(m[1], 'base64');
if (decoded.toString('base64') !== m[1]) return false; // reject trailing junk
const i = decoded.indexOf(0x3a); // split on the FIRST colon
if (i === -1) return false;
const userOk = safeEqual(decoded.subarray(0, i), Buffer.from(username));
const passOk = safeEqual(decoded.subarray(i + 1), Buffer.from(password));
return userOk && passOk; // evaluate both, no short-circuit
}
- Header:
Authorization: Basic base64(username:password); the scheme is case-insensitive. - Return
401for a missing or malformed header or a mismatch,400for a body that isn't a JSON object,200once handled. Credentials arrive on the first request (Metabase sends them preemptively), so there is no challenge round-trip to support. - Read the body raw (
express.raw({ type: '*/*', limit: '10mb' })) and authenticate before parsing it. Alerts embed a base64 PNG, so raise Express's default 100kb body limit. - In Python,
base64.b64decode(value, validate=True)andhmac.compare_digeston bytes (encode both sides;strwith non-ASCII raises). - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload and Event Types
There is one payload type, alert, and it's the type field of the body:
type | Sent When |
|---|---|
alert | A Metabase alert on a question fires (or an admin sends a test alert; then alert_id is null) |
{
"type": "alert",
"alert_id": null,
"alert_creator_id": 2666,
"alert_creator_name": "Roberto Bolaño",
"data": {
"type": "question",
"question_id": 108,
"question_name": "Sales",
"question_url": "https://example.com/question/108",
"visualization": "data:image/png;base64,...",
"raw_data": { "cols": ["CREATED_AT", "count"], "rows": [["2024-09-01T00:00:00Z", 346]] }
},
"sent_at": "2024-09-30T20:16:15.76582Z"
}
Environment Variables
METABASE_WEBHOOK_USERNAME=your-webhook-username # Admin > Settings > Webhooks > Basic
METABASE_WEBHOOK_PASSWORD=your-webhook-password
Local Development
Metabase can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 metabase --path /webhooks/metabase
Use the printed URL as the webhook URL in Metabase, then use Send a test alert.
Resources
- references/overview.md - What Metabase webhooks are, the alert payload
- references/setup.md - Create the webhook with Basic authentication
- references/verification.md - Basic auth check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests