Heppu APIv2.heppu.ai/api

Webhooks

We POST to you when a conversation ends. Verify the signature, respond 2xx fast.

Set up per worker in the dashboard (Workforce, pick a worker, Webhooks) or per job under Result delivery. Job config wins when both exist. HTTPS only, no private IPs.

Events

EventWhen
call.completedCall ended normally. Has summary.
call.no-answerRang out.
call.busyBusy tone.
call.failedCarrier or system error.
sms.sent sms.delivered sms.failed sms.undeliveredSMS lifecycle.
chat.completed chat.resolved chat.abandoned chat.failedChat ended. Job-level config only.

Tick the ones you want. Unticked events are never sent.

Payload

{
  "event": "call.completed",
  "timestamp": "2026-09-11T10:00:00.000Z",
  "webhookId": "9c1b…",
  "organizationId": "…",
  "agentId": "…",
  "agentName": "Collections",
  "conversationId": "…",
  "conversationType": "phone",
  "status": "completed",
  "outcome": "resolved",
  "summary": "Customer confirmed the invoice was paid on Monday. No further action.",
  "duration": 125,
  "direction": "outbound",
  "agentNumber": "+358…",
  "twilioCallSid": "CA…",
  "metadata": { "contactName": "Ada", "contactPhone": "+358…", "contactEmail": "ada@example.com" },
  "callMetadata": { "orderId": "A-1041" },
  "goal": { "configured": true, "achieved": true, "detectedBy": "ai", "confidence": 0.92 },
  "jobId": "…",
  "jobName": "Overdue invoices",
  "jobExecutionId": "…",
  "attemptNumber": 1,
  "structuredOutcome": "resolved"
}

Always present: event, timestamp, webhookId, organizationId, agentId, agentName, conversationId, conversationType, metadata. The rest depends on type.

  • Phone: status, outcome, summary, duration, direction, agentNumber, twilioCallSid, callMetadata. summary is only filled on call.completed.
  • Chat: duration, messageCount, chatMetadata with resolved, feedback, toolsUsed, firstMessageAt, lastMessageAt.
  • SMS: messageSid, fromNumber, toNumber, messageBody, errorCode.
  • goal only when goal tracking is on for the worker.
  • jobId … structuredOutcome only when the conversation came from a job.

Ignore fields you don't know. We add, we don't remove.

Headers

Content-Type: application/json
User-Agent: Heppu-Webhooks/1.0
X-Webhook-Event: call.completed
X-Webhook-ID: 9c1b…
X-Webhook-Timestamp: 1757584800
X-Webhook-Attempt: 1
X-Webhook-Signature-256: sha256=…

Plus any custom headers you configured.

Verify the signature

HMAC-SHA256 of the raw body with your secret, hex, prefixed sha256=. Only sent if you set a secret. Set a secret.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  return header?.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header or "", expected)

Sign the raw bytes, not a re-serialized object. The timestamp header is not part of the signed material, so check it yourself if you want replay protection.

Delivery and retries

  • Timeout is per config, default 30s, allowed 5 to 300. Respond 2xx and do the work after.
  • Failed deliveries retry 3 times with exponential backoff, then land in the dashboard as max_retries_exceeded. You can retry from there by hand.
  • Max 30 deliveries per minute per org, 5 concurrent. Bursts queue, they don't drop.
  • call.failed and sms.failed jump the queue.
  • webhookId is one UUID per event, the same across retries. Use it to dedupe.

On this page