# Qlik Cloud Webhooks

Qlik Cloud tenant webhooks send a JSON CloudEvent when something happens in the tenant: apps,
reloads, automations, users, API keys, data-integration tasks. If you give the webhook a secret,
every delivery carries a `qlik-signature` header: the **lowercase hex HMAC-SHA256 of the raw request
body**, keyed with that secret. There is no prefix, and no timestamp, id or other header is signed.

> **Hookdeck setup note:** Set a secret on the Qlik Cloud webhook. Without one, deliveries are unsigned and fail verification.

## When to Use This Skill

* How do I receive Qlik Cloud webhooks?
* How do I verify the `qlik-signature` header on a Qlik Cloud webhook?
* Why does my Qlik Cloud webhook signature verification fail or return 401?
* How do I handle `com.qlik.v1.app.reload.finished` or `com.qlik.v1.automation.created` events?
* Does Qlik Cloud retry failed webhooks? (No retry policy is documented; delivery history is kept 7 days and an event can be resent by hand)

## Verification (core)

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

function verifyQlikSignature(rawBody, signature, secret) { // rawBody: Buffer of the exact bytes received
  if (!secret || typeof signature !== 'string') return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before comparing
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signature.toLowerCase(), 'utf8'); // never Buffer.from(sig, 'hex') on unvalidated input
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

* Header: `qlik-signature`, the **whole value** is one bare hex digest (no `sha256=` prefix, no list).
* Signed content: the **raw body bytes** exactly as received. Never decode and re-encode them, and
  never `JSON.parse` then `JSON.stringify` before verifying. A `GET` with no body is verified over
  zero bytes.
* Key: the secret's **UTF-8 bytes**, used as-is (not hex- or base64-decoded). The secret is 32 to 64
  characters, write-once, and set when you create or edit the webhook.
* Hex is compared case-insensitively: require exactly 64 hex characters, lowercase both sides, then
  compare in constant time. Python: `hmac.compare_digest` on bytes, never `str`.
* Missing or empty header, malformed value or mismatch: **401**. Read the body raw
  (`express.raw({ type: '*/*' })`, `await request.arrayBuffer()`, `await request.body()`) and parse it
  only after the check.
* **No timestamp is signed**, so there is no replay window to check. Don't invent one from unsigned
  headers or body fields. Make the handler idempotent on the event `id` instead.
* After verifying, treat every field of the parsed body as untrusted: check that the body and each
  nested field have the expected type before reading into them.

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

## Set a Secret

Set a secret on the Qlik Cloud webhook. Without one, deliveries are unsigned and fail verification.

Without a secret Qlik Cloud sends **no** `qlik-signature` header, so every delivery fails
verification (and the examples answer 401). Secrets are write-once: to rotate one, edit the webhook
and enter a new value. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qlik-cloud-webhooks/references/setup.md).

## HTTP Methods

Qlik Cloud's docs don't name the HTTP method of a delivery. The examples therefore accept
**GET, POST, PUT, PATCH and DELETE** on the webhook route, verify the signature first for every
method, treat a missing body as empty bytes, and acknowledge a verified empty request with 200.
Hookdeck's Qlik Cloud source pre-selects POST without managing the method, so if Qlik Cloud uses any
other method, **also allow that method on the Hookdeck source**.

## Event Payload and Types

Dispatch on the body's `type` field (a top-level CloudEvents `type`):

```json
{
  "id": "b347fd0d-1321-417f-91ee-90e96c76389c",
  "time": "2018-10-30T07:06:22Z",
  "type": "com.qlik.v1.reload.finished",
  "source": "com.qlik/reloads",
  "specversion": "1.0",
  "tenantid": "UkeLb6ICti8OA8f3LueRI5kVNtuHc15f",
  "data": { "id": "6a849f7f18ec290e6ceb025d", "appId": "002ff8c5-3c11-4464-a22b-68fb5e7ee5e8", "succeeded": true }
}

```

Events in the Legacy format have been sent since November 3, 2025 as hybrids that also carry the
same value in the legacy `eventType`; the legacy fields are removed on December 10, 2026, leaving
`type` alone. Older payloads (the help page's example) carry only `eventType`, so the examples read
`type` and fall back to `eventType`.

| Event | Sent When |
| --- | --- |
| `com.qlik.v1.app.created` | An app is created |
| `com.qlik.v1.app.exported` | An app is exported |
| `com.qlik.v1.app.published` | An app is published |
| `com.qlik.v1.app.deleted` | An app is deleted |
| `com.qlik.v1.app.reload.finished` | An app reload finishes |
| `com.qlik.v1.app.data.updated` | An app's data is updated |
| `com.qlik.v1.reload.finished` | A reload finishes (`data.succeeded` tells the outcome) |
| `com.qlik.v1.api-key.validation.failed` | An API key fails validation |
| `com.qlik.v1.user.created` / `com.qlik.v1.user.deleted` | A user is created or deleted |
| `com.qlik.v1.automation.created` / `.updated` / `.deleted` | An automation is created, updated or deleted |
| `com.qlik.v1.automation.run.started` | An automation run starts |

Qlik documents 22 event types in all; handle a few and log the rest. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qlik-cloud-webhooks/references/overview.md).

## Environment Variables

```bash
QLIK_CLOUD_WEBHOOK_SECRET=your_32_to_64_character_webhook_secret   # the secret set on the Qlik Cloud webhook

```

## Local Development

Qlik Cloud can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 qlik-cloud --path /webhooks/qlik-cloud

```

Use the printed URL as the webhook's endpoint (Qlik Cloud requires HTTPS).

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qlik-cloud-webhooks/references/overview.md) - What Qlik Cloud webhooks are, event types and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qlik-cloud-webhooks/references/setup.md) - Create a webhook and set its secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qlik-cloud-webhooks/references/verification.md) - Signature check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/qlik-cloud-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests