# Guide to JFrog Webhooks: Features and Best Practices

JFrog webhooks tell your systems what is happening across the JFrog Platform: an artifact is deployed, a Docker tag is pushed, a build is promoted, a release bundle is distributed, an Xray scan finishes, or an application version clears an AppTrust gate. If you run Artifactory or the wider JFrog Platform, webhooks let CI/CD, deployment and security tooling react to those events instead of polling.

This guide covers predefined JFrog webhooks: how they are configured, how the secret token authenticates them, the domains and events they cover, what JFrog leaves undocumented, and the practices that keep a JFrog receiver reliable.

## What are JFrog webhooks?

JFrog webhooks are sent by the JFrog Event Service, which delivers events asynchronously. There are two kinds:

* **Predefined webhooks** send a fixed JSON payload for each domain and event. This guide covers these.
* **Custom webhooks** let you template the method, headers and body for a particular target service. Their format is whatever you configure.

A predefined webhook covers one **domain**, such as `artifact`, `docker` or `build`, and a selection of event types within it. Every payload names both, so the event is identified by the pair, for example `artifact` + `deployed`.

Authentication is optional and depends on how you set the webhook's secret. By default, JFrog sends the secret token unchanged in an `X-JFrog-Event-Auth` header, and your receiver compares it with the value it expects. JFrog also offers a signing mode, in which the secret signs the payload with HMAC-SHA256 and is not sent, but its documentation does not say which header carries that signature.

## JFrog webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Platform > Integrations > Webhooks** (all projects or one project), or `POST /event/api/v1/subscriptions` |
| Types | Predefined (fixed payload) or custom (your template) |
| Method | `POST` for predefined webhooks |
| Scope | One domain per webhook, with chosen event types and resource criteria |
| Project webhooks | Limited to Artifacts, Docker, Artifact Properties and Builds |
| Authentication | Optional secret token, sent verbatim in `X-JFrog-Event-Auth` (signing off, the API default) |
| Signing mode | HMAC-SHA256 of the payload, header and encoding not documented |
| Event identity | `domain` + `event_type` in the body |
| Custom headers | Any number, plus encrypted secrets injected with `{{.secrets.<name>}}` |
| Testing | A **Test** button sends a fake request |
| Troubleshooting | Successful executions can be shown in a **Troubleshooting** tab where available |
| Private targets | Blocked by default on self-hosted installs (`urlStrictPolicy`) |

JFrog does not document a response timeout, retry policy, event id, timestamp or source IP list for webhooks.

## Common events

JFrog documents 62 events with a `domain` and `event_type` across 12 domains, plus four Curation events.

| Domain | Event types |
| --- | --- |
| `artifact` | `deployed`, `deleted`, `moved`, `copied`, `cached` |
| `artifact_property` | `added`, `deleted` |
| `docker` | `pushed`, `deleted`, `promoted` |
| `build` | `uploaded`, `deleted`, `promoted` |
| `release_bundle` | `created`, `signed`, `deleted` |
| `distribution` | `distribute_started`, `distribute_completed`, `distribute_aborted`, `distribute_failed`, `delete_started`, `delete_completed`, `delete_failed` |
| `destination` | `received`, `delete_started`, `delete_completed`, `delete_failed` |
| `user` | `locked` |
| `release_bundle_v2` | `release_bundle_v2_started`, `release_bundle_v2_failed`, `release_bundle_v2_completed` |
| `release_bundle_v2_promotion` | `release_bundle_v2_promotion_started`, `release_bundle_v2_promotion_failed`, `release_bundle_v2_promotion_completed` |
| `xray_scan_status` | `done`, `failed`, `partial`, `not_supported` |
| `app_trust` | 24 events for entry and exit gate evaluations, application creation, update and deletion, version creation and promotion, and releases, each `_started`, `_completed` or `_failed` (gates use `_validation_passed` and `_validation_failed`) |

`event_type` alone is ambiguous: `deleted` exists in five domains. Always dispatch on both fields. The four Curation events (a package blocked by Curation, a waiver request created or updated, and a policy change) have payloads with no `domain` or `event_type`, so you can only recognise them by their shape.

JFrog's Distribution section headings say `deletion_started`, but its example payloads say `delete_started`. Match the example value.

Here is JFrog's `artifact` `deployed` example:

```json
{
  "domain": "artifact",
  "event_type": "deployed",
  "data": {
    "repo_key": "sample_repo",
    "path": "sample_dir/sample.txt",
    "name": "sample.txt",
    "sha256": "sample_checksum",
    "size": 0
  },
  "subscription_key": "test",
  "jpd_origin": "https://<your_origin>",
  "source": "jfrog/<your_source>",
  "userContext": {
    "id": "johndoe",
    "isToken": false,
    "realm": "internal"
  }
}

```

`data` holds the domain-specific object. `subscription_key` names the webhook, `jpd_origin` is your platform URL, and `userContext` identifies the user or token that caused the event. JFrog documents no event id, so there is no built-in deduplication key.

## Setting up JFrog webhooks

1. Choose **All Projects**, or a specific project, then open **Platform > Integrations > Webhooks** and click **New Webhook**.
2. Make sure the **Predefined** toggle is selected.
3. Enter a name, a description and the URL JFrog should call.
4. Select the events, then the resources each applies to, such as repositories, builds or AppTrust applications.
5. Enter a **secret token**, and leave the option to use the secret for signing **off**. With signing off, JFrog sends the token in `X-JFrog-Event-Auth` on every request.
6. Add any extra HTTP headers you need, click **Test**, then **Create**.

