Essere Voice API

Webhooks

Get events pushed to your server instead of polling.

Events

Type Fires when
call.completed A call ends and its transcript, summary, and insights are ready.
call.transferred A call was transferred to a human.
appointment.booked An agent booked an appointment.
appointment.cancelled An appointment was cancelled.
lead.captured The agent captured a lead during a call.
usage.threshold_reached The account hit 80% or 100% of its included minutes.

Payload envelope — data is the same schema the REST API returns for the resource:

{
  "id": "evt_3c9d8e7f6a5b4c3d2e1f0a9b",
  "type": "call.completed",
  "created": 1752841265,
  "data": {
    "id": "call_9f8e7d6c5b4a3f2e1d0c9b8a",
    "agent_id": "agt_1a2b3c4d5e6f7a8b9c0d1e2f",
    "status": "completed",
    "summary": "Caller asked about availability; agent booked a viewing.",
    "outcome": "booked"
  }
}

Subscribe

Via the dashboard (Developers → Webhooks) or the API (read_write scope):

curl -X POST https://voice-public-api.essere.ai/v1/webhook-endpoints \
  -H "Authorization: Bearer $ESSERE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/essere/webhook",
    "events": ["call.completed", "appointment.booked"],
    "description": "CRM sync"
  }'

The response includes the signing secret (whsec_...) — shown once. An empty events array subscribes to all event types.

Send a test event any time:

curl -X POST https://voice-public-api.essere.ai/v1/webhook-endpoints/we_5a4b3c2d1e0f9a8b7c6d5e4f/ping \
  -H "Authorization: Bearer $ESSERE_API_KEY"

Verify signatures

Every delivery is signed:

X-Essere-Signature: t=1752841265,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is the hex HMAC-SHA256 of "{t}.{raw request body}" with your whsec_ secret. Verify before trusting the payload, and reject timestamps older than 5 minutes (replay protection). Compute over the RAW bytes — do not re-serialize the JSON.

During a secret rotation the header carries two v1 entries (t=..,v1=<new>,v1=<old>). Your verifier must accept the delivery if any v1 matches — the snippets below already do.

Python (Flask example)

import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["ESSERE_WEBHOOK_SECRET"]  # from endpoint creation


def verify_essere_signature(payload: bytes, header: str, secret: str,
                            tolerance: int = 300) -> bool:
    t = None
    v1s: list[str] = []
    for part in header.split(","):
        key, _, value = part.partition("=")
        if key == "t":
            try:
                t = int(value)
            except ValueError:
                return False
        elif key == "v1":
            v1s.append(value)  # rotation overlap: there may be TWO v1 entries
    if t is None or not v1s:
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + payload,
                        hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, v1) for v1 in v1s)


@app.post("/essere/webhook")
def essere_webhook():
    sig = request.headers.get("X-Essere-Signature", "")
    if not verify_essere_signature(request.get_data(), sig, WEBHOOK_SECRET):
        abort(400)
    event = request.get_json()
    if event["type"] == "call.completed":
        print("call finished:", event["data"]["id"], event["data"]["outcome"])
    return "", 200

Node (Express example)

const crypto = require('node:crypto')
const express = require('express')

const app = express()
const WEBHOOK_SECRET = process.env.ESSERE_WEBHOOK_SECRET // from endpoint creation

function verifyEssereSignature(rawBody, header, secret, toleranceSeconds = 300) {
  let t = null
  const v1s = [] // rotation overlap: the header may carry TWO v1 entries
  for (const part of header.split(',')) {
    const i = part.indexOf('=')
    const key = part.slice(0, i)
    const value = part.slice(i + 1)
    if (key === 't') t = Number(value)
    else if (key === 'v1') v1s.push(value)
  }
  if (!t || v1s.length === 0) return false
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex')
  const a = Buffer.from(expected)
  return v1s.some((v1) => {
    const b = Buffer.from(v1)
    return a.length === b.length && crypto.timingSafeEqual(a, b)
  })
}

// express.raw keeps the exact bytes — required for signature verification
app.post('/essere/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.get('X-Essere-Signature') || ''
  if (!verifyEssereSignature(req.body, sig, WEBHOOK_SECRET)) {
    return res.status(400).end()
  }
  const event = JSON.parse(req.body)
  if (event.type === 'call.completed') {
    console.log('call finished:', event.data.id, event.data.outcome)
  }
  res.status(200).end()
})

app.listen(3000)

Secret rotation

Rotate an endpoint's signing secret any time (dashboard Developers → Webhooks → Rotate secret, or POST /v1/webhook-endpoints/{id}/rotate-secret). The response shows the NEW whsec_ secret once. For the next 24 hours every delivery is signed with both secrets — the header carries two v1 entries — so you can deploy the new secret without dropping verifications. After the overlap, only the new secret signs.

Delivery behavior