ruLog in to Senler

Application Webhooks

Application webhooks

On the Webhooks page, a developer application sends selected events to an external URL. One webhook can listen for several event types, and one application can have several webhooks with different URLs and event sets.

Use HTTPS for a production endpoint and respond promptly: Senler waits no more than 10 seconds.

Available events

  • command_start — the user sent /start; data contains command and args;
  • message_allow — the user allowed messages;
  • message_new — a new user message; its text is in data.content;
  • lead_created — a lead was created;
  • lead_unsubscribed — the user unsubscribed from messages;
  • lead_blocked — the user blocked the bot;
  • message_undelivered — a message was not delivered;
  • error — event processing raised an error.

For subscription- and error-related events, data can contain key and error_message. Fields without a value arrive as null; do not require every common field for every event type.

Creating a webhook

  1. Click Create Webhook.
  2. In the creation form, enter the full handler URL.
  3. Select one or more event types.
  4. Click Create.

A new webhook immediately receives Active status.

Secret and signature

After creation, a secret dialog opens. Use the copy action, store the value in a protected secret store, and only then click Done. The full secret is shown once; the list retains only its short prefix.

Every request contains these headers:

  • Content-Type: application/json;
  • X-App-Id — the application's Client ID;
  • X-Webhook-Timestamp — the event time, matching timestamp in the body;
  • X-Webhook-Event-Id — the unique event ID, matching event_id in the body;
  • X-Webhook-Signature — a hexadecimal HMAC-SHA256 string.

The signature protects the complete request body. JSON is first canonicalized: keys in every object are sorted, array order is preserved, properties with an undefined value are omitted, and undefined inside an array becomes null. The signature version, time, event ID, and canonical JSON are then joined:

import { createHmac, timingSafeEqual } from "node:crypto";

function canonicalize(value) {
  if (value === null || ["boolean", "string"].includes(typeof value)) {
    return value;
  }
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map((item) =>
      item === undefined ? null : canonicalize(item),
    );
  }
  if (typeof value === "object") {
    return Object.fromEntries(
      Object.keys(value)
        .sort()
        .filter((key) => value[key] !== undefined)
        .map((key) => [key, canonicalize(value[key])]),
    );
  }
  throw new TypeError("Unsupported webhook payload value");
}

const canonicalPayload = JSON.stringify(canonicalize(payload));
const signedContent = [
  "v1",
  payload.timestamp,
  payload.event_id,
  canonicalPayload,
].join(".");
const expectedHex = createHmac("sha256", webhookSecret)
  .update(signedContent, "utf8")
  .digest("hex");
const expected = Buffer.from(expectedHex, "hex");
const received = Buffer.from(request.headers["x-webhook-signature"] ?? "", "hex");
const signatureIsValid =
  received.length === expected.length && timingSafeEqual(received, expected);

Before processing, make sure X-Webhook-Timestamp and X-Webhook-Event-Id exactly match the body fields, then validate the signature, X-App-Id, allowed project_id, event type, and a freshness window appropriate for your integration. Never write the secret to logs or send it in chat.

Request format

A production request has this common shape:

{
  "event_id": "019c5a23-8b7c-7f10-a4dd-c4f4b650032a",
  "event_type": "message_new",
  "timestamp": "2026-07-10T12:00:00.000Z",
  "project_id": "project-id",
  "channel_id": "channel-id",
  "channel_type": "telegram",
  "lead_id": "lead-id",
  "dialog_id": "dialog-id",
  "platform_user_id": "platform-user-id",
  "data": {
    "content": "Message text"
  }
}

data depends on event_type. Do not make the handler depend on fields unrelated to the selected event. Use event_id as the idempotency key: retain accepted IDs and do not execute the same event twice.

Testing and retries

Each webhook in the list has a Test action. It sends one request with its own event_id, event_type: "test", project_id: "test", null identifiers, and a message in data. Headers and the signature follow the same rules as a production event. Handle this service event separately from the production event list.

The test succeeds only for an HTTP 200-299 response and reports the status code and response time. It does not start a retry sequence.

For a production event, Senler makes up to three attempts. Each attempt has a 10-second timeout; after the first failure it waits about 1 second, and after the second about 5 seconds. A 2xx response completes delivery, while network errors and other HTTP statuses cause another attempt. All attempts retain the same event_id, timestamp, body, and signature. Recognize a retry by event_id and return 2xx when the event was already accepted durably.

Managing and replacing a secret

A card shows selected events, status, last-triggered time, and the last HTTP status. Use the status toggle to disable and enable a webhook; a disabled webhook receives no production events.

The current page cannot edit an existing webhook's URL or event set or reveal its full secret again. To replace it, create a new webhook, store its secret, switch the external handler, run a test, and only then delete the old webhook.

The delete webhook action opens a confirmation. After final deletion, delivery to that URL stops and the secret cannot be recovered.

If events do not arrive

  1. Make sure the webhook has Active status and the required event type is selected.
  2. Run Test and inspect the last HTTP status and trigger time.
  3. Verify that the external HTTPS URL is reachable and responds within 10 seconds.
  4. Make sure the handler accepts Content-Type: application/json and the service event_type: "test".
  5. Compare X-App-Id, X-Webhook-Timestamp, and X-Webhook-Event-Id with the request body.
  6. Canonicalize the full body, build v1.timestamp.event_id.canonicalPayload, and verify HMAC-SHA256.
  7. Check external server logs by event_id and around the last-triggered time.
  8. Return 2xx only after the event is accepted or durably queued; do not process a repeated event_id again.
  9. If the secret is lost, create a new webhook and replace the old one using the safe sequence above.