# 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](https://huggingface.co), 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](https://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](https://console.hookdeck.com) — 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):

```bash
openssl rand -hex 32

```

Then create the webhook:

1. Open the Webhooks settings page at [huggingface.co/settings/webhooks](https://huggingface.co/settings/webhooks).
2. Click Add a new webhook.
3. 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.
4. 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](/docs/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.

```javascript
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](/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.timingSafeEqual` or `secrets.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](/docs/retries) delivery to your handler automatically on its own schedule, and [Issues](/docs/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](/docs/destinations#set-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 `updatedRefs` shas, `discussion.num`, `comment.id`) so overlapping events converge on the same record.
* Treat unknown narrowed scopes as an `update` on their broader scope instead of rejecting them.
* Guard for `comment.content` being undefined when a comment is hidden.

How Hookdeck Can Help: [Filters](/docs/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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](/webhooks/guides/implement-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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Hugging Face webhooks reliably in minutes.