If you use Hookdeck, set the same token on your JFrog source. Hookdeck supports token mode only: it checks `X-JFrog-Event-Auth` and cannot verify JFrog's payload signing mode, so keep signing off. If you don't set a secret on the JFrog webhook, leave verification off on the source too.

Through the REST API, the same settings are `handlers[].secret` and `handlers[].use_secret_for_signing`, which defaults to `false`.

## Securing JFrog webhooks

In token mode there is nothing to compute. Read `X-JFrog-Event-Auth` and compare it with your secret token in constant time. Because the body is not signed, you can parse it normally.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyJfrogToken(headerValue, expectedToken) {
  if (!headerValue || !expectedToken) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedToken, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/jfrog', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.JFROG_WEBHOOK_SECRET;
  if (!token) return res.sendStatus(500); // fail closed

  if (!verifyJfrogToken(req.get('x-jfrog-event-auth'), token)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  // Dispatch on `${event.domain}.${event.event_type}`; Curation payloads have neither.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac

def verify_jfrog_token(header_value, expected_token: str) -> bool:
    if not header_value or not expected_token:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected_token.encode("utf-8"))

# In FastAPI: verify_jfrog_token(request.headers.get("x-jfrog-event-auth"),
#                                os.environ["JFROG_WEBHOOK_SECRET"])

```

The token is a static credential sent with every request. Anyone who captures one request can forge others, so serve the endpoint over HTTPS only, use a long random token, and rotate it if it might have leaked.

### Signing mode

With **use secret for signing** on, JFrog stops sending the token and signs the payload with HMAC-SHA256 instead. Its docs show how to check that a payload and signature match (`echo -n '<actual payload>' | openssl sha256 -hmac "<secret>"`) but not which header carries the signature or how it is encoded. Capture a real delivery and confirm the header before you build on signing mode.

> **Make JFrog webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `X-JFrog-Event-Auth` token, answers JFrog with an immediate `200 OK`, and durably queues every artifact, build and release event before your service sees it.

## JFrog webhook limitations and pain points

### A static token instead of a signature

**The Problem:** The credential travels with every request, and nothing protects the body from being changed.

**Why It Happens:** In token mode, JFrog sends the secret itself in `X-JFrog-Event-Auth`. The body is not signed and there is no timestamp, so a captured request can be replayed or altered.

**Workarounds:**

* Use HTTPS, a long random token, and rotate it periodically.
* Treat the webhook as a notification and confirm important details, such as a build's status, through the JFrog API before acting.

**How Hookdeck Can Help:** Hookdeck checks the token, so your own services never need to hold it, and stops requests with a missing or wrong token before they reach your service.

### Undocumented signing mode

**The Problem:** Turning on signing breaks verification.

**Why It Happens:** With signing on, JFrog no longer sends `X-JFrog-Event-Auth`, and the header that carries the signature is not documented. Receivers that check the token reject every request, and you have nothing documented to check instead.

**Workarounds:**

* Keep **use secret for signing** off unless you have confirmed the signature header from a real delivery.
* If you need signing, verify your implementation against captured requests first.

**How Hookdeck Can Help:** Hookdeck's JFrog source supports token mode. Keep signing off, set the token on the source, and Hookdeck authenticates each request.

### No delivery guarantees documented

**The Problem:** You don't know whether a failed delivery will be retried, or how long JFrog waits for a response.

**Why It Happens:** JFrog's webhook pages describe asynchronous delivery through the Event Service but no timeout, retry schedule or success criteria.

**Workarounds:**

* Respond quickly with a `2xx` and process asynchronously.
* Turn on the **Troubleshooting** tab where it is available, to see executions.
* Reconcile important state through the JFrog API after an outage.

**How Hookdeck Can Help:** Hookdeck accepts each event as soon as it arrives and stores it, then retries delivery to your service on a schedule you control and lets you replay anything that failed.

### No event id

**The Problem:** You can't reliably tell a duplicate from a new event.

**Why It Happens:** JFrog payloads carry no event id or timestamp.

**Workarounds:**

* Build a key from `domain`, `event_type` and identifying `data` fields, such as `repo_key` and `path` with `sha256` for artifacts, or build name and number for builds.
* Make handlers idempotent, so a repeated deploy event does no harm.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeats based on the body fields you choose.

## Best practices

### Dispatch on `domain` and `event_type` together

`deleted` means different things in five domains. Build a key such as `artifact.deleted` and route on that.

### Recognise Curation payloads by shape

Curation events have no `domain` or `event_type`. If you subscribe to them, check for their distinctive fields before falling through to "unknown event".

### Narrow webhooks with criteria

Limit each webhook to the repositories, builds or applications you care about, using include and exclude patterns. Selecting "any local repository" also covers repositories created later, which can grow traffic over time.

### Make the endpoint reachable

JFrog Cloud can't reach `localhost`, and self-hosted installs block private-network targets by default. Use a public HTTPS URL, or a tunnel such as the Hookdeck CLI during development.

### Acknowledge fast and process asynchronously

Return `200` once the token is checked and the event is stored, then do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Predefined JFrog webhooks send a JSON payload identified by `domain` and `event_type`, authenticated by a secret token in `X-JFrog-Event-Auth` when signing is off. Keep signing off unless you have confirmed its undocumented header, compare the token in constant time over HTTPS, dispatch on both fields, build your own deduplication key, and acknowledge quickly, since JFrog documents no retry policy.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the JFrog token, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle JFrog webhooks reliably in minutes.