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. One delivery attempt waits no more than 120 seconds, but the handler should preferably accept the event into its own queue and return 2xx promptly.
The General tab contains public application events, For tools contains requests from HTTP tools, and For steps contains internal automation-step webhooks. All types have request history, but waiting for an agent result applies only to tools.
Delivery statistics
The Webhook statistics section combines delivery for every application webhook. The period selector offers 7, 30, and 90 days.
The metrics separate operations accepted for delivery, delivered, not delivered, processing, and cancelled. Delivery attempts are actual HTTP requests, including automatic retries and test sends. There can therefore be more attempts than accepted events.
Available events
command_start— the user sent/start;datacontainscommandandargs;message_allow— the user allowed messages;message_new— a new user message; its text is indata.content;button_clicked— the user clicked a button in a message;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
On the webhooks page, open the General tab, then:
- Click Add.
- In the creation form, enter a clear webhook name, for example, “New messages to CRM”.
- Enter the full handler URL.
- Select one or more event types with checkboxes. Each option shows a readable description first and the technical payload alias underneath.
- Click Create.

A new webhook immediately receives Active status.
Secret and signature
The Webhook secret section stores one signing secret for every app webhook, including tool calls. You can show or hide it and copy it into the application backend's protected secret storage. Learn more opens a dialog with the headers and HMAC formula.
If the secret is lost or compromised, select Replace secret. In the confirmation dialog, you can cancel or confirm the replacement. The previous secret immediately stops working for every app webhook and tool, so update the application backend without delay.

Every request contains these headers:
Content-Type: application/json;X-App-Id— the application's Client ID;X-Webhook-Timestamp— the time of the current delivery attempt; it changes on retry;X-Webhook-Event-Id— the unique event ID, matchingevent_idin the body;X-Webhook-Signature— a hexadecimal HMAC-SHA256 string.
The signature is calculated from the exact X-Webhook-Timestamp value:
import { createHmac, timingSafeEqual } from "node:crypto";
const deliveryTimestamp = request.headers["x-webhook-timestamp"] ?? "";
const expectedHex = createHmac("sha256", webhookSecret)
.update(deliveryTimestamp, "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, validate the freshness of X-Webhook-Timestamp, the signature, the match between X-Webhook-Event-Id and the body event_id, X-App-Id, the allowed project_id, and the event type. Never write the secret to logs or send it in chat. Do not process the same event_id twice.
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 opens a test dialog with the method, URL, headers, and JSON body of the upcoming request. The request is not sent until you select Run. After the run, the same dialog shows the result, HTTP status, duration, response body, and error reason when applicable.
The test 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 is one immediate request without retries and can wait up to 120 seconds for a response.
A production event is queued with a one-day retry window. After a network error, timeout, HTTP 408, 425, 429, or a 5xx response, Senler makes up to 12 attempts: immediately, then approximately after 1, 5, 15, and 30 minutes, and 1, 2, 4, 8, 12, 18, and 24 hours. Any other 4xx response, including 401 or 403, ends the operation without automatic retries: investigate the cause of the rejection first. After repairing access, a request that received 401 or 403 can be resent manually. Every automatic attempt preserves the same event_id, body timestamp, and request body. A new X-Webhook-Timestamp and signature are created for each attempt. Recognize retries by event_id and return 2xx when the event has already been accepted durably.
Managing and replacing a secret
The webhook list shows the assigned name, URL, 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 new production events.
Open the webhook page to change its name, URL, or checkbox-based public event set, then select Save. The secret is not tied to an individual URL; it remains available in the shared section on the application's webhooks page.

The delete webhook action opens a confirmation. After final deletion, delivery to that URL stops; the shared application secret does not change.
Tool webhooks
The For tools tab shows the application's HTTP tools and their webhooks. The URL, parameters, and execution mode are configured in the tool builder; the webhook page provides an Edit tool action.
The For steps tab shows internal webhooks created for automation steps. The endpoint and contract are configured in the step builder; from the webhook page, you can open its delivery history or return to editing the step.
HTTP tool calls are signed with the application's single webhook secret from the section above. Tools do not have separate secrets.

- in instant mode, the agent waits for one response and there are no automatic retries;
- in wait-for-result mode, the request is queued and the agent continues the step after successful delivery;
- in background mode, the request is also queued, but its response does not continue the agent's current step.
The call body contains event_type: "tool_call", event, application, installation, project, agent, and dialog IDs, optional lead_id, the system name and tool_instance_id, plus the instance's arguments, regular configuration, and protected private_data. Headers and the signature follow the rules in Secret and signature and use the shared application secret. The handler must validate the signature and input, keep private data out of public logs, and return a result that is clear to the agent. Execution modes, timeouts, retry windows, and an exact payload example are documented in Agent tools.
Delivery history and recovery
On the webhook page, the Requests area shows delivery operations that are queued, retrying, completed successfully, or finished with an error. Internal webhooks also show a readable type: an agent tool call or an automation step run. Select an operation in the list to open its details.

In the history filters, enter a full task_id or event_id in the identifier field and select the search button. ID search is exact and does not scan JSON contents. You can also choose a delivery status, problem state, event type, and period from the last 24 hours to the full retention window. Reset filters to return to the initial selection, or refresh the list for current results. Load further records with Show more.
What requires developer attention
The application list shows two separate numbers for each application: current incidents that require developer review and deliveries that did not complete successfully during the selected period. These are different metrics. A historical failure remains in analytics after it has been reviewed, while Developer review contains only current unresolved incidents. In the API, this current count is returned as developer_attention with the corresponding app_id.
The category answers who should investigate the cause:
| Category | What happened | Who acts |
|---|---|---|
Application error (application) | The application handler returned an error or reported a failure in its own logic | The application developer checks the handler |
Project configuration (configuration) | The installed application in a specific project is missing an account, access, or another setting | The project owner or member corrects the setting; this is not an application code defect |
Platform error (platform) | The request did not reach the application because delivery failed inside Senler | Senler resolves the cause; the application developer does not need to reconfigure anything |
Cause not identified (unknown) | The response is insufficient for reliable classification | The developer reviews the request and response manually |
application and unknown receive developer_action_required: true and contribute to the developer-review count. configuration and platform receive developer_action_required: false and do not contribute to that count.
How the category is determined
Senler does not classify an incident from only an HTTP status or error text. It applies this order:
- If delivery fails before the external handler is called, the incident is
platform. - If the handler returns a valid
incidentobject, Senler uses theapplicationorconfigurationcategory declared by the application. - Without an
incidentobject, a5xxresponse is classified asapplication. - Other unexplained failures are classified as
unknown.
The code field does not select a category automatically. It is a stable technical identifier for a cause inside an already determined category. The retryable field separately indicates whether repeating the request without correcting its data may help. For a response without an incident object, the value is derived from the delivery result, such as a network failure, timeout, or HTTP status. In request history, the API returns the resulting incident with code, category, developer_action_required, and retryable fields. Manual resending has an additional exception for an external handler's 401 and 403 responses, described below.
How an application reports the exact cause
To avoid making Senler infer the cause from the HTTP response, an application handler can return safe JSON with an incident object. For example, a payment tool cannot find the account selected by the user in project settings:
{
"message": "Configured Prodamus account is unavailable",
"incident": {
"code": "prodamus_account_unavailable",
"category": "configuration",
"retryable": false
}
}
The object contract is:
code— an optional stable machine-readable cause code from 1 to 128 characters; it starts with a Latin letter or digit and then contains only lowercase Latin letters, digits,.,_, or-; a missing or invalid code is stored asnullwithout discarding the category;category— onlyapplicationorconfiguration; Senler assignsplatformandunknownitself;retryable—trueonly when retrying the same payload without changing its data is genuinely safe and may help;- the application does not send
developer_action_required; Senler derives it from the category.
category and the boolean retryable value are required. If either is missing or invalid, Senler ignores the declared object and applies its regular classification from the failure origin and HTTP status.
The message is for a person, while incident.code is for an agent, logs, and automated diagnostics. Do not put secrets, tokens, personal data, or internal stack traces in either field. Use configuration when the cause is an account or setting of a specific installation; use application when the application handler logic itself is broken.
Retrying and resolving an incident
In the selected operation, inspect the request details: the source JSON, HTTP response, or network error. Below is the list of attempts with the time and result of each delivery. Fix the cause first, then choose the action you need:
- Send again resends the stored request to the webhook URL. The response stays in the journal and does not by itself resume a waiting agent.
- Send again and pass the response to the agent is available for a wait-for-result tool when the operation is linked to an agent and can be retried. After a successful response, the result is passed to the waiting agent to continue its work.
- If no retry is needed, mark the problem as resolved. It is removed from the unresolved count, but the request is not resent and history is retained.
On the mobile screenshot, marker 3 outlines the entire request card. Individual attempts are listed at the bottom of the card.

Resending is available only for an unresolved operation in a retry or failed state. It normally requires incident.retryable: true. The exception is an external handler's HTTP 401 or 403 response: after repairing authentication or permissions, you can resend manually even with retryable: false. The Automatic retry unavailable label does not prevent this manual resend. The exception does not apply to failures inside the platform.
If the resend button is absent, correct the data or configuration and run the originating action again. This creates a request with current data. Resending from the journal does not rebuild parameters: it uses the stored request body.
Before confirming, consider duplicate protection. A manual resend creates a new delivery operation but preserves the original event_id; the body timestamp is updated and other data stays unchanged. Automatic attempts preserve both the original event_id and body timestamp. The handler should therefore identify duplicates by event_id, not by time or an exact JSON match. If the action was already completed, do not create the order or message again; for a tool, return the previous result in the expected format.
For several unresolved operations, select all loaded requests or select rows manually. You can resend up to 25 selected requests in one action; each must allow a manual retry. Each creates a separate operation with its original event_id. Mark selected as resolved supports up to 100 requests. Mark all matching the filter as resolved handles up to 1,000 matches; narrow the filter when there are more.
When resolving an incident, select a reason and leave a comment from 10 to 1,000 characters. For a normal review, use reviewed when no fix was needed, or fixed when the cause was corrected. Special cases also support obsolete, superseded, invalid_payload, accepted_loss, task_completed, diagnostic_completed, replay_cancelled, and webhook_deleted. The reason code is stored as resolution_code and the comment as resolution_comment; an agent can use both fields to distinguish a reviewed incident from one that was actually fixed.
Operations are retained for 90 days, unresolved errors until resolution, and individual delivery attempts for 365 days. Marking a problem Resolved neither deletes history nor resends the request; it only removes the problem from the unresolved count.
If events do not arrive
- Make sure the webhook has Active status and the required event type is selected.
- Run Test and inspect the last HTTP status and trigger time.
- Verify that the external HTTPS URL is reachable and responds within 120 seconds.
- Make sure the handler accepts
Content-Type: application/jsonand the serviceevent_type: "test". - Match
X-App-Idto the application's Client ID andX-Webhook-Event-Idto the bodyevent_id. - Validate the freshness of
X-Webhook-Timestampand HMAC-SHA256 of that exact value with the shared application secret. - Open request history and match the saved payload, response, or error with the external server log by
event_id. - Return
2xxonly after the event is accepted or durably queued; do not process a repeatedevent_idagain. - If the secret is lost, replace the shared secret and immediately update every application handler.