Skip to content

Webhooks ​

Webhooks allow external systems to receive real-time notifications whenever important events occur on the SipPulse AI platform. Instead of polling our API, we automatically send an HTTP request to your server as soon as something happens.

Why Use Webhooks? ​

  • Instant response: Receive event data the moment it occurs
  • Scalability: Your system only processes events when necessary, saving resources
  • Automation: Trigger internal workflows (sending emails, updating databases, alerts) without manual intervention

How the Flow Works ​

  1. Registration: You create a webhook by providing a destination URL, an HMAC secret, and specifying which events you want to receive.
  2. Trigger: Whenever a registered event occurs, our platform generates a JSON payload and sends a POST request to your URL.
  3. Validation: Your server validates the signature using the shared secret.
  4. Confirmation: If your API responds with HTTP 200, we consider it delivered; otherwise, we make up to 3 automatic retry attempts.
  5. Logs: You can track each delivery, status, and response in the Event Logs interface.
  6. Retry: If delivery fails, the system retries up to 3 times. You can also manually resend from the logs interface.

Creating a Webhook ​

  1. Navigate to Webhooks in the sidebar menu
  2. Click the Create button
  3. Fill in the configuration:
    • URL: A public endpoint (HTTPS recommended) that will receive the POST requests
    • Secret: A private string you define, used to generate the HMAC signature. Choose a secure value and store it safely — you'll need it to validate request signatures
    • Events: Select the events you want to receive notifications for
  4. Click Save. The status becomes Active and you'll start receiving notifications immediately.

Resource Counters

If your plan has a webhook limit, you'll see a counter (e.g., "2/5 Webhooks") at the top of the page. When the limit is reached, the Create button is disabled.

Available Events ​

Thread Events ​

EventDescription
thread.createdA new conversation was started
thread.closedA conversation was closed
thread.messageA new message was added to a conversation

Voice Events ​

EventDescription
voice.start_callA voice call was initiated
voice.end_callA voice call was ended

Agent Events ​

EventDescription
agent.deployedAn agent was published to a channel
agent.undeployedAn agent was removed from a channel

WhatsApp Business Events ​

EventDescription
whatsapp_business.inboundAn inbound message was received on WhatsApp Business
whatsapp_business.outboundAn outbound message was sent via WhatsApp Business

WhatsApp Personal Events ​

EventDescription
whatsapp_personal.inboundAn inbound message was received on WhatsApp Personal
whatsapp_personal.outboundAn outbound message was sent via WhatsApp Personal

Telegram Events ​

EventDescription
telegram.inboundAn inbound message was received on Telegram
telegram.outboundAn outbound message was sent via Telegram

AI Model Events ​

EventDescription
llm.completionAn LLM response was generated
llm.streamLLM streaming response in progress
stt.transcriptionA speech-to-text transcription was completed
tts.synthesisA text-to-speech synthesis was completed

Billing Events ​

EventDescription
credit.low_balanceCredit balance dropped below the configured threshold
credit.rechargedCredits were recharged (manual or auto-refill)
subscription.changedThe subscription plan was changed
usage.billedA voice session was billed, with its billed quantity and selling price

Knowledge Events ​

EventDescription
knowledge.document_indexedA document was indexed in a knowledge base

Analysis Events ​

EventDescription
structured_analysis.completedA structured analysis (post-call analysis) was completed

Payload Format ​

Each webhook request includes:

Headers

text
x-timestamp: <ISO 8601 timestamp of the event creation>
x-signature: <HMAC-SHA256 of `${body}:${timestamp}`>
Content-Type: application/json

Body (Example)

json
{
  "id": "c93e2b69-88c8-4c19-b85c-1a57d73f6ea7",
  "event": "thread.closed",
  "timestamp": "2025-04-30T12:34:56.789Z",
  "payload": {
    "id": "thr_01966dd65ad074f69cd4346ddcb7a582",
    "title": "Initial Greeting",
    "history": [ /* ... */ ]
  }
}

The usage.billed Event ​

usage.billed fires once for each billed voice session, after the session's billing is committed. It carries the billed quantity and the selling price, which voice.end_call does not.

When It Fires ​

  • After the billing of a voice session commits. Only voice sessions emit it; other products do not.
  • If the platform cannot queue the event, it logs the failure and does not send the event again. Billing is never held up by webhook delivery. To catch a missed event, reconcile with Billed Voice Sessions.

Payload Example ​

json
{
  "id": "5f1d2c3b-7a8e-4f6d-9c0b-1e2f3a4b5c6d",
  "event": "usage.billed",
  "timestamp": "2026-09-15T17:22:02.114Z",
  "payload": {
    "request_id": "vce_a1b2c3d4e5f6g7h8",
    "project_id": "prj_qqqq",
    "agent_id": "agt_zzzz",
    "thread_id": "thr_wwww",
    "room_name": "room_xxxx",
    "event_timestamp": "2026-09-15T17:22:01.000Z",
    "currency": "BRL",
    "total_price": 0.048,
    "items": [
      {
        "charge_subtype": "agent",
        "pricing_rule": "voice",
        "identifier": "agt_zzzz",
        "quantity": 61,
        "unit": "second",
        "billed_quantity": 2,
        "billed_unit": "minute",
        "price": 0.048
      }
    ]
  }
}
  • request_id is the session ID and the deduplication key.
  • quantity and unit are the metered usage. billed_quantity and billed_unit are what was charged: 60-second blocks, rounded up. See Billed quantity and rounding.
  • price and total_price are the selling price, in currency.
  • event_timestamp is the end of the session.

