Logo

Link

Webhooks

Signed, idempotent event delivery.

HMAC-SHA256 signed event delivery for Link AI Voice and Mailer. Event catalog, signature verification, retry semantics, and replay protection.

Webhooks are how Link AI tells your backend that something happened — a call ended, a prospect replied, a campaign hit its send budget. Every delivery is a POST to a URL you control, with a JSON body and two signature headers.

Setup

Add an endpoint in Settings → Developers → Webhooks. Each endpoint receives its own signing secret — keep them narrowly scoped per service so a leak in one environment does not blast the others.

  • Endpoint URL must be HTTPS. localhost is rejected.
  • You may subscribe to a subset of event types per endpoint, or use the wildcard *.
  • Per-workspace limit: 25 active endpoints. Email office@linkaiil.com for more.

Payload envelope

Every event ships in the same wrapper. The data object varies by type; everything else is stable.

example event
{
  "id": "evt_01HFE9XQR4...",
  "type": "voice.call.completed",
  "created_at": "2026-05-21T09:18:44Z",
  "workspace_id": "ws_2b9c1f3a",
  "api_version": "2026-05-01",
  "data": {
    "call_id": "call_01HFE9X1G6...",
    "agent_id": "agt_5fd1a02b",
    "to": "+972541234567",
    "duration_seconds": 142,
    "outcome": "answered",
    "transcript_url": "https://api.linkaiil.com/v1/voice/calls/.../transcript",
    "metadata": { "lead_id": "lead_9182" }
  }
}

Event types

EventFired when
voice.call.queuedA call has been accepted and is waiting for the carrier dial-out.
voice.call.startedThe remote party picked up. Timestamp marks the start of billable minutes.
voice.call.completedCall ended cleanly. Carries transcript URL + outcome.
voice.call.failedCall did not connect (no-answer, busy, blocked, carrier error).
mailer.email.sentA generated email cleared the throttle and shipped via SmartLead.
mailer.reply.receivedA prospect replied. Payload includes the parsed reply body and sentiment.
mailer.campaign.completedEvery lead in a campaign has reached a terminal state.
workspace.plan.upgradedWorkspace switched to a higher tier. Useful for unlocking feature gates downstream.

Verifying signatures

Link AI signs every delivery with HMAC-SHA256 over the raw request body. The hex digest lands in X-Linkai-Signature; the delivery wall-clock timestamp lands in X-Linkai-Timestamp (Unix seconds). Reject anything older than five minutes to neutralise replay.

Node — Express
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.LINKAI_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 300;

app.post(
  "/linkai/webhook",
  // Critical: raw body, NOT express.json().
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-Linkai-Signature") ?? "";
    const timestamp = Number(req.header("X-Linkai-Timestamp") ?? "0");

    if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
      return res.status(401).end("stale");
    }

    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(req.body)
      .digest("hex");

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).end("bad signature");
    }

    const event = JSON.parse(req.body.toString("utf8"));
    handleEvent(event); // your business logic
    res.status(204).end();
  },
);

curl + openssl (for tests)

bash
# Replay a captured payload against your local handler.
BODY=$(cat payload.json)
SECRET=whsec_test
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST http://localhost:3000/linkai/webhook \
  -H "Content-Type: application/json" \
  -H "X-Linkai-Timestamp: $(date +%s)" \
  -H "X-Linkai-Signature: $SIG" \
  --data-raw "$BODY"

Retry semantics

  • Any non-2xx response (or a connect/read timeout over 30 seconds) marks the delivery as failed.
  • Failed deliveries retry with exponential backoff: 1m, 5m, 30m, 2h, 12h, 24h. After six attempts the delivery is dropped and the endpoint is flagged in the dashboard.
  • Twenty-four hours of consecutive failures auto-disable the endpoint. Re-enable from the dashboard once the receiver is back.
  • Replay any delivery from the dashboard. The replayed event carries a fresh signature + timestamp but the same id — your handler should be idempotent on event id.