Features

Webhooks

Trigger actions in your own systems when tours complete. HMAC-signed payloads with automatic retries.

What webhooks are for

Webhooks let your backend react in real time when something meaningful happens in Geyed. When a tour lifecycle event fires, Geyed sends an HTTP POST to a URL you control. Your server receives the payload, verifies the signature, and does whatever your workflow requires — update a CRM record, mark onboarding complete in your database, trigger a follow-up email, or log the event to your own analytics pipeline.

Webhooks are server-to-server. The Geyed SDK sends events to the Geyed backend, and the backend dispatches the signed HTTP call to your endpoint. Your users' browsers are not involved.

Webhooks are configured per app. Each app can have one endpoint URL.

Supported events

Geyed sends a webhook for the following events:

  • tour_started — a user began a tour
  • tour_completed — a user finished all steps of a tour
  • tour_dismissed — a user closed a tour before finishing

step_viewed events are not delivered to webhooks to avoid high delivery volume. Use the analytics dashboard for step-level data.

Configuring an endpoint

Webhook settings live on the app detail page, not in global settings.

  1. Open your app in the dashboard and scroll to the Webhooks section.
  2. Enter your endpoint URL in the Endpoint URL field. The URL must be publicly reachable and accept HTTPS POST requests.
  3. Toggle Enabled on.
  4. Click Create Webhook.
  5. Copy the Signing Secret that appears. It is shown in full only once — at creation time and when you regenerate it. Store it securely (for example, as an environment variable in your server).

To pause delivery without removing your configuration, toggle Enabled off and save. To remove the webhook entirely, click Delete.

Use the Test button to send a test.ping event to your endpoint and confirm your server is reachable and responding correctly.

Payload shape

Every webhook POST has a JSON body in this shape:

{
  "event": "tour_completed",
  "timestamp": "2026-03-22T14:30:00Z",
  "tour": {
    "id": 42,
    "name": "Onboarding Flow",
    "versionId": 7
  },
  "session": {
    "id": "abc-123-def",
    "userId": "user_123",
    "stepIndex": 4
  },
  "app": {
    "id": 5,
    "name": "My SaaS App"
  }
}

Field notes:

  • event — one of tour_started, tour_completed, tour_dismissed
  • timestamp — ISO 8601 UTC timestamp of when the event fired
  • tour.versionId — the published version of the tour at the time of the event
  • session.id — SDK session identifier; use this to correlate multiple events from the same run
  • session.userId — the userId you passed to Geyed.init(), or null if none was provided
  • session.stepIndex — the step the user was on when the event fired

HMAC signing

Every webhook request includes two headers you should verify:

  • X-Geyed-Signature: sha256={hex-encoded-signature}
  • X-Geyed-Timestamp: {unix-seconds}

The signature is computed as:

HMAC-SHA256(signingSecret, timestamp + "." + rawJsonBody)

where timestamp is the value from X-Geyed-Timestamp and rawJsonBody is the exact bytes of the request body.

To verify the signature on your server (Node.js example):

import crypto from "crypto";

function verifyWebhook(req, signingSecret) {
  const timestamp = req.headers["x-geyed-timestamp"];
  const signature = req.headers["x-geyed-signature"];

  // Reject stale deliveries (older than 5 minutes)
  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (age > 300) throw new Error("Webhook timestamp too old");

  const expected = "sha256=" + crypto
    .createHmac("sha256", signingSecret)
    .update(timestamp + "." + req.rawBody)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    throw new Error("Invalid webhook signature");
  }
}

Always use timingSafeEqual for the comparison to prevent timing attacks. Reject any delivery where the timestamp is more than 5 minutes old to guard against replay attacks.

Retries

If your endpoint returns a non-2xx status code, times out, or is unreachable, Geyed retries the delivery automatically. The retry policy is:

  • Maximum 5 attempts (1 initial attempt plus 4 retries)
  • Exponential backoff between attempts (approximately 30 seconds, 2 minutes, 10 minutes, 30 minutes)
  • 10-second HTTP timeout per attempt
  • Any 2xx response is treated as success; anything else (including connection failures) triggers a retry

Each attempt is logged independently so you can see exactly which attempts succeeded or failed.

Delivery log

The Webhooks section on your app detail page includes a delivery log below the configuration card. The log shows recent webhook attempts with:

  • Timestamp
  • Event type
  • HTTP status code
  • Success or failure badge
  • Duration in milliseconds
  • Attempt number

Expand any row to see the full payload that was sent and any error message returned by your endpoint. The log retains the last 30 days of delivery history.