Webhooks

View as Markdown

Webhooks notify your systems in real-time during the call lifecycle. Atoms sends an HTTP POST to your URL for each event you subscribe to.

Three events fire per call:

EventWhen it firesContains
pre-conversationBefore the agent speaksCaller numbers, agent ID, call ID
post-conversationAfter the call endsFull transcript, recording URL, call metadata, agent variables
analytics-completedAfter post-call analytics are computedSummary, disposition + success metrics, call metadata

All three events share the same callId - use it as the join key when correlating events for the same call.


Managing Webhooks

All webhooks are created and managed from the dashboard.

Location: Left Sidebar → Settings → Webhook

Webhooks dashboard
Webhooks page on dashboard

The table shows all your webhooks with their URL, assigned agents, created date, and status.


Creating a Webhook

Click Create to add a new webhook endpoint.

Create webhook
Create webhook modal
FieldDescription
Endpoint URLYour server URL that receives the POST requests
DescriptionOptional human-readable label (e.g. "Debt Collection Agent's Endpoint") - echoed back in every event body

The right panel shows example Flask code for handling webhooks with HMAC signature verification.

Click Add endpoint to save.


Webhook Details

Click any webhook to see its details and subscriptions.

Webhook details
Webhook detail page
FieldWhat it shows
Webhook URLThe endpoint receiving events
DescriptionYour note
SubscriptionsWhich agents are using this webhook and what events
StatusEnabled or disabled
Signing SecretPer-webhook HMAC key. Use it to verify request authenticity. See Verifying signatures.

Adding to an Agent

Once a webhook exists, connect it to your agent.

Location: Agent Editor → Agent Settings → Webhook tab

Webhook in agent
Webhook tab in agent settings

Select your webhook from the list, then check which events (pre-conversation, post-conversation, analytics-completed) you want to receive. The agent will now POST to that endpoint for each subscribed event.


Verifying signatures

Every request Atoms sends to your endpoint carries an X-Signature header. Verify it before trusting the body.

Algorithm

  • HMAC-SHA256, hex-encoded.
  • Key: the webhook’s signing secret (shown on the webhook detail page, or returned as decryptedSecretKey from GET /webhook in the API Reference).
  • Message: the raw request body bytes, exactly as delivered. Do not re-serialize.
X-Signature = hex( HMAC_SHA256( signingSecret, raw_request_body ) )

Python (Flask)

1import hmac, hashlib
2from flask import Flask, request, abort
3
4SIGNING_SECRET = "..." # from the dashboard or GET /webhook
5
6app = Flask(__name__)
7
8@app.post("/webhook")
9def receive():
10 provided = request.headers.get("X-Signature", "")
11 expected = hmac.new(
12 SIGNING_SECRET.encode(),
13 request.get_data(), # raw bytes, not request.json
14 hashlib.sha256,
15 ).hexdigest()
16 if not hmac.compare_digest(provided, expected):
17 abort(401)
18 # trust request.json now
19 ...
20 return "", 200

Node (Express)

1import crypto from "node:crypto";
2import express from "express";
3
4const app = express();
5const SIGNING_SECRET = process.env.WEBHOOK_SECRET;
6
7// Capture raw body so the HMAC matches byte-for-byte.
8app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
9
10app.post("/webhook", (req, res) => {
11 const provided = req.header("X-Signature") ?? "";
12 const expected = crypto
13 .createHmac("sha256", SIGNING_SECRET)
14 .update(req.rawBody)
15 .digest("hex");
16
17 const a = Buffer.from(provided, "hex");
18 const b = Buffer.from(expected, "hex");
19 if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
20 return res.status(401).end();
21 }
22 // trust req.body now
23 res.status(200).end();
24});

Use a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node) to avoid timing side channels.


Managing webhooks via API

Everything you can do from the dashboard is also available under the Webhooks section of the API Reference. Notable behaviors:

  • GET /webhook and GET /webhook?webhookId=<id> both return the signing secret in the decryptedSecretKey field, so you can fetch it programmatically without hitting the dashboard.
  • POST /webhook returns only the new webhook’s ID. Call GET /webhook?webhookId=<id> right after to retrieve the signing secret.
  • PATCH /webhook/{id} can update endpoint, description, and headers only. Event subscriptions cannot be changed with PATCH; add or remove them via the POST /agent/{agentId}/webhook-subscriptions and DELETE /agent/{agentId}/webhook-subscriptions endpoints, or from the agent editor.

