# Guide to Cloudsmith Webhooks: Features and Best Practices

Cloudsmith hosts private package repositories for formats such as Debian, npm, Python, Docker and raw files, and every package goes through a pipeline after upload: it is created, synchronised (processed and indexed), scanned for vulnerabilities, and possibly quarantined. Cloudsmith webhooks announce each step. A release pipeline can wait for `package.synced` before deploying, a security team can react to `package.quarantined`, and a mirror can follow deletions and restores.

This guide covers Cloudsmith's package events, its four payload formats and why they matter for routing, the optional HMAC-SHA1 signature, the published source IPs, and what Cloudsmith's documentation leaves out about delivery.

## What are Cloudsmith webhooks?

Each Cloudsmith webhook belongs to one repository. When you create it you pick the event types it receives, and you can add a package search query so that only matching packages produce events, for example `name:my-web-app AND tag:release`.

You also choose a payload format. The default **JSON Object** sends an envelope with the event in `meta.event_id` and the package in `data`. **JSON Array** wraps the same object in `[...]`, **Form Encoded JSON Object** sends `payload=<url-encoded JSON>`, and a **Handlebars Template** sends whatever your template produces, such as a Slack message.

Signing is per webhook too. With an **HMAC Signature Key**, every request carries `X-Cloudsmith-Signature`, a hex HMAC-SHA1 of the body. Without one, the request is unsigned.

## Cloudsmith webhook features

| Feature | Details |
| --- | --- |
| Configuration | Repository **Webhooks** settings, or the Cloudsmith API |
| Filtering | Chosen event types, plus an optional package search query |
| Payload formats | JSON Object (default), JSON Array, Form Encoded JSON Object, Handlebars Template |
| Signature | hex HMAC-SHA1 |
| Signature header | `x-cloudsmith-signature` |
| Signed content | The raw request body, in whichever format the webhook sends |
| Secret | HMAC Signature Key, replaceable later but not readable |
| Alternative | A static **Secret Header** and **Secret Value** sent on every request |
| Event routing | Body field `meta.event_id` (JSON Object format) |
| Source IPs | `34.252.163.216`, `52.208.86.0`, `108.129.59.129` |
| TLS | Certificate verification can be turned off per webhook |
| Retries and timeout | Not documented |

## Events and payloads

| Event (`meta.event_id`) | Triggered When | Common Use Cases |
| --- | --- | --- |
| `ping` | The test action on the webhook | Connectivity check |
| `package.created` | A package is uploaded | Audit trail, CI hand-off |
| `package.syncing` | A package starts syncing | Progress tracking |
| `package.synced` | A package is fully synchronised | Trigger deployments |
| `package.security_scanned` | A security scan finishes | Fetch results, gate releases |
| `package.quarantined` | A package is quarantined | Alert the owning team |
| `package.failed` | A package fails to sync | Failure alerts |
| `package.released` | A package is released | Promote downstream |
| `package.restored` | A package is restored | Re-sync mirrors |
| `package.tags_updated` | A package's tags change | Track `latest` moves |
| `package.deleted` | A package is deleted | Remove mirrored records |

`ping` is also sent when a webhook is first created. `package.released` means released from quarantine, and `package.restored` means restored after deletion.

In the JSON Object format the body has three parts. `data` is usually the full Package model, with checksums, file lists, download URLs and the sync stage. `meta` holds `event_id`, `trigger_id`, `webhook_id`, and the `event_at` and `attempt_at` times. `context` is optional and holds extra detail for some events, such as old and new tags for `package.tags_updated`. Here is the `meta` block and a few `data` fields from the `package.synced` sample in the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/cloudsmith/latest/package.synced.json):

```json
{
  "data": {
    "name": "my-file.deb",
    "format": "raw",
    "namespace": "my-org",
    "repository": "testo2",
    "slug_perm": "xKYgfsOsboz6",
    "stage_str": "Fully Synchronised",
    "status_str": "Completed"
  },
  "meta": {
    "attempt_at": "2020-07-07T17:30:34.342167+00:00",
    "event_at": "2020-07-07T17:30:34.296482+00:00",
    "event_id": "package.synced",
    "trigger_id": "c0e2b63e-3d84-4d54-bd62-ae7d0b2764a7",
    "webhook_id": 1
  }
}

```

`event_at` is when the event happened and `attempt_at` is when this delivery was attempted, so a gap between them tells you the request was delayed.

## Setting up Cloudsmith webhooks

Set an HMAC signature key on the Cloudsmith webhook. Without one, deliveries are unsigned and fail verification.

1. Open the repository in Cloudsmith and go to its **Webhooks** settings.
2. Add a webhook with your endpoint as the payload URL.
3. Enter an **HMAC Signature Key**, or note the one Cloudsmith suggests. Store it on your server straight away, for example as `CLOUDSMITH_WEBHOOK_KEY`, because you cannot read it back later. It is used as typed, not decoded.
4. Choose the events to receive, and add a package search query if you only care about some packages.
5. Leave the payload format on **JSON Object** unless you have a reason to change it. The receiver below expects that format.
6. Save, then send a `ping` from the webhook's page.

Cloudsmith also offers **Secret Header** and **Secret Value** fields, which send the same fixed value on every request. That is a shared token, not a signature, and Hookdeck's Cloudsmith source doesn't check it. Use the HMAC key for verification.

Only turn off SSL certificate verification if your endpoint uses a self-signed certificate. Cloudsmith warns that doing so lets an attacker who can intercept traffic impersonate your endpoint.

To route through Hookdeck, create a source with the Cloudsmith source type, enter the HMAC Signature Key as its verification secret, and set the Hookdeck source URL as the payload URL. During development, the [Hookdeck CLI](/docs/cli) can forward deliveries locally: `npx hookdeck-cli listen 3000 cloudsmith --path /webhooks/cloudsmith`.

