Skip to content
Wozena
ProductSolutionsPricingDocs
Log inGet started
Docs home
Get started
  • How Wozena works
  • Quickstart checklist
  • Create your agency
Products & services
  • Sell an agent as a service
Voice agents
  • Connect Retell
  • Phone numbers
  • Build an agent
  • Knowledge bases
  • Test and publish
  • Share an agent
White-label
  • Branding
  • Custom domains
  • Email and sending domain
  • The client portal
Clients & team
  • Clients
  • Team, roles and invites
Sell & bill
  • Connect Stripe
  • Client plans
  • Plan links and storefront
  • Billing clients
  • Your Wozena plan
Integrations & automation
  • CRMs and calendars
  • In-call tools
  • Post-call automations
  • Webhooks
Outbound & operations
  • Contacts and campaigns
  • Calls and reports
  • Security and access
  • Troubleshooting
Docs / Integrations & automation

Webhooks

Receive signed call events in Zapier, Make, n8n or your own endpoint.

Webhooks send a signed event to a URL you choose every time a call completes. Use them to feed call data into Zapier, Make, n8n or your own systems — for example to update a spreadsheet, alert a client in Slack, or push leads into a CRM we don’t integrate with directly.

Plan: Requires a plan that includes integrations — by default Agency and Agency Pro.

Before you start
  • An endpoint that accepts HTTPS POST requests on a public address.
  • An agency owner, agency admin or client admin account.

How webhooks work#

Webhooks are outbound only: we send (POST) events to you. There is no inbound API to call Wozena from your systems.

A webhook needs two pieces:

  • A Webhook integration that holds your URL and signing secret.
  • An automation that decides which calls send an event. See Post-call automations.

The only event is call.completed. It fires once the call has ended and its post-call analysis is ready.

Set up a webhook#

  1. Switch into the client
    Webhooks, like other integrations, belong to a workspace. Connect it for the client (choose the client under “Workspace” when connecting) to send that client’s calls. A webhook connected in agency-wide context is separate — see the warning in Which calls an automation runs on.
  2. Add the Webhook integration
    Go to Integrations and click “Connect” on the Zapier, Make, n8n or Custom webhook tile under “Automation & webhooks”. Enter the “Webhook URL” (a public HTTPS address).
  3. Add a signing secret
    Enter a long random string in “Signing secret” and store the same value on your server. It’s optional, but without it we don’t sign requests and you can’t prove they came from us.
  4. Save
    Click “Connect & validate”. We check the URL’s format only; nothing is sent yet.
  5. Create the automation
    Go to Automations, click “New automation”, choose the Webhook destination and the “POST every call to a webhook” template. Add conditions if you only want some calls, then click “Create”.
  6. Make a test call
    Call the agent, hang up, and wait for the post-call analysis. The event should arrive shortly after. Check “Run history” on the Automations page if it doesn’t.
Note
Each workspace holds one Webhook connection. To change the URL or add a signing secret later, click “Rotate token” on the Webhook connection card: it opens the same fields, with the saved settings filled in. To send the same event to several places, point it at a tool like Zapier, Make or n8n and fan out from there.

Request and headers#

Each event is an HTTP POST with content-type: application/json. We wait up to 8 seconds for a response. Respond with a 2xx status; any status of 400 or above counts as a failure. We ignore the response body.

