GTM Clarity Docs

Webhooks

Register webhook endpoints, consume signed deliveries, and verify signatures.

Webhooks send customer events to your HTTPS endpoint as signed JSON POSTs. Use https://app.gtmclarity.ai/api/v1 as the API base URL.

Register an endpoint

Register a customer webhook endpoint with a write key:

curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints \
  -X POST \
  -H 'Authorization: Bearer gtmc_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com/webhooks/gtmclarity",
    "description": "Production webhook",
    "events": ["conversation.started", "conversion.recorded"]
  }'

Omit events or send null to receive all events.

The response includes the endpoint and a signing secret:

{
  "endpoint": {
    "id": 7
  },
  "secret": "whsec_..."
}

The whsec_... secret is shown once. Store it immediately.

Manage endpoints

Use the webhook endpoint API to list, read, update, delete, rotate secrets, and inspect deliveries:

curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints \
  -H 'Authorization: Bearer gtmc_your_key_here'
curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints/7 \
  -H 'Authorization: Bearer gtmc_your_key_here'
curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints/7 \
  -X PATCH \
  -H 'Authorization: Bearer gtmc_your_key_here' \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Primary production webhook"
  }'
curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints/7 \
  -X DELETE \
  -H 'Authorization: Bearer gtmc_your_key_here'
curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints/7/rotate-secret \
  -X POST \
  -H 'Authorization: Bearer gtmc_your_key_here'

Delivery logs are cursor paginated. Each delivery includes attempt, status, and nextRetryAt.

curl https://app.gtmclarity.ai/api/v1/customers/42/webhook-endpoints/7/deliveries \
  -H 'Authorization: Bearer gtmc_your_key_here'

Receive deliveries

GTM Clarity sends a JSON POST:

{
  "event": "conversation.started",
  "data": {
    "customerId": 42,
    "orgId": "org_2h9x4kQ",
    "conversationId": 118,
    "visitorId": "6f9d2c1e-8a3b-4f70-9c55-2d41e0b7a9c3"
  },
  "ts": 1784448000
}

Each request includes:

  • X-GTMC-Signature: t=<ts>,v1=<hexHmac>
  • X-GTMC-Timestamp

The delivery timeout is 5 seconds. Respond with a 2xx quickly and do work asynchronously.

Events

customerId and orgId are always present in data.

EventAdditional data keys
conversation.startedconversationId, visitorId
conversation.endedconversationId, status, converted
identity.resolvedconversationId, resolvedPersonId, company, firstName, lastName, jobTitle, email
conversion.recordedconversionEventId, conversationId, conversionType, value
resolution.recordedresolutionEventId, conversationId, resolvedPersonId, resolvedCompany
handoff.requestedescalationId, conversationId, schemaName, urgency, reason
outcome.markedconversationId, category — the ladder rung a GTM reviewer marked the chat with, or null when the mark was cleared

conversation.ended is at-least-once. A bot conversion followed by an operator close re-fires the event with the new terminal status. Consumers must be idempotent on (event, conversationId, status).

Retries

Attempts 1 and 2 happen inline. Later retries happen after approximately 5 minutes, 30 minutes, and 2 hours. After 5 attempts, the delivery is terminal. Inspect terminal and retrying deliveries through the deliveries endpoint.

Verify signatures

Verify the signature against the raw request body string before JSON parsing. The signed payload is ${timestamp}.${rawBody}. Reject requests when |now - t| is greater than your tolerance; 300 seconds is recommended.

import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';

export function verifyGtmcSignature(
  secret: string,
  header: string,
  rawBody: string,
  toleranceSeconds = 300,
): boolean {
  const parts = new Map(
    header.split(',').map((part) => {
      const [key, ...rest] = part.split('=');
      return [key, rest.join('=')];
    }),
  );

  const timestamp = Number(parts.get('t'));
  const signature = parts.get('v1');
  if (!Number.isInteger(timestamp) || !signature) {
    return false;
  }

  const nowSeconds = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const actualBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  if (actualBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}

On this page