Gareth Wilson Gareth Wilson

Guide to Cloudsmith Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationRepository Webhooks settings, or the Cloudsmith API
FilteringChosen event types, plus an optional package search query
Payload formatsJSON Object (default), JSON Array, Form Encoded JSON Object, Handlebars Template
Signaturehex HMAC-SHA1
Signature headerx-cloudsmith-signature
Signed contentThe raw request body, in whichever format the webhook sends
SecretHMAC Signature Key, replaceable later but not readable
AlternativeA static Secret Header and Secret Value sent on every request
Event routingBody field meta.event_id (JSON Object format)
Source IPs34.252.163.216, 52.208.86.0, 108.129.59.129
TLSCertificate verification can be turned off per webhook
Retries and timeoutNot documented

Events and payloads

Event (meta.event_id)Triggered WhenCommon Use Cases
pingThe test action on the webhookConnectivity check
package.createdA package is uploadedAudit trail, CI hand-off
package.syncingA package starts syncingProgress tracking
package.syncedA package is fully synchronisedTrigger deployments
package.security_scannedA security scan finishesFetch results, gate releases
package.quarantinedA package is quarantinedAlert the owning team
package.failedA package fails to syncFailure alerts
package.releasedA package is releasedPromote downstream
package.restoredA package is restoredRe-sync mirrors
package.tags_updatedA package's tags changeTrack latest moves
package.deletedA package is deletedRemove 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:

{
  "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 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 names the algorithm, and its API schema describes the digest as hex.

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:

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 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 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 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 verifies each Cloudsmith signature and holds package events until your pipeline has processed them.

Get started with Hookdeck for free and handle Cloudsmith webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.