Agent skill

Qlik Cloud Webhooks Skill

Receive and verify Qlik Cloud webhooks (Qlik's SaaS analytics platform). Use when setting up a Qlik Cloud webhook endpoint, verifying the qlik-signature HMAC-SHA256 header, debugging 401s, or handling events like com.qlik.v1.app.created, com.qlik.v1.app.reload.finished, com.qlik.v1.reload.finished and com.qlik.v1.automation.created.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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/, examples/nextjs/, 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.

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):

{
  "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.

EventSent When
com.qlik.v1.app.createdAn app is created
com.qlik.v1.app.exportedAn app is exported
com.qlik.v1.app.publishedAn app is published
com.qlik.v1.app.deletedAn app is deleted
com.qlik.v1.app.reload.finishedAn app reload finishes
com.qlik.v1.app.data.updatedAn app's data is updated
com.qlik.v1.reload.finishedA reload finishes (data.succeeded tells the outcome)
com.qlik.v1.api-key.validation.failedAn API key fails validation
com.qlik.v1.user.created / com.qlik.v1.user.deletedA user is created or deleted
com.qlik.v1.automation.created / .updated / .deletedAn automation is created, updated or deleted
com.qlik.v1.automation.run.startedAn automation run starts

Qlik documents 22 event types in all; handle a few and log the rest. See references/overview.md.

Environment Variables

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):

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →