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-Signatureheader? - How do I handle
tour.completed,survey.completedorresponse.finishedevents? - 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 (nosha256=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_digeston bytes. - Return
401for 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_atis within 5 minutes; the examples offer that as an opt-in check (CHAMELEON_, off by default) that runs after verification.SENT_ AT_ TOLERANCE_ SECONDS
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The body is {id, kind, sent_. 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.
| Event | Sent When |
|---|---|
ping | Endpoint health check (verify it, nothing else to do) |
tour.started, tour.completed, tour.exited, tour.snoozed, tour.button.clicked | A Tour starts, completes, is exited or snoozed, or a button is clicked |
survey.started, survey.completed, survey.exited, survey.snoozed, survey.button.clicked | A Microsurvey starts, completes, is exited or snoozed, or a button is clicked |
response.finished | A Microsurvey is finished (all Steps, including any comment) |
embed.started, embed.completed, embed.exited, embed.snoozed, embed.button.clicked | The same lifecycle for Embeds |
demo.started, demo.reveal, demo.finished, demo.form.submitted, demo.email.added | A Demo starts, identifies a company, finishes, has a form submitted or has an email added |
helpbar.search, helpbar.answer, helpbar.item.action, helpbar.item.error | HelpBar results shown, AI answer generated, item actioned or an item action errors |
alert.triggered | An 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
- references/overview.md - What Chameleon webhooks are, events and payload shape
- references/setup.md - Add the endpoint and find the Webhook Secret
- references/verification.md - Signature details, gotchas, debugging failures
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests