Convot Convot
Back to Installing the Widget
Installing the Widget

App Webhooks

Send real-time event notifications to your own backend whenever conversations, messages, or contacts change in your Convot app.

Updated June 11, 2026

Webhooks let you receive an HTTP POST to your own server whenever something happens in your Convot app - a visitor sends a message, a conversation is resolved, a new contact is captured. Each request is signed with HMAC-SHA256 so you can verify it actually came from Convot.

Find webhooks at Settings - Apps - [your app] - App Webhooks.

Creating a webhook

  1. Go to Settings - Apps - [your app] - App Webhooks.
  2. Click the Webhook button (top-right).
  3. Enter your Endpoint URL - must start with https://.
  4. Check every event you want to receive.
  5. Click Create webhook.

Convot generates a signing secret automatically on creation. You cannot set your own secret.

Creating a new webhook in the App Webhooks page

Events

💡
Event When it fires
message.sent An agent sent a reply to a visitor.
message.received A visitor sent a message.
message.deleted Any message was deleted.
conversation.created A new conversation started.
conversation.assigned A conversation was assigned to an agent.
conversation.resolved A conversation was marked as resolved.
conversation.reopened A resolved conversation was reopened.
contact.created A new contact was captured by this app.
contact.identified An anonymous visitor was matched to a known user.

You can subscribe a single endpoint to any combination of events.

Editing a webhook

Click Edit on any webhook row to open the edit modal. You can change the URL and update the event selection. Click Update webhook to save.

Enabling and disabling a webhook

Each webhook row has a toggle switch. Flipping it on or off enables or disables delivery without deleting the webhook. Disabled webhooks receive no events.

Testing a webhook

Click Test on any webhook row to send a sample message.received event to the endpoint immediately. A green “Test sent” flash appears if the delivery was queued. Use this to confirm your endpoint is reachable and your signature verification code works before going live.

Deleting a webhook

Click the trash icon on any webhook row and confirm the prompt. Deletion is permanent - the webhook and its signing secret are removed and cannot be recovered. The last delivery status shown in the row is also gone.

Signing secret

Each webhook has its own signing secret, formatted as whsec_ followed by 48 hex characters. Click Show to reveal it, then Copy to copy it to your clipboard.

⚠️

Keep your signing secret out of client-side code and out of version control. Anyone who has the secret can construct a valid signature and forge webhook calls to your server.

The secret cannot be changed after creation. If it is compromised, delete the webhook and create a new one.

Payload format

Every webhook request is an HTTPS POST with:

Content-Type: application/json
User-Agent: Convot-Webhooks/1.0
X-Convot-Event: message.received
X-Convot-Timestamp: 1744540868
X-Convot-Signature: t=1744540868,v1=<hex-hmac-sha256>

Example payload body:

{
  "id": "8b8d2b7e-1d2f-4f3a-9c7a-6b1f2c3a4e5b",
  "event": "message.received",
  "created_at": "2026-04-13T10:21:08Z",
  "app_id": "app_3f9c1a2b4d5e6f7a8b9c0d1e",
  "data": {
    "id": 4821,
    "conversation_id": 219,
    "content": "Hi, I need help with my order",
    "message_type": "incoming",
    "sender_type": "Contact",
    "sender_id": 88,
    "sender_name": "Jane Doe",
    "created_at": "2026-04-13T10:21:08Z"
  }
}

The top-level id is a UUID unique to each delivery. Use it for idempotency.

Verifying the signature

The X-Convot-Signature header contains two parts separated by a comma: t=<unix-timestamp> and v1=<hex>.

The signature is an HMAC-SHA256 of {timestamp}.{raw-body} using your webhook’s signing secret as the key.

Verification algorithm:

  1. Split the header on , to get timestamp and signature.
  2. Reject the request if the timestamp is more than 5 minutes in the past or future (replay protection).
  3. Compute HMAC-SHA256(secret, "{timestamp}.{raw_request_body}").
  4. Compare the result to the v1= value using a timing-safe comparison.

Node.js:

import express from "express";
import crypto from "crypto";

app.post(
  "/webhooks/convot",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sigHeader = req.header("X-Convot-Signature") || "";
    const [tPart, v1Part] = sigHeader.split(",");
    const timestamp = (tPart || "").split("=")[1];
    const signature = (v1Part || "").split("=")[1];

    if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(400).send("stale");
    }

    const expected = crypto
      .createHmac("sha256", process.env.CONVOT_WEBHOOK_SECRET)
      .update(`${timestamp}.${req.body}`)
      .digest("hex");

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
      return res.status(401).send("bad signature");
    }

    const payload = JSON.parse(req.body);
    // process payload...
    res.sendStatus(200);
  }
);

Ruby on Rails:

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def convot
    raw_body = request.body.read
    sig_header = request.headers["X-Convot-Signature"].to_s
    timestamp  = sig_header[/t=([^,]+)/, 1]
    signature  = sig_header[/v1=([^,]+)/, 1]

    if timestamp.nil? || (Time.now.to_i - timestamp.to_i).abs > 300
      return head :bad_request
    end

    expected = OpenSSL::HMAC.hexdigest(
      "SHA256",
      ENV.fetch("CONVOT_WEBHOOK_SECRET"),
      "#{timestamp}.#{raw_body}"
    )

    unless ActiveSupport::SecurityUtils.secure_compare(expected, signature.to_s)
      return head :unauthorized
    end

    payload = JSON.parse(raw_body)
    # process payload...
    head :ok
  end
end
ℹ️

Use express.raw() in Node.js (not express.json()) to get the raw bytes needed for HMAC. Parsing the body as JSON before hashing will produce a different signature.

Retries and response requirements

Respond with any 2xx status within 10 seconds. Any other response (non-2xx, timeout, or connection error) is treated as a failure and triggers automatic retries with exponential backoff.

💡
Best practice Why
Respond 200 immediately, then process asynchronously. Prevents timeouts on slow processing paths.
Deduplicate on the top-level id field. The same event may arrive more than once on retry.
Use HTTPS only. The secret is transmitted in a signed header - use TLS in production.

Delivery status

The last HTTP status code returned by your endpoint is shown on the webhook row. A green “2xx OK” badge means the last delivery succeeded. A red “Failed” badge with the status code means it did not. Use Test to trigger a new delivery and see the result.

Next steps

Was this article helpful?