Deleting a webhook

DELETE /webhook/{id} fails with 400 if the webhook is still assigned to any agent:

Cannot delete webhook: It is currently assigned to one or more agents.
Please remove all agent assignments first.

Clean unassign path: call DELETE /agent/{agentId}/webhook-subscriptions for each agent that has the webhook attached, then retry the delete. Removing the webhook from the agent’s Webhook tab in the dashboard does the same thing.


Event envelope

Every webhook event shares the same top-level envelope:

FieldTypeDescription
urlstringThe webhook URL endpoint.
descriptionstringHuman-readable label configured on the webhook.
eventstring{agentId}.{eventType} - e.g. 69fc7dd072a0c1d28d948ace.post-conversation.
metadataobjectEvent-specific payload. See per-event sections below.
idstringUnique webhook delivery ID (separate from callId).

Common metadata fields (present in all events)

FieldTypeDescription
metadata.agentIdstringThe Atoms agent ID that handled the call.
metadata.eventTypestringOne of pre-conversation, post-conversation, analytics-completed.
metadata.conversationTypestringChannel type. Known values: telephonyOutbound, telephonyInbound.
metadata.callIdstringUnique call identifier (e.g. CALL-1778226705739-7e4c17).

1. pre-conversation

Fired before the agent begins speaking. Use this to enrich CRM data, log call attempts, or gate outbound calls.

metadata fields

FieldTypeDescription
agentIdstringAgent ID.
eventTypestring"pre-conversation"
conversationTypestringe.g. "telephonyOutbound"
toPhonestringDestination phone number in E.164 format.
fromPhonestringOriginating phone number in E.164 format.
callIdstringUnique call ID.

Example body

1{
2 "url": "https://example.com/webhook",
3 "description": "Debt Collection Agent's Endpoint",
4 "event": "69fc7dd072a0c1d28d948ace.pre-conversation",
5 "metadata": {
6 "agentId": "69fc7dd072a0c1d28d948ace",
7 "eventType": "pre-conversation",
8 "conversationType": "telephonyOutbound",
9 "toPhone": "+916296641821",
10 "fromPhone": "+918035317096",
11 "callId": "CALL-1778226705739-7e4c17"
12 },
13 "id": "69fd96118fd277fc807e4c23"
14}

pre-conversation does not contain callData, transcript, variables, analytics, or recordingUrl.


2. post-conversation

Fired after the call ends. Contains the full transcript, call metadata, recording URL, and all agent variables that were in scope during the conversation.

metadata fields

FieldTypeDescription
agentIdstringAgent ID.
eventTypestring"post-conversation"
conversationTypestringe.g. "telephonyOutbound"
callIdstringUnique call ID.
recordingUrlstringURL to the composite call recording (.wav).
callDataobjectCall-level metadata - see table below.
transcriptarrayOrdered list of transcript turns - see table below.
variablesobjectKey-value map of all agent variables injected into the call.

metadata.callData

FieldTypeDescription
fromNumberstringOriginating phone number (E.164).
toNumberstringDestination phone number (E.164).
callDurationnumberTotal call duration in seconds (float).
callStatusstringTerminal status. Known values: completed, no-answer, busy, failed, canceled.
callDirectionstring"telephony_outbound" or "telephony_inbound".
answerTimestringISO 8601 timestamp when the call was answered (UTC).
endTimestringISO 8601 timestamp when the call ended (UTC).

metadata.transcript[] (array of objects)

Each element represents one speaking turn.

FieldTypeDescription
rolestring"agent" or "user".
contentstringThe spoken text for this turn.
timestampstringISO 8601 timestamp of when this turn began (UTC).

metadata.variables

A flat key-value object. The platform always injects a fixed set of standard keys. Any additional keys come from the agent’s own configuration (Agent Settings → Variables) or from prompt-declared placeholders.

Standard variables the platform always sets

