Documentation
Webhooks
Receive real-time events as your call progresses. Set a webhookUrl when placing a call and By Your Side posts a signed JSON payload to your endpoint at key moments.
Set up a webhook
Pass a webhookUrl in the body of POST /v1/agent/calls. The URL must be publicly reachable over HTTPS. By Your Side will POST events to it during and after the call.
curl -X POST https://api.byourside.ai/v1/agent/calls \
-H "Authorization: Bearer bys_ak_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550123",
"objective": "Confirm the appointment for tomorrow at 2 PM.",
"webhookUrl": "https://your-server.example.com/webhooks/bys"
}'Events
Two events are dispatched per call:
| Event | When it fires |
|---|---|
call.in_progress | The moment the call is answered and the AI begins speaking. |
call.completed | Call finished normally. Summary and extracted fields are populated. |
call.no_answer | Destination did not pick up. |
call.voicemail | Reached voicemail. |
call.declined | Call rejected by recipient. |
call.failed | Technical failure. |
Every call produces exactly two events: one call.in_progress (if it was answered) and one terminal event. If the call is never answered (no_answer, declined, failed) only the terminal event fires.
Payload shape
Each request body is a JSON object. Fields that are not yet available at the time of the event (for example endedAt during call.in_progress) are null.
| Field | Type | Description |
|---|---|---|
event | string | Event name, e.g. call.completed. |
eventId | string | Unique per delivery. Retries of the same delivery reuse the same eventId; dedup on this field. |
timestamp | string | ISO 8601 time when the event was built. |
callId | string | The call ID from place_call. |
status | string | Current call status. |
to | string | Destination number (E.164). |
objective | string | The objective you supplied. |
startedAt | string | null | When the call was answered. Set for in_progress and terminal events where the call connected. |
endedAt | string | null | When the call ended. Set on terminal events only. |
durationSec | number | null | Call duration in seconds. Set on terminal events only. |
summary | string | null | Plain-English summary. Set on call.completed. |
extracted | object | null | Extracted field values. Set on call.completed. |
error | string | null | Error token. Set on call.failed and similar outcomes. |
call.in_progress payload example
{
"event": "call.in_progress",
"eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479:call.in_progress:1750163000000",
"timestamp": "2026-06-17T14:03:15.000Z",
"callId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "in_progress",
"to": "+14155550123",
"objective": "Confirm the appointment for tomorrow at 2 PM.",
"startedAt": "2026-06-17T14:03:12.000Z",
"endedAt": null,
"durationSec": null,
"summary": null,
"extracted": null
}call.completed payload example
{
"event": "call.completed",
"eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479:call.completed:1750163148000",
"timestamp": "2026-06-17T14:05:48.000Z",
"callId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "completed",
"to": "+14155550123",
"objective": "Confirm the appointment for tomorrow at 2 PM.",
"startedAt": "2026-06-17T14:03:12.000Z",
"endedAt": "2026-06-17T14:05:48.000Z",
"durationSec": 156,
"summary": "The contact confirmed the appointment at 2 PM.",
"extracted": { "confirmed": true }
}Signature verification
Every webhook request includes an X-BYS-Signature header. Verify it before processing the payload to confirm the request came from By Your Side and was not tampered with.
Header format: t=<unix_seconds>,v1=<hex>
The hex value is HMAC-SHA256(webhookSecret, "{t}.{rawBody}") where t is the Unix timestamp in seconds and rawBody is the raw (unparsed) request body as a string. Reject the request if the signature does not match or if the timestamp is older than your tolerance (5 minutes is a common choice).
Important: read the body as raw bytes before parsing. Parsing JSON first and re-serializing it will produce a different string and break verification.
Using the SDK helpers (recommended)
import { verifyWebhook } from './src/index.js';
// req is an Express request with raw body
const valid = verifyWebhook(
req.headers['x-bys-signature'],
req.body.toString('utf8'),
process.env.BYS_WEBHOOK_SECRET
);
if (!valid) return res.status(400).send('Bad signature');from byourside import verify_webhook
valid = verify_webhook(
sig_header=request.headers.get('X-BYS-Signature', ''),
raw_body=request.get_data(as_text=True),
secret=WEBHOOK_SECRET,
)
if not valid:
abort(400, 'Invalid signature')Verifying manually (Node)
import crypto from 'node:crypto';
function verifyWebhook(sigHeader, rawBody, secret) {
// sigHeader format: "t=<unix_seconds>,v1=<hex>"
const parts = Object.fromEntries(sigHeader.split(',').map((p) => p.split('=')));
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
// Reject if older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(t, 10);
return valid && age < 300;
}Verifying manually (Python)
import hmac, hashlib, time
def verify_webhook(sig_header: str, raw_body: str, secret: str) -> bool:
# sig_header format: "t=<unix_seconds>,v1=<hex>"
parts = dict(p.split('=', 1) for p in sig_header.split(','))
t = parts.get('t', '')
v1 = parts.get('v1', '')
if not t or not v1:
return False
mac = hmac.new(secret.encode(), f'{t}.{raw_body}'.encode(), hashlib.sha256)
expected = mac.hexdigest()
valid = hmac.compare_digest(v1, expected)
age = int(time.time()) - int(t)
return valid and age < 300Delivery semantics
- At-least-once. An event may be delivered more than once (for example, if your server does not return a 2xx within the timeout). Deduplicate on
eventId: retries of the same delivery reuse the sameeventId. - Ordering.
call.in_progressis emitted before the terminal event in wall-clock time. Under retries, arrival order is not guaranteed. Use thetimestampfield to order events, not arrival order. - Your endpoint must return 2xx. Any non-2xx response or a timeout is treated as a failed delivery and retried. Return 200 immediately; do your processing asynchronously if needed.
- No transcript in the payload. The webhook payload is intentionally lean. Call
GET /v1/agent/calls/{callId}to fetch the full transcript and recording URL.
For the full endpoint and parameter reference, see the API reference. For SDK usage, see SDKs.