Answering Machine Detection (AMD) lets you automatically distinguish between a live human and a voicemail machine, IVR, or beep when making outbound calls. The Voice API v2 runs AMD analysis on the call audio after the call is answered, then executes a different branch of SVAML commands depending on the result: onHuman, onMachine, onBeep, or onUnknown. You can wire a completely different call flow for each outcome, for example connect a human to a live agent, leave a voicemail when a beep is detected, or hang up silently on a machine.
AMD is controlled by the amd SVAML command, which you place inline in the call payload or return from a webhook. The command is non-blocking: the rest of your sequence keeps executing while detection runs in parallel, and the matching AMD branch fires as soon as a verdict is reached.
There are two ways to observe a result:
- The AMD branch runs on the call itself. Whatever commands you put under
onHuman/onBeep/ etc. execute on the live call, so the human hears your greeting and the voicemail gets your message. This is audible on the answering phone with no backend required. - AMD result webhook events. If your call runs on a service with a webhook URL, Sinch additionally POSTs a
call.amd.human/call.amd.machine/call.amd.beep/call.amd.unknownevent to that URL. This is how you get a visible log in your own server of which branch fired (see Track Call Status for consuming call events).
The fastest path to a first success is the inline callout below: place the call, answer it, and hear the AMD branch run. Add the callback server later when you want server-side visibility or dynamic per-call AMD configuration.
- Outreach campaigns: leave a tailored voicemail only when a beep is detected, and connect a live answer to an agent.
- Appointment reminders: connect to a live agent when a human answers, leave a reminder message on voicemail systems.
- Debt collection: route live answers to a specialist, leave a callback number on answering machines.
- Survey calls: launch the survey IVR only when a real human is detected, abort silently on machine answers.
All scripts read their configuration from environment variables. Export them in the shell you will run from:
export PROJECT_ID=... # from https://dashboard.sinch.com
export KEY_ID=...
export KEY_SECRET=...
export SINCH_NUMBER=+1... # your Sinch virtual number (E.164)
export DESTINATION_NUMBER=+1... # the number to call (E.164)
# Only needed for the callback-server path:
export CALLBACK_URL=... # your public ngrok URL
export PORT=3000Notes:
- Auth is HTTP Basic using
KEY_ID:KEY_SECRET. - These variables live only in the current shell. Open a new terminal and you will need to export them again (or add them to your shell profile).
- For the callback-server path you also need a public URL. Install ngrok and a runtime (Node 18+, or Python 3.8+ with
flask).
This places an outbound call and runs AMD on it. The matching branch executes on the call, so you observe the result by answering the phone.
Pick a language below, paste the script, export your variables, and run it.
What happens:
- The script POSTs the full SVAML (including all four AMD branches) to
POST /v2/projects/{projectId}/callsand printsAMD call created successfullywith the returnedsessionIdon HTTP 201. - Your
DESTINATION_NUMBERrings. Depending on how it is answered:- A person answers, AMD resolves to human, the
onHumanbranch plays "Hello! This is a call from Acme Corp..." then hangs up. - Voicemail picks up, AMD waits for the beep, and on the tone the
onBeepbranch plays "Hi, this is Acme Corp calling about your recent inquiry..." then hangs up. - Machine greeting, no beep yet,
onMachinehangs up silently. - Inconclusive,
onUnknownhangs up.
- A person answers, AMD resolves to human, the
Visibility note: the script's terminal output only confirms the call was created (HTTP 201). It does not print which AMD branch fired, because that verdict lives on the call. To see the branch in your terminal, run the callback-server path further down and watch for
call.amd.*events.
#!/bin/bash
# Sinch AMD Callout: outbound call with Answering Machine Detection (AMD).
# AMD detects human vs. machine and fires different SVAML for each outcome.
# Reads credentials from exported environment variables (see Setup).
set -e
: "${PROJECT_ID:?ERROR: PROJECT_ID is not set. Export it first.}"
: "${KEY_ID:?ERROR: KEY_ID is not set.}"
: "${KEY_SECRET:?ERROR: KEY_SECRET is not set.}"
: "${SINCH_NUMBER:?ERROR: SINCH_NUMBER is not set.}"
: "${DESTINATION_NUMBER:?ERROR: DESTINATION_NUMBER is not set.}"
BASE_URL="https://voice.api.sinch.com/v2"
echo "Placing AMD callout from ${SINCH_NUMBER} to ${DESTINATION_NUMBER} ..."
BODY=$(printf '{
"commands": [
{
"command": "dial",
"callName": "amd-call",
"from": { "type": "PHONE", "phone": { "number": "%s" } },
"to": { "type": "PHONE", "phone": { "number": "%s" } },
"dialTimeoutDurationSeconds": 45,
"maxCallDurationSeconds": 300,
"events": {
"onAnswer": [
{
"command": "amd",
"events": {
"onHuman": [
{
"command": "messages",
"messagesName": "human-greeting",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
"voiceName": "Emma"
}
}
],
"events": { "onFinish": [{ "command": "hangup" }] }
}
],
"onMachine": [
{ "command": "hangup" }
],
"onBeep": [
{
"command": "messages",
"messagesName": "voicemail-message",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
"voiceName": "Emma"
}
}
],
"events": { "onFinish": [{ "command": "hangup" }] }
}
],
"onUnknown": [
{ "command": "hangup" }
]
}
}
]
}
}
]
}' "${SINCH_NUMBER}" "${DESTINATION_NUMBER}")
RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST \
-u "${KEY_ID}:${KEY_SECRET}" \
"${BASE_URL}/projects/${PROJECT_ID}/calls" \
-H "Content-Type: application/json" \
-d "${BODY}")
HTTP_BODY=$(echo "${RESPONSE}" | head -n -1)
HTTP_CODE=$(echo "${RESPONSE}" | tail -n 1)
if [ "${HTTP_CODE}" -eq 201 ]; then
echo "AMD call created successfully (HTTP ${HTTP_CODE}):"
echo "${HTTP_BODY}" | (command -v jq > /dev/null && jq '.' || cat)
else
echo "ERROR: API returned HTTP ${HTTP_CODE}:" >&2
echo "${HTTP_BODY}" >&2
exit 1
fiRequires pip install requests.
# Sinch AMD Callout: outbound call with Answering Machine Detection (AMD).
# AMD detects human vs. machine and executes different SVAML for each outcome.
# Reads credentials from exported environment variables (see Setup).
# Requirements: pip install requests
import os
import sys
import json
import requests
project_id = os.environ.get("PROJECT_ID")
key_id = os.environ.get("KEY_ID")
key_secret = os.environ.get("KEY_SECRET")
sinch_number = os.environ.get("SINCH_NUMBER")
destination_number = os.environ.get("DESTINATION_NUMBER")
for var, name in [
(project_id, "PROJECT_ID"), (key_id, "KEY_ID"), (key_secret, "KEY_SECRET"),
(sinch_number, "SINCH_NUMBER"), (destination_number, "DESTINATION_NUMBER"),
]:
if not var:
print(f"ERROR: {name} is not set. Export it first.", file=sys.stderr)
sys.exit(1)
url = f"https://voice.api.sinch.com/v2/projects/{project_id}/calls"
# The `amd` command must be placed in the `onAnswer` event of a `dial` command.
# It fires different SVAML commands based on what AMD detects.
payload = {
"commands": [
{
"command": "dial",
"callName": "amd-call",
"from": {"type": "PHONE", "phone": {"number": sinch_number}},
"to": {"type": "PHONE", "phone": {"number": destination_number}},
"dialTimeoutDurationSeconds": 45,
"maxCallDurationSeconds": 300,
"events": {
"onAnswer": [
{
"command": "amd",
"events": {
# Human picked up: play a personalized greeting
"onHuman": [
{
"command": "messages",
"messagesName": "human-greeting",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
"voiceName": "Emma"
}
}
],
"events": {
"onFinish": [{"command": "hangup"}]
}
}
],
# Machine greeting detected (beep not yet heard): just hang up
"onMachine": [
{"command": "hangup"}
],
# Beep detected: leave a voicemail message right now
"onBeep": [
{
"command": "messages",
"messagesName": "voicemail-message",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
"voiceName": "Emma"
}
}
],
"events": {
"onFinish": [{"command": "hangup"}]
}
}
],
# Unknown: hang up safely
"onUnknown": [
{"command": "hangup"}
]
}
}
]
}
}
]
}
print(f"Placing AMD callout from {sinch_number} to {destination_number} ...")
try:
response = requests.post(url, json=payload, auth=(key_id, key_secret))
data = response.json()
if response.status_code == 201:
print("AMD call created successfully:")
print(json.dumps(data, indent=2))
else:
print(f"ERROR {response.status_code}:", file=sys.stderr)
print(json.dumps(data, indent=2), file=sys.stderr)
sys.exit(1)
except requests.RequestException as e:
print(f"Request failed: {e}", file=sys.stderr)
sys.exit(1)Save as amd-callout.mjs (or use "type": "module" in package.json) so top-level await works.
// Sinch AMD Callout: Node.js (native fetch, Node 18+) outbound call with AMD.
// Reads credentials from exported environment variables (see Setup).
// Run: node amd-callout.mjs
const projectId = process.env.PROJECT_ID || (() => { throw new Error("PROJECT_ID not set"); })();
const keyId = process.env.KEY_ID || (() => { throw new Error("KEY_ID not set"); })();
const keySecret = process.env.KEY_SECRET || (() => { throw new Error("KEY_SECRET not set"); })();
const sinchNumber = process.env.SINCH_NUMBER || (() => { throw new Error("SINCH_NUMBER not set"); })();
const destinationNumber = process.env.DESTINATION_NUMBER || (() => { throw new Error("DESTINATION_NUMBER not set"); })();
const authHeader = "Basic " + Buffer.from(`${keyId}:${keySecret}`).toString("base64");
const payload = {
commands: [
{
command: "dial",
callName: "amd-call",
from: { type: "PHONE", phone: { number: sinchNumber } },
to: { type: "PHONE", phone: { number: destinationNumber } },
dialTimeoutDurationSeconds: 45,
maxCallDurationSeconds: 300,
events: {
onAnswer: [
{
command: "amd",
events: {
// Human detected: greet and connect to an agent (or take further action)
onHuman: [
{
command: "messages",
messagesName: "human-greeting",
messages: [
{
type: "SAY",
say: {
text: "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
// Machine detected (no beep yet): hang up silently
onMachine: [{ command: "hangup" }],
// Beep detected: play the voicemail message right after the beep
onBeep: [
{
command: "messages",
messagesName: "voicemail-message",
messages: [
{
type: "SAY",
say: {
text: "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
// Unknown: safe default is to hang up
onUnknown: [{ command: "hangup" }]
}
}
]
}
}
]
};
console.log(`Placing AMD callout from ${sinchNumber} to ${destinationNumber} ...`);
const response = await fetch(
`https://voice.api.sinch.com/v2/projects/${projectId}/calls`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: authHeader
},
body: JSON.stringify(payload)
}
);
const data = await response.json();
if (response.status === 201) {
console.log("AMD call created successfully:", JSON.stringify(data, null, 2));
} else {
console.error(`ERROR ${response.status}:`, JSON.stringify(data, null, 2));
process.exit(1);
}Requires PHP 8+ with the curl extension. getenv() reads the exported variables directly.
<?php
// Sinch AMD Callout: PHP outbound call with Answering Machine Detection.
// Reads credentials from exported environment variables (see Setup).
// Requirements: PHP 8+ with the curl extension.
$projectId = getenv('PROJECT_ID') ?: die("ERROR: PROJECT_ID not set.\n");
$keyId = getenv('KEY_ID') ?: die("ERROR: KEY_ID not set.\n");
$keySecret = getenv('KEY_SECRET') ?: die("ERROR: KEY_SECRET not set.\n");
$sinchNumber = getenv('SINCH_NUMBER') ?: die("ERROR: SINCH_NUMBER not set.\n");
$destinationNumber = getenv('DESTINATION_NUMBER') ?: die("ERROR: DESTINATION_NUMBER not set.\n");
$url = "https://voice.api.sinch.com/v2/projects/{$projectId}/calls";
// The `amd` command must be inside `onAnswer`. AMD fires different SVAML
// depending on whether a human, machine, or beep is detected.
$payload = [
'commands' => [
[
'command' => 'dial',
'callName' => 'amd-call',
'from' => ['type' => 'PHONE', 'phone' => ['number' => $sinchNumber]],
'to' => ['type' => 'PHONE', 'phone' => ['number' => $destinationNumber]],
'dialTimeoutDurationSeconds' => 45,
'maxCallDurationSeconds' => 300,
'events' => [
'onAnswer' => [
[
'command' => 'amd',
'events' => [
// Human detected: play a personalized greeting
'onHuman' => [
[
'command' => 'messages',
'messagesName' => 'human-greeting',
'messages' => [
[
'type' => 'SAY',
'say' => [
'text' => 'Hello! This is a call from Acme Corp. An agent will be with you shortly.',
'voiceName' => 'Emma',
],
],
],
'events' => [
'onFinish' => [['command' => 'hangup']],
],
],
],
// Machine greeting (no beep yet): hang up
'onMachine' => [
['command' => 'hangup'],
],
// Beep detected: leave voicemail immediately after the beep
'onBeep' => [
[
'command' => 'messages',
'messagesName' => 'voicemail-message',
'messages' => [
[
'type' => 'SAY',
'say' => [
'text' => 'Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.',
'voiceName' => 'Emma',
],
],
],
'events' => [
'onFinish' => [['command' => 'hangup']],
],
],
],
// Unknown result: hang up safely
'onUnknown' => [
['command' => 'hangup'],
],
],
],
],
],
],
],
];
echo "Placing AMD callout from {$sinchNumber} to {$destinationNumber} ...\n";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_USERPWD => "{$keyId}:{$keySecret}",
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$responseBody = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
fwrite(STDERR, "curl error: {$curlError}\n");
exit(1);
}
$data = json_decode($responseBody, true);
if ($httpCode === 201) {
echo "AMD call created successfully:\n";
echo json_encode($data, JSON_PRETTY_PRINT) . "\n";
} else {
fwrite(STDERR, "ERROR {$httpCode}:\n");
fwrite(STDERR, json_encode($data, JSON_PRETTY_PRINT) . "\n");
exit(1);
}System.getenv reads the exported variables directly.
// Sinch AMD Callout: Java outbound call with Answering Machine Detection.
// Requires Java 11+ (java.net.http.HttpClient).
// Reads credentials from exported environment variables (see Setup).
//
// Compile: javac -d out AmdCallout.java
// Run: java -cp out com.sinch.tutorials.amd.AmdCallout
package com.sinch.tutorials.amd;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
public class AmdCallout {
public static void main(String[] args) throws Exception {
String projectId = requireEnv("PROJECT_ID");
String keyId = requireEnv("KEY_ID");
String keySecret = requireEnv("KEY_SECRET");
String sinchNumber = requireEnv("SINCH_NUMBER");
String destinationNumber = requireEnv("DESTINATION_NUMBER");
String url = "https://voice.api.sinch.com/v2/projects/" + projectId + "/calls";
String credentials = Base64.getEncoder()
.encodeToString((keyId + ":" + keySecret).getBytes());
// The `amd` command must be inside `onAnswer`.
// AMD fires different SVAML depending on detection result.
String body = String.format("""
{
"commands": [
{
"command": "dial",
"callName": "amd-call",
"from": { "type": "PHONE", "phone": { "number": "%s" } },
"to": { "type": "PHONE", "phone": { "number": "%s" } },
"dialTimeoutDurationSeconds": 45,
"maxCallDurationSeconds": 300,
"events": {
"onAnswer": [
{
"command": "amd",
"events": {
"onHuman": [
{
"command": "messages",
"messagesName": "human-greeting",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
"voiceName": "Emma"
}
}
],
"events": { "onFinish": [{ "command": "hangup" }] }
}
],
"onMachine": [
{ "command": "hangup" }
],
"onBeep": [
{
"command": "messages",
"messagesName": "voicemail-message",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
"voiceName": "Emma"
}
}
],
"events": { "onFinish": [{ "command": "hangup" }] }
}
],
"onUnknown": [
{ "command": "hangup" }
]
}
}
]
}
}
]
}
""", sinchNumber, destinationNumber);
System.out.println("Placing AMD callout from " + sinchNumber + " to " + destinationNumber + " ...");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.header("Authorization", "Basic " + credentials)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int statusCode = response.statusCode();
if (statusCode == 201) {
System.out.println("AMD call created successfully:");
System.out.println(response.body());
} else {
System.err.println("ERROR " + statusCode + ":");
System.err.println(response.body());
System.exit(1);
}
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
System.err.println("ERROR: " + name + " is not set. Export it first.");
System.exit(1);
}
return value;
}
}Calling the Sinch API directly from a browser hits CORS restrictions, so this is a copy-paste template rather than a runnable script. It ships with placeholder strings you must replace, and in production you should proxy the call through your own backend.
// Sinch AMD Callout: browser JS to trigger an outbound call with AMD.
// Note: calling the Sinch API directly from a browser will hit CORS restrictions.
// In production, proxy these calls through your backend.
// Replace the placeholders below; this file will not run as-is.
(async function sinchAmdCallout() {
const projectId = "YOUR_PROJECT_ID";
const keyId = "YOUR_KEY_ID";
const keySecret = "YOUR_KEY_SECRET";
const sinchNumber = "+1XXXXXXXXXX";
const destinationNumber = "+1YYYYYYYYYY";
const baseUrl = "https://voice.api.sinch.com/v2";
const authHeader = "Basic " + btoa(`${keyId}:${keySecret}`);
const payload = {
commands: [
{
command: "dial",
callName: "amd-call",
from: { type: "PHONE", phone: { number: sinchNumber } },
to: { type: "PHONE", phone: { number: destinationNumber } },
dialTimeoutDurationSeconds: 45,
maxCallDurationSeconds: 300,
events: {
onAnswer: [
{
command: "amd",
events: {
onHuman: [
{
command: "messages",
messagesName: "human-greeting",
messages: [
{
type: "SAY",
say: {
text: "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
onMachine: [{ command: "hangup" }],
onBeep: [
{
command: "messages",
messagesName: "voicemail-message",
messages: [
{
type: "SAY",
say: {
text: "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
onUnknown: [{ command: "hangup" }]
}
}
]
}
}
]
};
console.log(`Placing AMD callout from ${sinchNumber} to ${destinationNumber} ...`);
const response = await fetch(
`${baseUrl}/projects/${projectId}/calls`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: authHeader
},
body: JSON.stringify(payload)
}
);
const data = await response.json();
if (response.status === 201) {
console.log("AMD call created successfully:", data);
} else {
console.error(`ERROR ${response.status}:`, data);
}
})();Run amd as the first command in the dial's onAnswer event:
{
"commands": [
{
"command": "dial",
"callName": "amd-call",
"from": { "type": "PHONE", "phone": { "number": "+1SINCH_NUMBER" } },
"to": { "type": "PHONE", "phone": { "number": "+1DESTINATION" } },
"dialTimeoutDurationSeconds": 45,
"maxCallDurationSeconds": 300,
"events": {
"onAnswer": [
{ "command": "amd", "events": { /* onHuman / onMachine / onBeep / onUnknown */ } }
]
}
}
]
}The four AMD branches (verified against the spec's amdEvents schema):
onHuman: a live person picked up. Connect to an agent or start a conversation.onMachine: a machine answered and is playing its greeting (beep not yet detected). Usually hang up or wait for the beep.onBeep: a voicemail beep was detected. Leave your voicemail message now.onUnknown: detection was inconclusive. Safe default is to hang up, or treat as human.
Each branch holds a normal array of SVAML commands. All four are optional: branches you leave out are simply not executed, so you can wire only the outcomes you care about.
Per the spec, the
amdcommand has no fields besidescommandandevents. There are no tuning knobs (no timeouts, no sensitivity) on the command itself.
Use this when you want per-call AMD configuration (for example, choosing the voicemail script by destination) or when you want server-side logs of the AMD verdict. The callback server answers inbound calls and returns SVAML that runs amd, and it logs the call.amd.* result events Sinch posts back.
Paste one of the servers below, export your variables, then run it and expose it with ngrok:
node amd-callback-server.mjs # Express, port 3000
# or
python amd-callback-server.py # Flask, port 3000
ngrok http 3000Copy the ngrok HTTPS URL into your service's webhook configuration (set the service callBehavior to WEBHOOK, see Inbound PSTN Calls for the exact PATCH request). The server's /webhook endpoint receives the call.incoming event and responds with SVAML that answers the call and runs amd.
There is no wrapper command in the webhook response: the commands array is returned at the top level, starting with answer, then amd as the first command after answering.
Requires npm install express, and "type": "module" in package.json (or a .mjs extension).
// Sinch AMD Callback Server: Express.js webhook server.
// Handles inbound call events, responds with SVAML including the AMD command,
// and logs the AMD verdict events Sinch posts back.
// Reads config from exported environment variables (see Setup).
//
// Requirements: npm install express
// Run: node amd-callback-server.mjs
import express from "express";
const sinchNumber = process.env.SINCH_NUMBER || (() => { throw new Error("SINCH_NUMBER not set"); })();
const destinationNumber = process.env.DESTINATION_NUMBER || (() => { throw new Error("DESTINATION_NUMBER not set"); })();
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.json());
// POST /webhook: receives Sinch call events and responds with AMD SVAML
app.post("/webhook", (req, res) => {
const event = req.body?.event;
const call = req.body?.call;
console.log(`Received event: ${event}`, call?.callId);
if (event === "call.incoming") {
// Inbound call: respond with SVAML to answer and run AMD detection.
// Return the commands directly at the top level: `answer` first, then
// `amd` as the first command after answering. AMD then fires one of:
// onHuman, onMachine, onBeep, onUnknown.
const svamlResponse = {
commands: [
{ command: "answer" },
// Run AMD detection as the first thing after answering
{
command: "amd",
events: {
// Live human detected: play a personalized message
onHuman: [
{
command: "messages",
messagesName: "human-greeting",
messages: [
{
type: "SAY",
say: {
text: "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
// Machine greeting detected (waiting for beep): hang up silently
onMachine: [{ command: "hangup" }],
// Beep detected: leave voicemail right after the beep tone
onBeep: [
{
command: "messages",
messagesName: "voicemail-message",
messages: [
{
type: "SAY",
say: {
text: "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
voiceName: "Emma"
}
}
],
events: { onFinish: [{ command: "hangup" }] }
}
],
// AMD inconclusive: hang up rather than risk a bad experience
onUnknown: [{ command: "hangup" }]
}
}
]
};
return res.status(200).json(svamlResponse);
}
// AMD verdict events: this is how you see which branch fired, server-side.
if (event?.startsWith("call.amd.")) {
console.log(`AMD verdict: ${event}`); // call.amd.human, call.amd.beep, ...
return res.status(200).json({ commands: [] }); // no further SVAML needed
}
// Acknowledge all other events
console.log(`Unhandled event: ${event}`);
res.status(200).json({ commands: [] });
});
app.listen(PORT, () => {
console.log(`AMD callback server listening on port ${PORT}`);
console.log(`Set your Sinch service webhook URL to: http://localhost:${PORT}/webhook`);
console.log(`(Use ngrok: ngrok http ${PORT})`);
});Requires pip install flask.
# Sinch AMD Callback Server: Flask webhook server.
# Handles inbound call events, responds with SVAML including the AMD command,
# and logs the AMD verdict events Sinch posts back.
# Reads config from exported environment variables (see Setup).
# Requirements: pip install flask
import os
import sys
from flask import Flask, request, jsonify
sinch_number = os.environ.get("SINCH_NUMBER")
destination_number = os.environ.get("DESTINATION_NUMBER")
for var, name in [(sinch_number, "SINCH_NUMBER"), (destination_number, "DESTINATION_NUMBER")]:
if not var:
print(f"ERROR: {name} is not set. Export it first.", file=sys.stderr)
sys.exit(1)
port = int(os.environ.get("PORT", 3000))
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
"""Handles Sinch call events and responds with AMD SVAML."""
body = request.get_json(force=True)
event = body.get("event")
call = body.get("call", {})
print(f"Received event: {event}, callId: {call.get('callId')}")
if event == "call.incoming":
# Inbound call: answer and run AMD.
# Return the commands directly at the top level: `answer` first, then
# `amd` as the first command after answering. AMD fires one of:
# onHuman, onMachine, onBeep, onUnknown.
svaml_response = {
"commands": [
{"command": "answer"},
# AMD detection: first command after answering
{
"command": "amd",
"events": {
# Live human: play a personalized greeting
"onHuman": [
{
"command": "messages",
"messagesName": "human-greeting",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hello! This is a call from Acme Corp. An agent will be with you shortly.",
"voiceName": "Emma"
}
}
],
"events": {"onFinish": [{"command": "hangup"}]}
}
],
# Machine greeting (no beep yet): hang up silently
"onMachine": [{"command": "hangup"}],
# Beep detected: leave voicemail right after the beep
"onBeep": [
{
"command": "messages",
"messagesName": "voicemail-message",
"messages": [
{
"type": "SAY",
"say": {
"text": "Hi, this is Acme Corp calling about your recent inquiry. Please call us back at 555-1234. Thank you.",
"voiceName": "Emma"
}
}
],
"events": {"onFinish": [{"command": "hangup"}]}
}
],
# Unknown result: hang up safely
"onUnknown": [{"command": "hangup"}]
}
}
]
}
return jsonify(svaml_response), 200
# AMD verdict events: this is how you see which branch fired, server-side.
if event and event.startswith("call.amd."):
print(f"AMD verdict: {event}") # call.amd.human, call.amd.beep, ...
return jsonify({"commands": []}), 200 # no further SVAML needed
print(f"Unhandled event: {event}")
return jsonify({"commands": []}), 200
if __name__ == "__main__":
print(f"AMD callback server listening on port {port}")
print(f"Set your Sinch service webhook URL to: http://localhost:{port}/webhook")
print(f"(Use ngrok: ngrok http {port})")
app.run(host="0.0.0.0", port=port)Both servers above include a handler for the AMD result events. Sinch posts the verdict back to the same webhook URL as one of call.amd.human, call.amd.machine, call.amd.beep, or call.amd.unknown (verified event names). With the handler in place, answering as a human logs AMD verdict: call.amd.human, and a voicemail logs call.amd.machine then call.amd.beep.
Webhook delivery uses CloudEvents (HTTP binary content mode, ce-type: com.sinch.voice.call.control.v1); the JSON body is { event, call }.
| Scenario | On the answering phone | In your callback log |
|---|---|---|
| Human answers | Hears the onHuman greeting | call.amd.human |
| Voicemail | Beep, then hears the onBeep voicemail message | call.amd.machine then call.amd.beep |
| Machine, no beep | Silence then hangup | call.amd.machine |
| Inconclusive | Hangup | call.amd.unknown |
The inline-callout terminal only ever prints AMD call created successfully (HTTP 201). That confirms the call was placed, not which branch ran. Use the callback server for the branch verdict.
- Allow
dialTimeoutDurationSecondsof at least30, because machines may take a few seconds to answer. The scripts use45. - Make
amdthe first command inonAnswer. Do not play messages before AMD runs, since they bias detection. onBeepis the right moment to start your voicemail message, notonMachine.- If you want hold tones during analysis, play them on a separate call leg, not the leg being analysed.
Six callout variants are shown above, plus two callback-server variants. All send an identical AMD payload (same four branches, same voiceName: Emma, dialTimeoutDurationSeconds: 45, maxCallDurationSeconds: 300). Every server-side variant reads credentials from exported environment variables, so export your variables in the same shell before running.
| Variant | Run | Notes |
|---|---|---|
| Bash | bash amd-callout.sh | curl + Basic auth, pretty-prints with jq if present. |
| Python | python amd-callout.py | pip install requests. |
| Node.js | node amd-callout.mjs | Node 18+ native fetch, save as .mjs or set "type": "module". |
| Browser JS | (browser) | Demo only, has placeholder credentials and hits CORS. Proxy through a backend in production. |
| PHP | php amd-callout.php | PHP 8+ with curl. Reads env via getenv(), so export the vars first. |
| Java | javac -d out AmdCallout.java && java -cp out com.sinch.tutorials.amd.AmdCallout | Java 11+. Reads System.getenv, so export the vars first. |
| Node callback server | node amd-callback-server.mjs | Express, npm install express. |
| Python callback server | python amd-callback-server.py | Flask, pip install flask. |
The five runnable callouts (Bash, Python, Node.js, PHP, Java) all read credentials from the exported environment. The browser JS variant is the odd one out: it uses placeholder strings and will not run as-is, since a browser has no environment variables to export and calling the API directly hits CORS. The Node.js callout and the browser JS are intentionally different (server-side Node vs browser), not duplicates.
amdis a non-blocking SVAML command with onlycommandandevents.eventsholds optionalonHuman,onMachine,onBeep, andonUnknownarrays, each a list of SVAML commands.- Its natural home is the first command after the call is answered: inside
dial.events.onAnswerfor outbound calls, or right afteranswerin a webhook response for inbound calls. - The AMD verdict is also delivered as a webhook event:
call.amd.human/call.amd.machine/call.amd.beep/call.amd.unknown.
- A Sinch account with API credentials and a Sinch virtual number.
- For the callback-server path: a publicly reachable URL (use ngrok during development) and a service set to
WEBHOOK.