Idempotency ​

Each billed session produces one usage.billed event, identified by its request_id. Store the request_id of every event you process and skip any event you have already seen. A retry of the same delivery carries the same request_id, so it must not be counted twice.

Difference from voice.end_call ​

voice.end_call tells you that a call ended. It has no billing data. usage.billed arrives once the session is billed and carries the billed quantity and the price. Use voice.end_call for call workflows and usage.billed for billing and cost records.

How to Validate the Signature ​

To ensure the request actually came from SipPulse AI and was not altered:

  1. Retrieve the body and the timestamp from the x-timestamp header.
  2. Locally calculate the HMAC-SHA256 of ${payload}:${timestamp} using your secret.
  3. Securely compare it with the x-signature header.
  4. (Optional) Reject requests with a timestamp outside a specific window — for example, ±5 minutes.
typescript
import express, { Request, Response } from 'express';
import { createHmac } from 'crypto';

const app = express();
app.use(express.json());

const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET || '';

function isSignatureValid(payload: string, timestamp: string, signature: string): boolean {
  const message = `${payload}:${timestamp}`;
  const hmac = createHmac("sha256", WEBHOOK_SECRET)
    .update(message)
    .digest("hex");

  return hmac === signature;
}

app.post('/webhook', (req: Request, res: Response) => {
  const payload = JSON.stringify(req.body);
  const timestamp = req.headers['x-timestamp'] as string;
  const signature = req.headers['x-signature'] as string;

  // Check if the timestamp is within a 5-minute window
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) {
    return res.status(400).json({ error: 'Timestamp expired' });
  }

  // Validate the signature
  if (!isSignatureValid(payload, timestamp, signature)) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  // Process the received event
  console.log('Event received:', req.body);
  res.status(200).json({ status: 'ok' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
python
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os
from datetime import datetime, timezone

app = FastAPI()

WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET')

def is_signature_valid(payload: str, timestamp: str, signature: str) -> bool:
    message = f"{payload}:{timestamp}".encode()
    hmac_obj = hmac.new(WEBHOOK_SECRET.encode(), message, hashlib.sha256)
    expected_signature = hmac_obj.hexdigest()
    return hmac.compare_digest(expected_signature, signature)

@app.post("/webhook")
async def webhook(request: Request):
    body = await request.body()
    payload = body.decode()
    timestamp_str = request.headers.get('x-timestamp')
    signature = request.headers.get('x-signature')

    # Check if the timestamp is within a 5-minute window
    event_time = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
    if abs((datetime.now(timezone.utc) - event_time).total_seconds()) > 300:
        raise HTTPException(status_code=400, detail="Timestamp expired")

    # Validate the signature
    if not is_signature_valid(payload, timestamp_str, signature):
        raise HTTPException(status_code=400, detail="Invalid signature")

    # Process the received event
    print("Event received:", payload)
    return {"status": "ok"}

Importance of Signature Validation ​

Verifying the HMAC signature is a critical step for the security of your integration:

Risks of Not Validating ​

  • Spoofing attacks: Anyone who knows your endpoint can send fake payloads simulating events from SipPulse AI
  • Malicious data injection: Without validation, attackers can inject manipulated data to compromise your system
  • Replay attacks: Without checking the timestamp, old requests can be captured and resent repeatedly
  • Accidental process activation: Fake events can trigger unwanted operations in your system

Benefits of Validation ​

  • Authenticity guarantee: Confirmation that the request genuinely came from SipPulse AI
  • Data integrity: Assurance that the payload was not altered during transmission
  • Timeliness: Verifying the timestamp prevents the reuse of old requests
  • Non-repudiation: Cryptographic proof of the message's origin

DANGER

Never store the webhook secret in public source code or in variables exposed to the front-end.

Event Logs ​

Each webhook has an Event Logs page where you can monitor delivery status:

  • Status: Whether the delivery succeeded (200) or failed
  • Attempts: Number of delivery attempts made
  • Response: Your server's response body
  • Timestamp: When the event was triggered

Use the pagination controls to navigate through historical logs. You can manually resend any failed delivery from this interface.

Local Testing with ngrok ​

To develop and test without deploying:

  1. Install ngrok:

    bash
    # For macOS:
    brew install ngrok

    For other operating systems, consult the official ngrok documentation.

  2. Start a tunnel pointing to your local port (e.g., 3000):

    bash
    ngrok http 3000
  3. Copy the generated HTTPS URL (e.g., https://abc123.ngrok.io) and use it as the URL when registering the webhook.

  4. Run your local application, receive the POST /webhook requests, and watch the console.

  5. Go to Webhooks → [your webhook] → Event Logs to see the status, attempts, and your server's response.