## Securing Cloudsmith webhooks

Cloudsmith uses HMAC-SHA1, not SHA-256. Compute an HMAC-SHA1 of the raw body with the signature key's UTF-8 bytes, and compare it in constant time with `X-Cloudsmith-Signature` after checking that the header is 40 hex characters. There is no prefix to strip. Cloudsmith's [webhook documentation](https://docs.cloudsmith.com/developer-tools/webhooks#validating-webhooks) names the algorithm, and its API schema describes the digest as hex.

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-cloudsmith-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha1', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{40}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

A receiver for the JSON Object format, routing on `meta.event_id`:

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

const app = express();

app.post('/webhooks/cloudsmith', express.raw({ type: '*/*' }), async (req, res) => {
  const key = process.env.CLOUDSMITH_WEBHOOK_KEY;
  if (!key) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, key)) return res.sendStatus(401);

  let parsed;
  try {
    parsed = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
  const meta = parsed.meta && typeof parsed.meta === 'object' ? parsed.meta : {};
  const data = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};

  try {
    switch (meta.event_id) {
      case 'package.synced':
        await startDeployment(data.namespace, data.repository, data.slug_perm);
        break;
      case 'package.quarantined':
      case 'package.failed':
        await alertOwners(meta.event_id, data.name, data.version);
        break;
      case 'ping':
      default:
        break;
    }
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

```

`startDeployment` and `alertOwners` are placeholders. If you change the payload format, the parsing has to change too, but the signature check stays the same because it runs over the raw bytes.

The signature has no timestamp, so it cannot tell a fresh request from a replayed one. Hookdeck's Cloudsmith source verifies the same HMAC-SHA1 header with no time window. It responds to Cloudsmith with `200` and records a failed verification, rather than a `401`, when the signature doesn't match.

> **Make Cloudsmith webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `X-Cloudsmith-Signature` on every package event, keeps a searchable history of uploads, syncs and quarantines, and retries delivery to your pipeline when it fails.

## Cloudsmith webhook limitations and pain points

### Retries aren't documented

**The Problem:** A deployment trigger can be missed if your endpoint is down when a package syncs.

**Why It Happens:** Cloudsmith's webhook page describes no retry policy, no timeout and no rule for which status codes count as success.

**Workarounds:**

* Return a `2xx` quickly and keep the endpoint available during releases.
* For critical pipelines, confirm the package state through the Cloudsmith API before and after deploying, rather than relying on the webhook alone.

**How Hookdeck Can Help:** Hookdeck stores each Cloudsmith delivery as it arrives and retries your service on a schedule you configure, so delivery to your pipeline no longer depends on Cloudsmith's unpublished behaviour.

### Signing is opt-in and SHA-1 only

**The Problem:** Unsigned webhooks are easy to create, and a SHA-256 verifier never matches a signed one.

**Why It Happens:** The HMAC key is optional, and the static Secret Header is offered as an alternative. When a key is set, Cloudsmith uses HMAC-SHA1.

**Workarounds:**

* Always set an HMAC Signature Key, and treat the Secret Header as an extra check at most.
* Use SHA-1 in your verifier and check for a 40-character hex digest.
* Allowlist Cloudsmith's three published IPs as a second layer.

**How Hookdeck Can Help:** Hookdeck's Cloudsmith source is configured for HMAC-SHA1 on `x-cloudsmith-signature`, so you don't have to get the algorithm right in each service.

### Payload format changes the routing field

**The Problem:** A handler that reads `meta.event_id` finds nothing.

**Why It Happens:** Only the JSON Object format puts the event at `meta.event_id`. JSON Array wraps it in a list, form encoding hides it inside a `payload` field, and a Handlebars template may drop it entirely.

**Workarounds:**

* Use JSON Object for machine receivers, and save templates for chat notifications.
* If you need a template, include `{{meta.event_id}}` or an equivalent in its output.

**How Hookdeck Can Help:** Hookdeck transformations can unwrap an array or decode a form body into the JSON Object shape before your service sees it.

### No replay protection

**The Problem:** A captured request still verifies later.

**Why It Happens:** Only the body is signed.

**Workarounds:**

* Deduplicate on `meta.trigger_id`.
* Treat a `package.synced` event as a prompt to check the package, not as an instruction to deploy blindly.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop requests with a `meta.trigger_id` it has already delivered.

## Best practices

### Filter with a search query

Use the webhook's package search query to send only the packages a receiver cares about. Fewer events means less to verify and fewer chances to fail.

### Verify the raw body

The signature covers whatever bytes the chosen format produced. Use `express.raw({ type: '*/*' })` in Express, `await request.arrayBuffer()` in Next.js and `await request.body()` in FastAPI.

### Deduplicate on `trigger_id`

Store `meta.trigger_id` when you act on an event. The [webhook idempotency guide](/webhooks/guides/implement-webhook-idempotency) covers the storage side.

### Hand off long work

Deployments and scans take longer than a webhook request should. Store the event, respond, and start the job afterwards. [Asynchronous webhook processing](/webhooks/guides/why-implement-asynchronous-processing-webhooks) goes into the details.

## Conclusion

Cloudsmith webhooks follow packages through upload, synchronisation, scanning and quarantine, with filters for events and packages and a choice of payload formats. Set an HMAC Signature Key, verify with SHA-1 over the raw body, keep machine receivers on the JSON Object format, and deduplicate on `meta.trigger_id`, since Cloudsmith documents neither its retries nor any replay protection.

[Hookdeck Event Gateway](https://hookdeck.com) verifies each Cloudsmith signature and holds package events until your pipeline has processed them.

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