Webhooks

Webhooks

Receive real-time HTTP callbacks when things happen in your workspace — conversations, messages, tickets, and CSAT ratings.

Overview

A webhook is an HTTPS endpoint you host. AwnCare POSTs a signed JSON event to it whenever a subscribed event occurs.

Use webhooks to sync tickets to your systems, trigger workflows, alert on new conversations, or feed a data warehouse — without polling the API.

Managing endpoints

Create and manage endpoints in the Developer Console (or via the REST API). Each endpoint has a URL, a set of subscribed events, and its own signing secret.

Save your signing secret

The signing secret is shown only once, when the endpoint is created. Store it securely — you will need it to verify signatures.

Event catalog

The events you can subscribe an endpoint to.

EventDescription
conversation.createdA new conversation was started (widget or integration channel).
message.createdA message was sent in a conversation (inbound or outbound).
ticket.createdA new support ticket was created.
ticket.status_changedA ticket transitioned to a new status.
ticket.comment_createdA public reply was added to a ticket (internal notes excluded).
csat.receivedA customer submitted a satisfaction rating.

Event envelope

Every delivery has the same envelope: a stable delivery id, the event name, a timestamp, and the event data.

json
{
  "id": "dlv_20",
  "event": "ticket.created",
  "createdAt": "2026-07-31T09:01:00.000Z",
  "data": {
    "id": "tkt_5",
    "number": 1043,
    "subject": "Cannot log in",
    "status": "OPEN"
  }
}

Two headers accompany each delivery — the event name and the signature.

http
X-AwnCare-Event: ticket.created
X-AwnCare-Signature: t=1753952460,v1=4f8c1e…hex-hmac…

Verifying signatures

Always verify the signature before trusting a payload.

The X-AwnCare-Signature header has the form t=TIMESTAMP,v1=SIGNATURE. Compute an HMAC-SHA256 over the string made of the timestamp, a dot, then the raw request body (that is, TIMESTAMP.RAWBODY) using your endpoint's secret, and compare it to v1 in constant time. Always verify against the raw request body, before JSON parsing.

Node.js (Express + crypto)

js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.AWNCARE_WEBHOOK_SECRET; // whsec_…

// IMPORTANT: verify against the RAW body, before JSON parsing.
app.post(
  "/hooks/awncare",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.get("X-AwnCare-Signature") || "";
    const rawBody = req.body.toString("utf8");

    // Header format: "t=<unix>,v1=<hex hmac-sha256('<t>.<rawBody>')>"
    const parts = Object.fromEntries(
      header.split(",").map((kv) => kv.split("="))
    );
    const timestamp = parts.t;
    const signature = parts.v1;

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

    const valid =
      signature &&
      crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
      );

    if (!valid) return res.status(400).send("invalid signature");

    // Optional: reject old timestamps to prevent replay.
    const ageSec = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (ageSec > 300) return res.status(400).send("stale timestamp");

    const event = JSON.parse(rawBody);
    console.log("Verified", event.event, event.id);

    res.sendStatus(200); // ack fast — process async
  }
);

app.listen(3000);

PHP (hash_hmac)

php
<?php
// AwnCare webhook receiver — signature verification.
$secret = getenv('AWNCARE_WEBHOOK_SECRET'); // whsec_…
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_AWNCARE_SIGNATURE'] ?? '';

// Parse "t=<unix>,v1=<hex>"
parse_str(str_replace(',', '&', $header), $parts);
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';

$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(400);
    exit('invalid signature');
}

// Optional replay guard.
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit('stale timestamp');
}

$event = json_decode($rawBody, true);
error_log('Verified ' . $event['event'] . ' ' . $event['id']);

http_response_code(200); // ack fast

Retries & failures

Delivery is at-least-once. Failed deliveries are retried with backoff.

  • Backoff schedule after each failed attempt: 1 minute, then 5 minutes, then 30 minutes.
  • Up to 4 attempts total (1 initial + 3 retries) before a delivery is given up on.
  • An endpoint that reaches 10 consecutive failures is automatically disabled.
  • Retries resend a byte-identical body with the same delivery id, so you can dedupe on it.
A delivery counts as delivered on any 2xx response. Respond 2xx quickly and process asynchronously.

Best practices

A few rules keep your integration robust under retries and at scale.

Be idempotent

The same event may arrive more than once. Dedupe on the envelope id.

Acknowledge fast

Return 2xx immediately and do heavy work asynchronously; slow responses count as failures and trigger retries.

Verify, then parse

Check the signature against the raw body before parsing JSON or acting on the event.

Use HTTPS

Endpoints must be HTTPS in production so payloads and signatures stay confidential in transit.