Agent skill

Chameleon Webhooks Skill

Receive and verify Chameleon webhooks. Use when setting up a Chameleon (in-app product tours, microsurveys, embeds, HelpBar, demos) webhook endpoint, debugging X-Chameleon-Signature verification failures, or handling events like tour.completed, survey.completed, response.finished, helpbar.search and alert.triggered.

Install this skill

npx skills add hookdeck/webhook-skills --skill chameleon-webhooks


Chameleon (chameleon.io) sends a JSON POST to your endpoint when users interact with your in-app Experiences: a Tour starts or completes, a Microsurvey is answered, an Embed or Demo runs, a HelpBar search happens, or an Alert fires. Each request is signed with an HMAC-SHA256 of the raw body in the X-Chameleon-Signature header.

When to Use This Skill

  • How do I receive Chameleon webhooks?
  • How do I verify the X-Chameleon-Signature header?
  • How do I handle tour.completed, survey.completed or response.finished events?
  • Why does my Chameleon webhook signature verification fail?
  • Where do I find the Chameleon Webhook Secret?

Verification (core)

const crypto = require('crypto');

const HEX_SHA256 = /^[0-9a-fA-F]{64}$/;

// rawBody: Buffer exactly as received. Never parse and re-serialize it first.
function verifyChameleonSignature(rawBody, signature, secret = process.env.CHAMELEON_WEBHOOK_SECRET) {
  if (!secret || typeof signature !== 'string' || !HEX_SHA256.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest(); // key = UTF-8 secret
  return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), expected); // validated, so equal length
}
  • Header: X-Chameleon-Signature, the bare lowercase hex digest (no sha256= prefix). Compare the whole value as one digest.
  • Signed content is the raw request body only. No timestamp, id or other header is signed.
  • The key is the account's Webhook Secret used as-is (UTF-8 bytes, not hex- or base64-decoded).
  • Validate the header first (exactly 64 hex characters, either case), then compare in constant time. Python: hmac.compare_digest on bytes.
  • Return 401 for a missing, malformed or mismatching signature. Read the body raw (express.raw({ type: '*/*' })) and parse JSON only after it verifies.
  • Replay: nothing signed carries a time, so there is no replay window to enforce by default. Chameleon's Rails sample also checks the body's sent_at is within 5 minutes; the examples offer that as an opt-in check (CHAMELEON_SENT_AT_TOLERANCE_SECONDS, off by default) that runs after verification.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The body is {id, kind, sent_at, data}. Dispatch on kind; no header names the topic. data holds the included models (profile, tour, survey, response, step, ...). Chameleon also sends X-Chameleon-Id, the same id as the body. Dedupe on it, because non-200 responses are retried.

EventSent When
pingEndpoint health check (verify it, nothing else to do)
tour.started, tour.completed, tour.exited, tour.snoozed, tour.button.clickedA Tour starts, completes, is exited or snoozed, or a button is clicked
survey.started, survey.completed, survey.exited, survey.snoozed, survey.button.clickedA Microsurvey starts, completes, is exited or snoozed, or a button is clicked
response.finishedA Microsurvey is finished (all Steps, including any comment)
embed.started, embed.completed, embed.exited, embed.snoozed, embed.button.clickedThe same lifecycle for Embeds
demo.started, demo.reveal, demo.finished, demo.form.submitted, demo.email.addedA Demo starts, identifies a company, finishes, has a form submitted or has an email added
helpbar.search, helpbar.answer, helpbar.item.action, helpbar.item.errorHelpBar results shown, AI answer generated, item actioned or an item action errors
alert.triggeredAn Alert's conditions are violated

Environment Variables

CHAMELEON_WEBHOOK_SECRET=your_webhook_secret   # Integrations -> Webhooks page in Chameleon
# CHAMELEON_SENT_AT_TOLERANCE_SECONDS=300      # optional: reject a stale or future sent_at with 400

Local Development

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

npx hookdeck-cli listen 3000 chameleon --path /webhooks/chameleon

Use the printed URL as the endpoint URL in Chameleon.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →