Guide to Navattic Webhooks: Features and Best Practices
Navattic webhooks send your demo engagement data wherever you need it. When a visitor finishes a session in one of your interactive or AI agent demos, Navattic sends the session, the steps they viewed, who they are and which company they work for, so you can score leads, alert the account owner or load the data into your warehouse.
This guide covers what Navattic's Webhook integration sends and when, the two action types, how to authenticate requests with the Authorization header, and the practices that keep a receiver reliable when much of the delivery behaviour is undocumented.
What are Navattic webhooks?
Navattic sends an event to your Webhook integration whenever a visitor session in your demo is marked as closed. There is one event type, session_closed, and two webhook action types that share the same envelope:
- A Visitor event carries the full session, the visitor and their company account.
- An Account event carries only the company account.
Navattic doesn't sign webhook requests. Each request has Content-Type: application/json and User-Agent: navattic-tour, and, only when you configure one on the webhook, an Authorization header with Basic <credentials> or Bearer <token>. That header is the only way to tell a Navattic request from anyone else's.
Navattic webhook features
| Feature | Details |
|---|---|
| Trigger | A visitor session in a demo is marked closed |
| Event type | session_closed, currently the only value |
| Action types | Visitor event or Account event |
| Format | JSON, Content-Type: application/json |
| User agent | navattic-tour |
| Authentication | Optional Authorization header: Basic <credentials> or Bearer <token> |
| Signature and timestamp header | None |
| HTTP method | Not documented |
| Retries and timeout | Not documented |
| Source IPs | Not published |
| Change policy | Navattic aims to minimise changes and gives notice before breaking changes |
Payload
The envelope has three fields: eventType, timestamp (when the session closed, ISO 8601) and data. A Visitor event, trimmed from Navattic's example:
{
"eventType": "session_closed",
"timestamp": "2026-07-14T18:32:05.000Z",
"data": {
"session": {
"type": "INTERACTIVE_DEMO",
"recordedGuideSessionId": "clx8f2k1a0003abcd12345678",
"createdAt": "2026-07-14T18:20:11.000Z",
"closedAt": "2026-07-14T18:32:05.000Z",
"mostRecentDemoName": "Acme Product Tour",
"recordedEvents": [
{
"id": "clx8f3xyz0004abcd12345678",
"type": "VIEW_STEP",
"recordedStepName": "Welcome",
"recordedFlowName": "Getting Started"
}
]
},
"endUser": {
"id": "clx8f1hhh0010abcd12345678",
"mostRecentEmail": "jane.doe@example.com",
"mostRecentCompany": "Example Corp",
"converted": true,
"engaged": true
},
"companyAccount": {
"id": "clx8f4iii0011abcd12345678",
"name": "Example Corp",
"websiteDomain": "example.com",
"employeeCount": 250,
"estimatedAnnualRevenue": "50000000"
}
}
}
A few details matter for parsing:
session.typeisINTERACTIVE_DEMOorAGENT_DEMO, and each adds its own fields.companyAccountisnullon a Visitor event when no account matched.- For an Account event,
datais the company account object itself, not wrapped incompanyAccount. - Large numbers, such as
estimatedAnnualRevenue, are strings. - When a Launchpad presentation is linked to a Salesforce opportunity, the payload adds
opportunityOwnerEmailandopportunityOwnerId.
Nothing in the body names the action type, so check for data.session to tell a Visitor event from an Account event.
Inspect Navattic webhooks as they arrive. Navattic warns that public testing services can expose customer data. A Hookdeck Inspect URL shows the exact method, headers and body Navattic sends, with no account or setup required; send test sessions only.
Setting up Navattic webhooks
Generate a long random token first, for example with openssl rand -hex 32.
- In Navattic, add the Webhook integration.
- Enter your HTTPS endpoint URL.
- Choose the action type: Visitor event or Account event.
- Configure an
Authorizationvalue on the webhook, either a Bearer token or Basic credentials. Without it, Navattic sends noAuthorizationheader at all. - Complete a session in one of your demos and check that the event arrives.
Navattic's docs describe the payload and headers but not every label in the integration screen, so match these steps to what you see there.
With Hookdeck. Navattic is a supported source in Hookdeck. Its setup note reads: "Paste the full Authorization value Navattic sends, including the "Basic " or "Bearer " prefix." Create a source with the Navattic type and use its URL as the webhook URL. In Authorization Header, paste the whole value, for example Bearer your-long-random-token; for Basic, paste Basic followed by the base64 credentials, not the username and password. The source compares the whole header with that value.
Navattic doesn't document the HTTP method, so the source pre-selects POST without managing it. After your first test session, look at the request in Hookdeck; if its method isn't POST, allow that method on the source too, or requests with it won't be accepted. A request with a missing or wrong Authorization value still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
For local development, hookdeck listen 3000 navattic --path /webhooks/navattic gives you a public URL that forwards to your machine.
Securing Navattic webhooks
Compare the whole Authorization header with the value you configured, prefix included, in constant time. There's no need to decode Basic credentials; comparing the full string checks both parts at once.
const crypto = require('crypto');
const express = require('express');
// The full value, e.g. "Bearer your-long-random-token".
const EXPECTED = process.env.NAVATTIC_AUTHORIZATION;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// The method isn't documented, so accept any and check the header first.
app.all('/webhooks/navattic', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('authorization');
if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (event.eventType === 'session_closed') {
const isVisitorEvent = Boolean(event.data && event.data.session);
// Queue the session or account for processing.
}
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
EXPECTED = os.environ.get("NAVATTIC_AUTHORIZATION", "") # e.g. "Bearer your-token"
def verify_navattic_request(headers) -> bool:
if not EXPECTED:
return False # never accept when unconfigured
received = headers.get("Authorization")
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))
A static Authorization value authenticates the sender, not the body, so a modified payload still passes, and nothing stops a captured request from being replayed. Use HTTPS only and a long random value. These payloads carry visitors' names, email addresses and locations, so treat the endpoint and its logs as personal data.
Make Navattic webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationheader, stores every closed session durably, and lets you retry and replay delivery to your CRM or warehouse.
Navattic webhook limitations and pain points
Delivery behaviour is undocumented
The Problem: You don't know the method, how long Navattic waits, or whether a failed request is retried.
Why It Happens: Navattic's Webhook page documents the payload and headers, but not the delivery mechanics.
Workarounds:
- Capture a real request to learn the method, and accept it explicitly.
- Respond with a
200quickly, and alert when a demo with traffic produces no events for longer than usual.
How Hookdeck Can Help: Hookdeck accepts each request as it arrives and stores it, then delivers to your service with retries you configure, so an outage on your side doesn't depend on whether Navattic retries.
One event type, two shapes
The Problem: A handler written for Visitor events breaks on an Account event, or the other way round.
Why It Happens: Both action types send eventType: session_. For Account events, data is the company account itself.
Workarounds:
- Give each action type its own webhook URL or path.
- If they share a URL, check for
data.sessionbefore reading anything else.
How Hookdeck Can Help: Hookdeck filters can route on the presence of data.session, so each shape reaches its own handler.
Basic credentials as a pasted value
The Problem: Hookdeck's source or your receiver rejects every request after you switch between Bearer and Basic.
Why It Happens: The comparison is on the full header string. A Basic value has to be compared as Basic <base64>, and the base64 string changes with any change to the username or password.
Workarounds:
- Prefer a Bearer token: it's one random string, easy to rotate.
- After any change, capture one request and copy the
Authorizationvalue exactly.
How Hookdeck Can Help: Hookdeck keeps failed-verification requests with their headers, so you can compare what Navattic sent with what the source expects.
Best practices
Deduplicate on the session
Use data.session.recordedGuideSessionId for Visitor events, and the account id plus timestamp for Account events, so a repeated delivery doesn't create duplicate CRM activity. See our guide to webhook idempotency.
Treat custom properties as optional
customPropertyValues depends on your workspace's properties and how each was captured (source values such as FORM or ENRICHMENT). Look properties up by name and handle missing ones.
Don't use visitorOpportunityId as an origin signal
Navattic warns that it lives on the visitor record and persists until a newer value replaces it, so every session after a single Launchpad touch carries it forward. Use session.presentation to identify sessions that came through a presentation, and opportunityOwnerEmail to route to the right rep.
Acknowledge fast and process asynchronously
Store the payload, return 200, and push to your CRM in a background job. See why to process webhooks asynchronously.
Conclusion
Navattic's Webhook integration sends a session_closed event when a demo session closes, as a Visitor event with the session, visitor and account or an Account event with the account alone. There's no signature, so configure an Authorization value, compare the full header in constant time, tell the two shapes apart by data.session, and confirm the undocumented method and retry behaviour with a real capture.
Hookdeck Event Gateway checks the header, stores every session, and retries and replays delivery so demo engagement data reaches your systems.
Get started with Hookdeck for free and handle Navattic webhooks reliably in minutes.