HeaderValue
x-wozena-eventcall.completed
x-wozena-deliveryA stable id for this call and automation, the same value as event_id in the body. Retries of the same event reuse it, so use it to drop duplicates.
x-wozena-signaturev=<unix time in ms>,d=<hex digest>. Sent only when a signing secret is saved.
User-Agentwozena/1.0 (+https://iwozena.com)
Note
The User-Agent header names the platform, not your brand. If a client can see your raw request logs, they’ll see it there.

Payload#

The body is compact JSON (no spaces or line breaks) with keys sorted alphabetically. The example below is formatted for reading, and every value in it is illustrative.

FieldWhat it contains
eventAlways call.completed.
event_idUnique per call and automation. Same as the x-wozena-delivery header.
call_idThe call’s id at the voice provider (Retell).
agent_idThe agent that handled the call.
client_idThe client the call belongs to.
directioninbound or outbound.
fromThe calling number, in E.164 format.
toThe number called, in E.164 format.
outcomesuccessful, unsuccessful, or empty, from the post-call success check.
sentimentThe caller’s sentiment from post-call analysis.
duration_msCall length in milliseconds.
transcriptThe full transcript as text.
recording_urlA link to the call recording, when one exists.
summaryThe post-call summary.
analysisEvery post-call analysis field, including your custom ones (for example appointment_booked).
timestampWhen the call started, in Unix milliseconds, sent as a string.

Some fields can be null, for example when a call has no recording or no summary. Handle missing values.

Example call.completed payload
{
  "agent_id": "3f1c2a9e-5b7d-4c21-9e0a-7d2b6f4e8a10",
  "analysis": {
    "appointment_booked": true,
    "call_successful": true,
    "call_summary": "Caller asked for a cleaning next week and booked Tuesday at 10:00.",
    "email": "[email protected]",
    "name": "Jamie Rivera",
    "user_sentiment": "Positive"
  },
  "call_id": "call_0a1b2c3d4e5f60718293a4b5",
  "client_id": "8c4e7b12-0f3a-4d6e-b5a9-2e1f9c7d6b34",
  "direction": "inbound",
  "duration_ms": 142000,
  "event": "call.completed",
  "event_id": "call_0a1b2c3d4e5f60718293a4b5:6d2f8e41-7a9c-4b3e-8f15-0c9a2b7e4d58",
  "from": "+14155550123",
  "outcome": "successful",
  "recording_url": "https://recordings.example.com/call_0a1b2c3d4e5f60718293a4b5.wav",
  "sentiment": "Positive",
  "summary": "Caller asked for a cleaning next week and booked Tuesday at 10:00.",
  "timestamp": "1790517600000",
  "to": "+14155550199",
  "transcript": "Agent: Thanks for calling Acme Dental, how can I help?\nUser: I'd like to book a cleaning..."
}

Verify the signature#

When you save a signing secret, every request carries an x-wozena-signature header like v=1790517600123,d=9f2c…:

  • v is the time we signed the request, in Unix milliseconds.
  • d is a hex-encoded HMAC-SHA256 digest. The key is your signing secret. The message is the raw request body bytes followed immediately by the v value as a decimal string.

To verify, recompute the digest from the raw body and compare it in constant time. Reject the request if the timestamp is more than 5 minutes from your server’s clock.

Node.js

verify-wozena.js
import crypto from "node:crypto";

// rawBody: a Buffer with the exact request body.
// Do not JSON.parse and re-stringify it before verifying.
export function verifyWozena(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) {
  if (!header) return false;

  let ts;
  let digest;
  for (const part of header.split(",")) {
    const i = part.indexOf("=");
    const key = part.slice(0, i).trim();
    const value = part.slice(i + 1);
    if (key === "v") ts = Number.parseInt(value, 10);
    else if (key === "d") digest = value;
  }
  if (!Number.isFinite(ts) || !digest) return false;

  // Reject old or replayed requests.
  if (Math.abs(Date.now() - ts) > toleranceMs) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(Buffer.concat([rawBody, Buffer.from(String(ts))]))
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(digest, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Example with Express: keep the raw body for this route.
// app.post("/wozena", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyWozena(req.body, req.get("x-wozena-signature"), process.env.WOZENA_SIGNING_SECRET)) {
//     return res.sendStatus(401);
//   }
//   const event = JSON.parse(req.body.toString("utf8"));
//   res.sendStatus(200); // respond fast, then process the event
// });

Python

verify_wozena.py
import hashlib
import hmac
import time


def verify(raw_body: bytes, header: str | None, secret: str, tolerance_ms: int = 300_000) -> bool:
    """raw_body must be the exact request bytes, not re-serialized JSON."""
    if not header:
        return False

    ts = digest = None
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "v":
            ts = int(value)
        elif key == "d":
            digest = value
    if ts is None or not digest:
        return False

    # Reject old or replayed requests.
    if abs(int(time.time() * 1000) - ts) > tolerance_ms:
        return False

    expected = hmac.new(secret.encode(), raw_body + str(ts).encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, digest)


# Example with Flask:
# @app.post("/wozena")
# def wozena():
#     if not verify(request.get_data(), request.headers.get("x-wozena-signature"), SIGNING_SECRET):
#         abort(401)
#     event = request.get_json()
#     return "", 200
Verify the raw body
Compute the signature over the exact bytes you received. If your framework parses the JSON first and you re-serialize it, the bytes change and the signature won’t match.

Best practices#

  • Always set a signing secret and reject requests that fail verification.
  • Reject old timestamps. The examples reject anything more than 5 minutes old, which blocks replayed requests. Keep your server clock in sync.
  • Deduplicate on x-wozena-delivery. A retry can deliver an event you already processed.
  • Respond with 2xx quickly. We wait up to 8 seconds. Acknowledge first, then do slow work (CRM writes, emails) in the background.
  • Expect retries. Any status of 400 or above is a failure. Rate limits and server errors are retried briefly. A failed delivery is then retried after 1, 5 and 15 minutes — 4 attempts in total. Each retry is signed again with a fresh timestamp.
  • Replay after an outage. Fix your endpoint, then replay failed runs from “Run history”. See Run history, retries and replay.

Zapier, Make and n8n#

You don’t need a server to use webhooks. In any automation tool that accepts incoming webhooks:

  1. Create a webhook trigger
    Add a Catch Hook or Webhook trigger in your tool and copy the URL it gives you.
  2. Paste the URL into Wozena
    Use it as the “Webhook URL” when you connect the Webhook integration.
  3. Send a sample
    Make a test call so the tool receives a real event and can show you the fields.
  4. Build the rest of the flow
    Map fields such as from, summary and the values inside analysis to the next steps.

Most no-code tools can’t check the signature. If your tool can’t, treat the trigger URL as a secret and don’t share it.

Troubleshooting#

What you seeWhat to do
“A public HTTPS webhook URL is required”Use an address that starts with https:// and is reachable from the internet.
“Webhook destination must be public”Localhost and private network addresses aren’t allowed. Expose the endpoint publicly or use a tunnel with a public HTTPS address.
“Webhook destination must be a public hostname”Use a full domain name such as hooks.example.com, not a single-word host name.
“Invalid webhook port”Remove the unusual port, or use a standard HTTPS port.
URL rejected for another reasonRemove any username or password from the URL (user:pass@) and any #fragment at the end.
No x-wozena-signature headerNo signing secret is saved on the Webhook connection. Add one with “Rotate token” on the connection card.
Signature never matchesYou’re hashing parsed or re-serialized JSON, the secret differs, or your clock is more than 5 minutes off.
No events arriveCheck the automation is Active, its conditions match (try a dry run), and it lives in the same workspace as the calls. Then check “Run history” with “Failed only” on.
The same event arrives twiceA retry followed a slow or failed response. Deduplicate on x-wozena-delivery and respond faster.
Stuck? Ask Ada or email support, or check Troubleshooting.
PreviousPost-call automationsNextContacts and campaigns
On this page
  • How webhooks work
  • Set up a webhook
  • Request and headers
  • Payload
  • Verify the signature
  • Best practices
  • Zapier, Make and n8n
  • Troubleshooting
Wozena

A white-label platform for agencies selling AI phone agents to client businesses.

A Retell AI product
ProductClient portalsAgentsCampaignsClient billingReports
ResourcesDocumentationPricingChangelogStatusSupport
CompanySolutionsPartnersTrust centerPrivacyTerms
© 2026 Wozena · Powered by Retell AI
LinkedInXYouTube