# 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. > **Note:** 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 1. Open your app in the Chipp builder. 2. Go to **Access** in the left navigation. 3. Scroll to the **Session Webhooks** section. 4. Enter your endpoint URL (must be `https://`). 5. Choose which events to receive: `session.created`, `session.ended`, or both. 6. 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. ```json { "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`: ```json { "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 | ## 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=B ``` The webhook payload will include: ```json "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=`. **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 ```js 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 ```python 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) ``` > **Warning:** 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=` | | `X-Chipp-Event` | `session.created` or `session.ended` | | `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. ### 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: 1. Click the rotate button (circular arrow) next to the signing secret field. 2. Copy the new secret immediately. It is shown only once. 3. 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 - [Custom actions](/docs/guides/custom-actions) - let the AI call your endpoints based on conversation context - [System variables](/docs/guides/variables-reference) - access `{{system.query.NAME}}` and `{{system.external_id}}` inside the agent - [Inbound webhooks](/docs/guides/headless-webhooks) - the reverse direction: send messages into your app from external systems