# Cloudinary Webhooks

Cloudinary sends webhook notifications to a URL you register, telling your
app when assets are uploaded, eager transformations finish, moderation completes,
assets are deleted or renamed, folders change, and more. Each POST is a JSON body
signed with your account API Secret and delivered with two headers:
`x-cld-signature` (a hex digest) and `x-cld-timestamp` (a unix timestamp).

## When to Use This Skill

* How do I receive Cloudinary webhooks / notifications?
* How do I verify the `x-cld-signature` header on a Cloudinary webhook?
* Why is my Cloudinary webhook signature verification failing?
* How do I handle `upload`, `eager`, or `moderation` notifications?
* What are the Cloudinary `notification_type` values?

## Verification (core)

Cloudinary signs the raw request body concatenated with the timestamp and your
account API Secret. Verify with the official SDK — it enforces a freshness window
(default 7200s) as well as the digest. Use the raw body byte-for-byte — do not
`JSON.parse` then re-stringify before verifying.

```javascript
const cloudinary = require('cloudinary').v2;
cloudinary.config({
  api_secret: process.env.CLOUDINARY_API_SECRET,
  signature_algorithm: process.env.CLOUDINARY_SIGNATURE_ALGORITHM || 'sha1', // 'sha256' if enabled
});

// rawBody = exact request body string; headers come from the request
const signature = req.get('x-cld-signature');
const timestamp = req.get('x-cld-timestamp');

// Reject missing headers with 400 first — a 401 should mean "bad signature".
if (!signature || !timestamp) return res.status(400).send('Missing signature headers');

// verifyNotificationSignature(body, timestamp, signature, valid_for = 7200) -> boolean
const valid = cloudinary.utils.verifyNotificationSignature(rawBody, Number(timestamp), signature);
if (!valid) return res.status(401).send('Invalid signature');

```

Cloudinary computes the signature as a plain hex digest of
`rawBody + timestamp + api_secret` using sha1 (default) or sha256 (an
opt-in account setting) — it is not a keyed HMAC, though Cloudinary's docs
sometimes call it "HMAC-SHA1". The SDK abstracts this; see
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudinary-webhooks/references/verification.md) for the exact scheme and
a manual fallback.

> For complete handlers with tests, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudinary-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudinary-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudinary-webhooks/examples/fastapi/).

## Common Event Types (`notification_type`)

| `notification_type` | Fires When | Notable fields |
| --- | --- | --- |
| `upload` | An asset finishes uploading (async/eager or large uploads) | `public_id`, `secure_url`, `version` |
| `eager` | Eager (async) transformations finish generating | `public_id`, `eager` |
| `delete` | One or more assets are deleted | `resources` |
| `rename` | An asset is renamed | `from_public_id`, `to_public_id` |
| `moderation` | A moderation result is available | `public_id`, `moderation_status` |
| `resource_tags_changed` | Tags are added to / removed from assets | `resources` |
| `create_folder` | A folder is created | `folder_path` |
| `delete_folder` | A folder is deleted | `folder_path` |

Other notification types include `resource_context_changed`,
`resource_metadata_changed`, `access_control_changed`, and `move`. Every
notification carries a `notification_type`, a `timestamp`, and a `signature` in
the body — but authentication uses the `x-cld-signature` / `x-cld-timestamp`
headers, not the in-body fields. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudinary-webhooks/references/overview.md).

## Environment Variables

```bash
CLOUDINARY_API_SECRET=your_account_api_secret   # account API Secret (Console → Settings → API Keys); the api_secret in CLOUDINARY_URL
# CLOUDINARY_SIGNATURE_ALGORITHM=sha1           # sha1 (default) or sha256 if enabled on your account

```

The signing secret is your account API Secret — there is no separate
per-webhook signing secret.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 cloudinary --path /webhooks/cloudinary

```

Use port `8000` for the FastAPI example.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudinary-webhooks/references/overview.md) - Notification types, payload structure, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudinary-webhooks/references/setup.md) - Register the Notification URL, find the API Secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudinary-webhooks/references/verification.md) - The digest scheme, SDK + manual verification, gotchas