Session Lifecycle Webhooks
Receive signed HTTP events when consumers start or end a chat session, including the URL query params they arrived with.
Session webhooks let you receive a signed HTTP POST to your own endpoint whenever a consumer starts or ends a chat session on your Chipp app. This lets you track conversation starts and ends deterministically, without depending on the AI to call a custom action.
Session webhooks are the deterministic complement to {{system.query.*}} variables. The variables let your AI agent pass attribution data to a custom action at the right moment in the conversation. Session webhooks give you the same data automatically, at session start, with no model involvement.
When to Use Session Webhooks
Use session webhooks when you need reliable, automatic notification that a session started or ended, such as:
- Logging every session start to your analytics or CRM (no matter what the AI does)
- Binding a tokenized URL parameter (like
?rp_token=abc123) to a session record in your system - Triggering a workflow when a user opens the chat
For actions that depend on what happened in the conversation (a form submitted, a product selected, a service requested), use custom actions instead, since those let the AI decide when to fire based on context.
Configure the Webhook
- Open your app in the Chipp builder.
- Go to Access in the left navigation.
- Scroll to the Session Webhooks section.
- Enter your endpoint URL (must be
https://). - Choose which events to receive:
session.created,session.ended,integration.tools_dropped,integration.tools_restored, or any combination. - Click Save.
On first save, Chipp generates a signing secret and shows it once. Copy it immediately and store it somewhere secure, like an environment variable. You will not be able to retrieve it again. To replace it later, use the rotate button.
Send a Test Event
After saving, click Send test event. Chipp posts a synthetic session.created payload flagged with "test": true to your endpoint so you can confirm it receives and verifies the signature before relying on it in production.
Payload Shape
Both event types share the same envelope. Fields specific to session.ended are noted.
{
"event": "session.created",
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"sessionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source": "web",
"consumerId": "c3f8b2a0-1234-5678-abcd-ef0123456789",
"externalId": "user-from-crm-456",
"queryParams": [
{ "name": "rp_token", "value": "abc123" },
{ "name": "ab_variant", "value": "B" }
],
"startedAt": "2026-07-09T14:22:31.000Z",
"timestamp": "2026-07-09T14:22:31.412Z"
}For session.ended, the payload also includes endedAt:
{
"event": "session.ended",
"endedAt": "2026-07-09T14:45:02.000Z",
...
}Field Reference
| Field | Type | Description |
|---|---|---|
event | string | "session.created" or "session.ended" |
applicationId | UUID | Your app’s ID |
sessionId | UUID | The chat session ID (stable, matches the Conversations tab) |
source | string | How the session was initiated: "web", "widget", "whatsapp", "slack", etc. |
consumerId | UUID or null | The consumer’s ID, or null for anonymous sessions before sign-in |
externalId | string or null | The value of ?chipp_external_id= from the URL (colon-free values only, see below) |
queryParams | array | All captured URL query params as [{name, value}] pairs (excludes Chipp internal params like bt and session) |
startedAt | ISO 8601 | When the session was created |
endedAt | ISO 8601 or absent | When the session ended (only on session.ended) |
timestamp | ISO 8601 | When this webhook was fired |
Integration Tool Events (MCP)
If your app is grounded on an MCP integration, two additional opt-in events tell you when that integration’s tools stop being available to the model, and when they come back. Without these, a tool dropout on a client-facing app is only discoverable when a user hits it.
integration.tools_dropped fires when the runtime confirms, on a real chat turn, that an active MCP integration has zero tools available to the model. It fires once per outage (at most once per 24 hours per integration while the condition persists), so you can alert on it directly without rate-limiting on your side.
{
"event": "integration.tools_dropped",
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"integrationId": "9f1c2b3a-4d5e-6789-abcd-ef0123456789",
"integrationName": "Report Library",
"serverUrl": "https://mcp.example.com/mcp",
"toolCount": 0,
"reason": "discovery_failed",
"error": "MCP server returned 401",
"timestamp": "2026-07-17T14:22:31.412Z"
}integration.tools_restored fires when the tools become available again (automatic recovery or a manual reconnect). It only fires if a tools_dropped event was previously announced for that integration, so a first-time connect never produces a spurious restore.
{
"event": "integration.tools_restored",
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"integrationId": "9f1c2b3a-4d5e-6789-abcd-ef0123456789",
"integrationName": "Report Library",
"serverUrl": "https://mcp.example.com/mcp",
"toolCount": 12,
"timestamp": "2026-07-17T15:01:07.891Z"
}Integration Event Field Reference
| Field | Type | Description |
|---|---|---|
event | string | "integration.tools_dropped" or "integration.tools_restored" |
applicationId | UUID | Your app’s ID |
integrationId | UUID | The MCP integration’s ID (matches list_mcp_integrations) |
integrationName | string or null | The integration’s display name |
serverUrl | string or null | The MCP server URL configured on the integration |
toolCount | number | Active tools after the observation: 0 on dropped, the recovered count on restored |
reason | string or absent | Dropped only: "discovery_failed" (the server could not be reached or errored) or "server_returned_zero_tools" (reachable, but reported an empty tool list) |
error | string or absent | Dropped with discovery_failed only: a short, credential-redacted error snippet |
timestamp | ISO 8601 | When this webhook was fired |
These events cover integrations connected to an external MCP server URL. For point-in-time health checks (rather than push notification), poll list_mcp_integrations and watch the toolAvailability and toolCount fields.
URL Query Param Capture
When a consumer opens your Chipp app with URL query parameters, those parameters are captured on the session automatically. They appear in the webhook payload under queryParams as an array of {name, value} objects.
For example, if a consumer arrives via:
https://your-app.on.chipp.ai?rp_token=abc123&ab_variant=BThe webhook payload will include:
"queryParams": [
{ "name": "rp_token", "value": "abc123" },
{ "name": "ab_variant", "value": "B" }
]The externalId Field
The ?chipp_external_id= query parameter is special. Chipp promotes it to the first-class externalId field on the session record, which also appears in the Conversations tab and all data exports.
Restrictions: the value must not contain a colon (:) so it cannot collide with Chipp’s internal channel namespaces (like webhook:, whatsapp:, voice:). If the value contains a colon it is still captured in queryParams.chipp_external_id but not promoted to externalId.
The same variables are also available inside the agent at {{system.external_id}} and {{system.query.rp_token}}. Session webhooks fire these values deterministically at session start, before the first AI turn.
Verifying Signatures
Chipp signs every webhook payload with HMAC-SHA256 using your signing secret. The signature is sent in the X-Chipp-Signature header as sha256=<hex>.
Always verify the signature before acting on a webhook payload. This prevents replay attacks and ensures the request came from Chipp.
Node.js Verification Example
import crypto from "node:crypto";
function verifyChippSignature(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Use a constant-time comparison to prevent timing attacks
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express example
app.post("/session-webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-chipp-signature"];
if (!verifyChippSignature(req.body, sig, process.env.CHIPP_WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
// Handle event...
res.json({ ok: true });
});Python Verification Example
import hmac
import hashlib
def verify_chipp_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)Always verify against the raw request body bytes, not a parsed JSON object. Re-serializing the parsed object can produce different byte ordering, causing verification to fail.
Request Headers
Chipp includes these headers on every webhook request:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Chipp-Signature | sha256=<hmac-sha256-hex> |
X-Chipp-Event | The event type: session.created, session.ended, integration.tools_dropped, or integration.tools_restored |
User-Agent | Chipp-Session-Webhook/1.0 |
Reliability Notes
session.created
Fires reliably for every new session, regardless of channel or auth mode. Anonymous web sessions, widget sessions, WhatsApp, Slack, and email sessions all produce a session.created event.
session.ended
Fires when a session is explicitly ended or deleted, for example when a consumer clears their history or a session is cleaned up server-side. Abandoned web sessions do not trigger session.ended because a user closing a browser tab is not detectable server-side. Plan accordingly: use session.created for start-of-conversation attribution, and treat the absence of session.ended as normal for web sessions.
integration.tools_dropped / integration.tools_restored
The dropped event is detected on real chat traffic: the runtime checks tool availability when it assembles the model’s tool set for a turn, so a dropout on an idle app is announced on the first conversation that would have been affected. One announcement per outage: repeat observations of the same broken integration are suppressed for 24 hours. The restored event clears that suppression, so a new outage after a recovery is announced immediately. These events currently cover integrations connected to an external MCP server URL (not sandboxed custom-code integrations or integrations linked from another app).
Delivery
Chipp makes a best-effort delivery with a 10-second timeout. Failed deliveries are not automatically retried. If your endpoint is unavailable, the event is lost. Design your endpoint to be idempotent (use sessionId + event as a dedup key) since at-least-once delivery means duplicates are possible in rare conditions.
Your endpoint must return a 2xx status within 10 seconds. Slow responses that time out are logged at warn level and treated as delivery failures.
Rotating the Signing Secret
To rotate your signing secret:
- Click the rotate button (circular arrow) next to the signing secret field.
- Copy the new secret immediately. It is shown only once.
- Update your endpoint to accept the new secret.
During the rotation window, requests signed with the old secret will fail verification at your endpoint. Brief downtime is expected. Rotate during low-traffic periods if that is a concern.
Related
- Bring Your Own Auth - pre-authenticate users from your own backend; session webhooks are how your backend learns which session belongs to which user
- Custom actions - let the AI call your endpoints based on conversation context
- System variables - access
{{system.query.NAME}}and{{system.external_id}}inside the agent - Inbound webhooks - the reverse direction: send messages into your app from external systems