KeyDescription
call_idSame as metadata.callId.
user_numberCaller’s phone number (E.164). Same as callData.fromNumber for inbound.
agent_numberNumber the agent answered on (E.164). Same as callData.toNumber for inbound.
conversation_typetelephonyInbound or telephonyOutbound.
agent_genderFrom the agent’s voice configuration.
default_languageThe agent’s default language code (e.g. en, hi).
supported_languagesComma-separated list of language codes the agent is configured to accept.
current_date, current_time, current_dayRendered at call start in the agent’s timezone. Available as {{current_date}} etc. in prompts.
timezoneThe agent’s configured timezone (e.g. Asia/Kolkata).

Custom keys (e.g. agent_name, customer_name, due_amount) sit alongside these.

The variables object is otherwise fully dynamic. New custom keys can appear at any time depending on the agent’s prompt or the payload passed to the outbound call API. Your code should handle unknown keys gracefully. Store them in a generic JSON column rather than mapping each to a fixed column.

Example body

1{
2 "url": "https://example.com/webhook",
3 "description": "Debt Collection Agent's Endpoint",
4 "event": "69fc7dd072a0c1d28d948ace.post-conversation",
5 "metadata": {
6 "agentId": "69fc7dd072a0c1d28d948ace",
7 "eventType": "post-conversation",
8 "conversationType": "telephonyOutbound",
9 "callId": "CALL-1778226705739-7e4c17",
10 "recordingUrl": "https://db2izkvf1oocn.cloudfront.net/call-recordings/CALL-.../composite.wav",
11 "callData": {
12 "answerTime": "2026-05-08T07:51:58.813Z",
13 "callDirection": "telephony_outbound",
14 "callDuration": 49.350622,
15 "callStatus": "completed",
16 "endTime": "2026-05-08T07:52:48.164Z",
17 "fromNumber": "+918035317096",
18 "toNumber": "+916296641821"
19 },
20 "transcript": [
21 { "role": "agent", "content": "नमस्ते, मैं Nisha बोल रही हूँ…", "timestamp": "2026-05-08T07:52:05.123Z" },
22 { "role": "user", "content": "हाँ बोलिए", "timestamp": "2026-05-08T07:52:07.369Z" }
23 ],
24 "variables": {
25 "call_id": "CALL-1778226705739-7e4c17",
26 "user_number": "+916296641821",
27 "agent_number": "+918035317096",
28 "conversation_type": "telephonyOutbound",
29 "agent_gender": "female",
30 "default_language": "hi",
31 "supported_languages": "hi,en",
32 "current_date": "2026-05-08",
33 "current_day": "Friday",
34 "current_time": "13:21",
35 "timezone": "Asia/Kolkata",
36 "agent_name": "Nisha",
37 "customer_name": "Rahul Sharma",
38 "due_amount": "29000"
39 }
40 },
41 "id": "69fd96578d7c3809f58ce555"
42}

3. analytics-completed

Fired after Atoms finishes running the configured disposition and success metrics on the transcript. Arrives some time after post-conversation.

metadata fields

FieldTypeDescription
agentIdstringAgent ID.
eventTypestring"analytics-completed"
conversationTypestringe.g. "telephonyOutbound"
callIdstringUnique call ID.
analyticsobjectContains summary, dispositionMetrics, and successMetrics.
callDataobjectSame structure as post-conversation.callData - but the callDirection field may be absent.

metadata.analytics

FieldTypeDescription
summarystringLLM-generated plain-text summary of the call.
dispositionMetricsarrayArray of metric objects - see schema below.
successMetricsarrayArray of metric objects (same schema as disposition metrics). May be empty [].

analytics.dispositionMetrics[] and analytics.successMetrics[]

Each metric is a self-describing object. The set of metrics is configured per-agent and can vary.

FieldTypeDescription
identifierstringMachine-readable metric name (e.g. turn_taking_balance, escalation_needed).
valueinteger / string / booleanThe evaluated result. Type depends on dispositionMetricType.
confidencenumberConfidence score (0–1).
reasoningstringLLM-generated explanation for the assigned value.
dispositionMetricPromptstringThe prompt/question that was used to evaluate this metric.
dispositionMetricTypestringData type of value. One of: INTEGER, STRING, BOOLEAN.

Example body

