Guide to Hugging Face Webhooks: Features and Best Practices
Hugging Face webhooks notify your systems about activity on the Hub: a model repo receives new commits, a dataset flips from private to public, a Pull Request opens, a comment lands on a discussion. If you're building on Hugging Face, webhooks are the foundation for MLOps automation: trigger CI for datasets, kick off retraining when data changes, run discussion and PR bots, and sync external catalogs without polling.
This guide covers how Hugging Face webhooks work, the shared-secret verification model, the scope and action event taxonomy, the payload shape, and the best practices for production.
What are Hugging Face webhooks?
Hugging Face webhooks are HTTP POST requests sent by the Hub when events occur on repositories (models, datasets, and Spaces) or in their associated discussions and Pull Requests. A single webhook can watch specific repos or all repos owned by a user or organization, including repos you don't own. The distinctive part is verification: Hugging Face does not use HMAC signatures. The secret you configure is sent verbatim in the X-Webhook-Secret header on every request, and you verify it with a timing-safe string comparison.
Hugging Face webhook features
| Feature | Details |
|---|---|
| Configuration | huggingface.co/settings/webhooks; watch specific repos and/or all repos of a user or org, and choose repo updates, Pull Requests, discussions, and/or comments |
| Verification | Shared secret sent verbatim in the X-Webhook-Secret header, or as a ?secret= query parameter; timing-safe comparison, no HMAC; ASCII-only secrets |
| Payload | JSON with top-level event, repo, and webhook (version 3) objects, plus discussion, comment, updatedRefs, or updatedConfig depending on the scope |
| Events | Identified by event.scope plus event.action; five scopes (repo, repo.content, repo.config, discussion, discussion.comment) with actions create, update, delete, and move |
| Rate limit | 1,000 triggers per webhook per 24 hours; PRO, Team, and Enterprise plans can request a higher limit via website@huggingface.co |
| Delivery visibility | The Activity tab lists every delivery with the request payload, response status, and response body, plus a per-delivery Replay button |
| Jobs | A webhook can trigger a Hugging Face Job instead of (or in addition to) calling your endpoint |
| SDK | None needed; verification is a constant-time string comparison with crypto.timingSafeEqual or secrets.compare_digest |
Common events
Hugging Face identifies events by a pair of fields: event.scope and event.action.
event.scope | event.action values | Fires when |
|---|---|---|
repo | create, update, delete, move | A repo (model, dataset, or Space) is created, has its metadata updated, is deleted, or is renamed or transferred |
repo.content | update | New commits, branches, or tags land, including refs created by new Pull Requests; updatedRefs lists the changed refs |
repo.config | update | Settings, secrets, DOI, or privacy change; updatedConfig is included (currently only private is reported) |
discussion | create, update, delete | A discussion or Pull Request is opened, retitled, merged, closed, or deleted |
discussion.comment | create, update | A comment is posted or edited; when a comment is hidden, content is undefined |
Branch on the concatenation of event.scope and event.action. Two catalog details worth knowing: on the Hub, a Pull Request is a special type of discussion, so check discussion.isPullRequest to tell them apart; and new narrowed scopes may be added over time (for example repo.config.dois), so treat an unknown narrowed scope as an update on its broader scope rather than an error.
See Hugging Face webhook payloads in action. Inspect and replay sample Hugging Face webhook payloads in the Hookdeck Console — no account or setup required.
Setting up Hugging Face webhooks
You need a publicly reachable HTTPS endpoint and, recommended, a secret token. Generate a strong random value first (only ASCII characters are supported):
openssl rand -hex 32
Then create the webhook:
- Open the Webhooks settings page at huggingface.co/settings/webhooks.
- Click Add a new webhook.
- Fill in the form: your Target URL (for example
https://api.example.com/webhooks/huggingface), the Secret you generated, and the Watched items (users, orgs, and/or specific repos, including repos you don't own). Choose whether to subscribe to repo updates, Pull Requests, discussions, and/or comments. - Save the webhook, and store the same secret in your app as
HUGGINGFACE_WEBHOOK_SECRET.
If reading HTTP headers is difficult in your environment, you can instead append the secret to the URL as ?secret=your_secret_value; your handler should still verify it with a timing-safe comparison. To test, open the webhook's Activity tab, which lists every recent event with the request payload, response status, and response body, and click Replay next to any delivery to send the same payload again. Replays use the webhook's current target URL and secret, not the ones at the time of the original delivery.
For local development you still need a public HTTPS URL. The Hookdeck CLI provides one with no account needed: run hookdeck listen 3000 huggingface --path /webhooks/huggingface and Hugging Face deliveries are tunneled to your local server, with an inspector for replaying them as you iterate.
Securing Hugging Face webhooks
Verification is a byte-for-byte comparison: the X-Webhook-Secret header contains the literal secret string you configured, so no HMAC computation, timestamp check, or digest is involved. Because no signature is computed over the body, you don't need the raw request body to verify; parsed JSON is fine. Compare the header (or the ?secret= query parameter, with the header taking precedence) against your stored secret using a timing-safe comparison, and fail closed with a 401 on any mismatch or missing value.
const crypto = require("crypto");
function verifyHuggingFaceWebhook(provided, secret) {
if (!provided || !secret) return false;
try {
// The secret is sent verbatim; compare it timing-safely
return crypto.timingSafeEqual(
Buffer.from(provided),
Buffer.from(secret)
);
} catch {
return false; // buffers must be the same length
}
}
app.post("/webhooks/huggingface", express.json(), (req, res) => {
// Header takes precedence; fall back to the ?secret= query parameter
const provided = req.headers["x-webhook-secret"] || req.query.secret;
if (!verifyHuggingFaceWebhook(provided, process.env.HUGGINGFACE_WEBHOOK_SECRET)) {
return res.status(401).send("Unauthorized");
}
const { event, repo, discussion, comment, updatedRefs, updatedConfig } = req.body;
const key = `${event.scope}.${event.action}`;
// Acknowledge, then process asynchronously
processQueue.add({ key, repo: repo.name, discussion, comment, updatedRefs, updatedConfig });
res.json({ received: true });
});
Note that Express lowercases header names, so read req.headers["x-webhook-secret"], not the capitalized form. Prefer the header over the query parameter in production, since query strings can leak through logs, proxies, and browser history, and serve the endpoint over HTTPS only: the secret travels in cleartext on every request, so TLS is non-negotiable. When rotating, update the Hugging Face setting and your environment variable together.
Make Hugging Face webhooks production-ready. Hookdeck Event Gateway verifies the shared secret upstream, deduplicates, and durably queues every event.
Hugging Face webhook limitations and pain points
A shared secret instead of a signature
The Problem: The X-Webhook-Secret header authenticates the sender but signs nothing, so there is no payload-integrity guarantee, no timestamp, and no replay protection, and the secret itself is present on every delivery.
Why It Happens: Hugging Face chose a shared-secret token over payload signing, which keeps verification trivially simple; TLS carries the integrity burden.
Workarounds:
- Compare the secret with
crypto.timingSafeEqualorsecrets.compare_digest, never===. - Use a cryptographically random ASCII secret (
openssl rand -hex 32) and rotate both sides together. - Prefer the header over the
?secret=query parameter, and don't log headers or URLs that carry the secret.
How Hookdeck Can Help: Hookdeck verifies the shared secret at the edge, so unverified traffic never reaches your handler, and your app only holds the secret in one place.
Recovery from failed deliveries is manual
The Problem: When your endpoint is down or returning errors, failed deliveries accumulate in the webhook's Activity tab, and the documented re-delivery mechanism is clicking Replay on each one. Replays also target the webhook's current URL and secret, not the ones in effect at delivery time.
Why It Happens: The Activity tab is designed as a debugging surface: it shows every request and response, and Replay re-sends a single payload against your current configuration.
Workarounds:
- After an incident, work through the Activity tab and replay the failed deliveries once your endpoint is fixed.
- If you've moved the endpoint, update the webhook settings before replaying, since replays go to the current URL.
How Hookdeck Can Help: Hookdeck acknowledges Hugging Face immediately, then retries delivery to your handler automatically on its own schedule, and Issues alert you as soon as deliveries start failing, before a backlog builds up.
A 1,000-trigger daily cap on a potentially org-wide firehose
The Problem: Each webhook is limited to 1,000 triggers per 24 hours. A webhook watching all repos of an active user or organization funnels every commit, discussion, and comment across those repos into that one budget, and an active day can exhaust it.
Why It Happens: The cap is a per-webhook platform limit. PRO, Team, and Enterprise plans can request a higher limit via website@huggingface.co.
Workarounds:
- Scope the watched items to the repos you actually need, and subscribe only to the event categories (repo updates, Pull Requests, discussions, comments) you consume.
- Monitor usage in the Activity tab so you see when you're approaching the cap.
- Request a higher limit from Hugging Face if you're on an eligible plan.
How Hookdeck Can Help: For the traffic you do receive, Hookdeck queues bursts durably and sets a max delivery rate to your handler, so a spike of commits or comments never overwhelms your infrastructure.
One action on the Hub can fire multiple events
The Problem: Opening a Pull Request triggers both a discussion create event and a repo.content update event (for the newly created ref), and a comment posted when a discussion is created also fires discussion.comment create. Handlers that assume one delivery per user action process the same change twice, and naive switch statements break when a new narrowed scope like repo.config.dois appears.
Why It Happens: Scopes are independent views of the same underlying change, and the taxonomy is designed to grow: Hugging Face documents that more scopes may be added in the future.
Workarounds:
- Key side effects on stable identifiers (the
updatedRefsshas,discussion.num,comment.id) so overlapping events converge on the same record. - Treat unknown narrowed scopes as an
updateon their broader scope instead of rejecting them. - Guard for
comment.contentbeing undefined when a comment is hidden.
How Hookdeck Can Help: Filters route each scope to the handler that owns it, so every consumer receives only the events it cares about.
Best practices
Verify the secret on every request
Timing-safe comparison of X-Webhook-Secret (or the query parameter fallback) against your stored secret, 401 on any failure including a missing header. Skip the raw-body plumbing; there is no signature over the body, so parsed JSON is fine.
Acknowledge fast, process asynchronously
Webhook reactions on the Hub tend to be heavy: retraining a model, running CI on a dataset, calling an LLM to answer a discussion. Verify, enqueue, and respond, then do the work off the request path. See why to process webhooks asynchronously.
Branch on scope plus action, with a forgiving default
event.scope plus event.action identifies the event. Give your switch a default branch that treats unknown narrowed scopes as an update on the broader scope, so new scopes degrade gracefully.
Make handlers idempotent
Replay re-sends the exact same payload, and a single action such as opening a PR fires events under multiple scopes. Upsert on stable identifiers and make side effects safe to repeat; see our guide to webhook idempotency.
Check isPullRequest before treating a discussion as a PR
PRs are a special type of discussion, so discussion events cover both. Branch on discussion.isPullRequest, and use discussion.changes.base when you need the target ref.
Watch the Activity tab
It's your delivery log, your usage meter against the 1,000-trigger cap, and your replay tool. Remember that replays use the current target URL and secret, so update settings before replaying if the endpoint has moved.
Conclusion
Hugging Face webhooks verify with a shared secret sent verbatim in the X-Webhook-Secret header rather than an HMAC signature, identify events by event.scope plus event.action across models, datasets, and Spaces, and cap each webhook at 1,000 triggers per 24 hours. Verify the secret timing-safely, branch on scope and action with a forgiving default, make handlers idempotent, and keep recovery in mind, since re-delivery is a manual replay from the Activity tab.
Hookdeck Event Gateway verifies the secret at the edge, queues bursts durably, retries your handler automatically, and routes each scope to the right consumer, so the manual-replay and rate-limit constraints stop shaping your handler code.
Get started with Hookdeck for free and handle Hugging Face webhooks reliably in minutes.