1{
2 "url": "https://example.com/webhook",
3 "description": "Debt Collection Agent's Endpoint",
4 "event": "69fc7dd072a0c1d28d948ace.analytics-completed",
5 "metadata": {
6 "agentId": "69fc7dd072a0c1d28d948ace",
7 "eventType": "analytics-completed",
8 "conversationType": "telephonyOutbound",
9 "callId": "CALL-1778226705739-7e4c17",
10 "analytics": {
11 "summary": "The call involved the agent discussing an overdue EMI payment…",
12 "dispositionMetrics": [
13 {
14 "identifier": "turn_taking_balance",
15 "value": 2,
16 "confidence": 1,
17 "reasoning": "The user had 4 speaking turns while the agent had 5…",
18 "dispositionMetricPrompt": "Measure the balance of speaking turns…",
19 "dispositionMetricType": "INTEGER"
20 },
21 {
22 "identifier": "escalation_needed",
23 "value": false,
24 "confidence": 1,
25 "reasoning": "The call did not present complex issues…",
26 "dispositionMetricPrompt": "Based on the interaction, did this call require escalation?",
27 "dispositionMetricType": "BOOLEAN"
28 }
29 ],
30 "successMetrics": []
31 },
32 "callData": {
33 "fromNumber": "+918035317096",
34 "toNumber": "+916296641821",
35 "callDuration": 49.350622,
36 "callStatus": "completed",
37 "answerTime": "2026-05-08T07:51:58.813Z",
38 "endTime": "2026-05-08T07:52:48.164Z"
39 }
40 },
41 "id": "69fd96632b717269846aa433"
42}

Event lifecycle and ordering

Call initiated
┌─────────────────┐
│ pre-conversation│ ← Fired before agent speaks
└────────┬────────┘
Call in progress…
┌──────────────────┐
│ post-conversation│ ← Fired after call ends (has transcript + recording)
└────────┬─────────┘
Analytics processing…
┌─────────────────────┐
│ analytics-completed │ ← Fired after metrics are computed
└─────────────────────┘

All three events share the same callId - use it as the join key.


Integration notes

  1. variables is dynamic - never hard-code column mappings. Store as JSON or iterate keys.
  2. dispositionMetrics / successMetrics are agent-configured - the set of identifiers and their types will differ across agents.
  3. pre-conversation uses toPhone / fromPhone while post-conversation and analytics-completed use toNumber / fromNumber - normalize these in your ingestion layer.
  4. Timestamps are always UTC ISO 8601 strings.
  5. callDuration is a float representing seconds (not milliseconds).
  6. recordingUrl only appears in post-conversation.
  7. transcript only appears in post-conversation.
  8. analytics (summary + metrics) only appears in analytics-completed.

FAQ

30 seconds per request. If your endpoint hasn’t returned a response in that window, Atoms marks the delivery as failed.

No. A non-2xx response or a network-level failure (timeout, DNS, TLS, connection refused) is logged as FAILED and the event moves on. Handle retries on your side: acknowledge fast with a 2xx, then process asynchronously; if your endpoint is down, drain from your own persistent queue once you recover.

No fixed egress IP is published. Use the X-Signature header for authenticity; don’t rely on IP allow-lists.

Yes. Assign the same webhook to several agents in the agent editor. The metadata.agentId field on every event tells you which agent produced it.

Within a single call, the three events fire in this sequence: pre-conversationpost-conversationanalytics-completed. Deliveries themselves are best-effort; treat metadata.callId as the join key and reconcile on your side rather than assuming exact arrival order across concurrent calls.

Run a receiver on localhost and expose it with a tunnel (ngrok, cloudflared, etc.). Register the tunnel URL as the webhook endpoint from the dashboard, then place a test call. Live payloads land on your local process.

Call GET /webhook?webhookId=<id> (or GET /webhook to get all). The response includes a decryptedSecretKey field with the plaintext HMAC key.

Not through PATCH /webhook/{id} (it only updates endpoint, description, headers). Add or remove event subscriptions via the POST /agent/{agentId}/webhook-subscriptions and DELETE /agent/{agentId}/webhook-subscriptions endpoints, or from the agent editor.

The webhook is still assigned to at least one agent. Remove the assignment (either from the agent’s Webhook tab or via DELETE /agent/{agentId}/webhook-subscriptions), then retry DELETE /webhook/{id}.

The 11 platform-set standard keys (call_id, user_number, agent_number, conversation_type, agent_gender, default_language, supported_languages, current_date, current_time, current_day, timezone) are stable. Any additional keys come from the agent’s own config and can change whenever the prompt or outbound-call payload changes, so store variables as JSON on your side.