{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/voice-2.0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"product":"Voice v2 (preview)","type":"markdown"},"seo":{"title":"Call Hunting","siteUrl":"https://developers.sinch.com","llmstxt":{"title":"Sinch Developer Documentation","description":"LLMs.txt containing a map of all the documentation files for Sinch.","sections":[{"title":"Numbers API","description":"The Numbers API enables you to search for, view, and activate numbers. It's considered a precursor to other APIs in the Sinch product family. The numbers API can be used in tandem with any of our APIs that perform messaging or calling.","includeFiles":["docs/numbers/**/*.md","docs/numbers/**/*.yaml"],"excludeFiles":["docs/numbers/index.md"]},{"title":"Conversation API","description":"Send and receive messages globally on many popular channels with ease and confidence when using Sinch's Conversation API. Conversation API is the preferred API for sending mobile messages on SMS and other social channels with Sinch. It is a simple API with unified error messages, consistent request payloads, and common webhook payloads that are channel-agnostic.","includeFiles":["docs/conversation/**/*.md","docs/conversation/**/*.yaml"],"excludeFiles":["docs/conversation/index.md"]},{"title":"Voice API","description":"The Voice API works as a big telephony switch. The Voice API handles incoming phone calls (also known as incoming call “legs”), sets up outgoing phone calls (or outgoing call “legs”), and bridges the two. The incoming call leg may come in over a data connection (from a smartphone or web application using the Sinch SDKs) or through a local phone number (from the PSTN network). Similarly, the outgoing call leg can be over data (to another smartphone or web application using the Sinch SDKs) or the PSTN network.","includeFiles":["docs/voice/**/*.md","docs/voice/**/*.yaml"],"excludeFiles":["docs/voice/index.md"]},{"title":"Voice API v2","description":"The Voice API works as a big telephony switch. The Voice API handles incoming phone calls (also known as incoming call “legs”), sets up outgoing phone calls (or outgoing call “legs”), and bridges the two. The incoming call leg may come in over a data connection (from a smartphone or web application using the Sinch SDKs) or through a local phone number (from the PSTN network). Similarly, the outgoing call leg can be over data (to another smartphone or web application using the Sinch SDKs) or the PSTN network.","includeFiles":["docs/voice-2.0/**/*.md","docs/voice-2.0/**/*.yaml"],"excludeFiles":["docs/voice-2.0/index.md"]},{"title":"Verification API","description":"The Verification API is a platform for phone number verification. It consists of the API and different software development kits (the Sinch SDKs) that you integrate with your smartphone or web application and cloud based back-end services. Together they enable SMS, Flashcall, Phone Call and Data verification in your application.","includeFiles":["docs/verification/**/*.md","docs/verification/**/*.yaml"],"excludeFiles":["docs/verification/index.md"]},{"title":"Provisioning API","description":"Provisioning API allows you to programmatically set up your senders, accounts and templates on your favorite messaging platforms on the Conversation API. For now, you can create your first WhatsApp channel through Meta's Embedded sign up, you can configure your first SMS App and configure your webhooks. As development continues, we will be adding the most commonly used channels.","includeFiles":["docs/provisioning-api/**/*.md","docs/provisioning-api/**/*.json"],"excludeFiles":["docs/provisioning-api/index.md"]},{"title":"Elastic SIP Trunking API","description":"With Elastic SIP Trunking you can create and manage your SIP trunks and phone numbers programmatically.","includeFiles":["docs/est/**/*.md","docs/est/**/*.yaml"],"excludeFiles":["docs/est/index.md"]},{"title":"Fax API","description":"Send and receive HIPAA compliant faxes on our modern fax platform using our developer-friendly API.","includeFiles":["docs/fax/**/*.md","docs/fax/**/*.yaml"],"excludeFiles":["docs/fax/index.md"]},{"title":"In-app Voice and Video SDK","description":"The In-app Voice and Video SDK enables you to add voice and video calling capabilities directly into your mobile or web application using the Sinch SDKs.","includeFiles":["docs/in-app-calling/**/*.md"],"excludeFiles":["docs/in-app-calling/index.md"]},{"title":"Number Lookup API","description":"The Number Lookup API is designed to provide in-depth information about phone numbers, helping enterprises enhance their communication strategies and prevent fraud. By identifying the type of phone line (for example, mobile, landline, VoIP) and the associated carrier, the API allows businesses to optimize routing, reduce unnecessary costs, and improve customer engagement.","includeFiles":["docs/number-lookup-api-v2/**/*.md","docs/number-lookup-api-v2/**/*.yaml"],"excludeFiles":["docs/number-lookup-api-v2/index.md"]},{"title":"Functions","description":"Serverless compute for voice and messaging. Deploy your code and Sinch routes live calls and messages to it — no infrastructure to run.","includeFiles":["docs/functions/functions/**/*.md","docs/functions/concepts/**/*.md","docs/functions/reference/**/*.md"],"excludeFiles":["docs/functions/functions/index.md"]},{"title":"CLI","description":"One command line for every Sinch API and the full Functions lifecycle — scaffold, run locally, deploy, and manage.","includeFiles":["docs/functions/cli/**/*.md"],"excludeFiles":["docs/functions/cli/index.md"]}]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"call-hunting","__idx":0},"children":["Call Hunting"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Call hunting"]}," is the pattern where the Voice API tries to reach a person at one of several phone numbers and stops as soon as someone answers. There is no dedicated ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hunt"]}," command: you build the pattern by chaining a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," command's lifecycle events (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBusy"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onReject"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onFailure"]},") and using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," to connect the answering leg back to the caller."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Three flavors of hunt are covered here:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Pattern"},"children":["Pattern"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"When to use"},"children":["When to use"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sequential hunt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Try agent A; if no answer, try B; if no answer, try C."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Skill-based routing, cost-sensitive dispatch, on-call escalation."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Simultaneous ring (sim-ring)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ring all agents at once; first to answer wins, others cancelled."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Time-critical alerts, smallest pool, \"first available\" lead distribution."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Hybrid (groups in sequence)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sim-ring tier 1; if all tier-1 miss, sim-ring tier 2."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Tiered support, follow-the-sun teams."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All three patterns are pure SVAML; no proprietary extension is needed."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Start here:"]}," the fastest path to a working hunt is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Pattern 1 (sequential hunt, static SVAML)"]},". It runs from one ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["curl"]}," with no backend server, no public URL, and no service configuration. Get that working first, then reach for the webhook-driven and tiered variants when you need a dynamic agent pool."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"real-life-examples","__idx":2},"children":["Real-life examples"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sales lead dispatch"]},": a new inbound web lead triggers an outbound campaign; the API hunts through a list of sales reps until one picks up, then bridges the lead in."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["On-call escalation"]},": a monitoring alert triggers an outbound call that hunts primary → secondary → manager until acknowledged."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Field-service dispatch"]},": a customer requests a callback; the platform hunts available technicians by region until one accepts."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Personal find-me"]},": one published number is hunted across an employee's desk, mobile, and home line."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"setup","__idx":3},"children":["Setup"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every example below reads its configuration from environment variables. Export them in the shell session you'll run the examples from (or put the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["export"]}," lines in a file you ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["source"]},"):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# Credentials and project (Sinch Dashboard: https://dashboard.sinch.com)\nexport PROJECT_ID=\"your-project-id\"\nexport KEY_ID=\"your-access-key-id\"\nexport KEY_SECRET=\"your-access-key-secret\"\n\n# Numbers (E.164)\nexport SINCH_NUMBER=\"+15551234567\"        # Sinch virtual number, caller ID on every outbound leg\nexport CUSTOMER_NUMBER=\"+15559876543\"     # the customer/target you connect the agent to\nexport AGENT_NUMBERS=\"+15551110001,+15551110002,+15551110003\"\n\n# Patterns 2 and 4 only (webhook-driven)\nexport SERVICE_ID=\"your-voice-service-id\"\nexport CALLBACK_URL=\"https://<your-ngrok-subdomain>.ngrok.io\"   # public HTTPS base URL\nexport PORT=3000                                                # webhook server port\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Variable"},"children":["Variable"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Used by"},"children":["Used by"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Where to get it"},"children":["Where to get it"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROJECT_ID"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["all"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com"},"children":["Sinch Dashboard"]}," → your project."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_SECRET"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["all (HTTP Basic auth)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Dashboard → Access Keys. Sent as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["-u \"$KEY_ID:$KEY_SECRET\""]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["all"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A Sinch virtual number (E.164) provisioned on the project. Used as the caller ID (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},") on every outbound leg."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CUSTOMER_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["all"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The customer/target you connect the agent to."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AGENT_NUMBERS"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["all"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Comma-separated E.164 list of agent destinations. The shell examples read the first three; the Node server reads the whole list."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SERVICE_ID"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Patterns 2/4 only"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Dashboard → your voice service. Needed to configure the webhook."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CALLBACK_URL"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Patterns 2/4 only"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your publicly reachable webhook base URL. During development run ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok http $PORT"]}," and use the HTTPS URL it prints."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Patterns 2/4 Node server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The port the webhook server listens on; defaults to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3000"]}," if unset. Point ngrok at the same port."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tools:"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bash"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["curl"]},", and (optional but recommended) ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["jq"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uuidgen"]}," for the shell patterns; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["node"]}," (ES modules) + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install express"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok"]}," for the webhook server."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Required environment for the first-success path (Pattern 1): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROJECT_ID"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_SECRET"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CUSTOMER_NUMBER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AGENT_NUMBERS"]},". ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No webhook server or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CALLBACK_URL"]}," is needed for Pattern 1."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"prerequisites-knowledge","__idx":4},"children":["Prerequisites knowledge"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Familiarity with the SVAML v2 command reference, specifically ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stopMessages"]},", and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," block."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Auth is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["HTTP Basic"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID:KEY_SECRET"]},")."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-it-works","__idx":5},"children":["How it works"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A hunt always involves ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["two roles"]}," inside one call session:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Target"]},": the customer/lead you ultimately want connected. Often this is the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["first"]}," leg you dial. (For hunts that begin with an inbound call, the inbound leg plays this role.)"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Agent pool"]},": the list of internal numbers the platform tries until one answers."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Three SVAML primitives carry the pattern:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds"]}]}," decides how long each ring attempt lasts before moving on. When it expires, the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]}," event fires."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events.onAnswer"]}]}," is where you ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stopMessages"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," the winning agent to the target."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events.onTimeout"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBusy"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onReject"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onFailure"]}]}," are where you issue the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["next"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," in the hunt chain."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events.onHangup"]}," is what tears the session down once any leg drops."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"pattern-1--sequential-hunt-static-svaml-no-backend-required--start-here","__idx":6},"children":["Pattern 1 — Sequential hunt (static SVAML, no backend required) ← start here"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Call the customer, and on answer try Agent A → B → C until someone picks up. Because the hunt list is static, you send the entire flow inline with the initial ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/projects/{projectId}/calls"]}," request: ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["no webhook server, no public URL, no service configuration"]},". This is the fastest way to see a hunt work end to end."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"run-it","__idx":7},"children":["Run it"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Paste this into a shell where the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#setup"},"children":["Setup"]}," variables are exported. It splits ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AGENT_NUMBERS"]}," into three agent variables, builds the SVAML body with your numbers substituted, and posts it:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"IFS=',' read -r A1 A2 A3 <<< \"$AGENT_NUMBERS\"\nA2=\"${A2:-$A1}\"; A3=\"${A3:-$A2}\"\n\nBODY=$(cat <<EOF\n{\n  \"commands\": [\n    {\n      \"command\": \"dial\",\n      \"callName\": \"customer\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${CUSTOMER_NUMBER}\" } },\n      \"dialTimeoutDurationSeconds\": 30,\n      \"events\": {\n        \"onAnswer\": [\n          { \"command\": \"messages\", \"messagesName\": \"hold\",\n            \"messages\": [ { \"type\": \"SAY\",\n              \"say\": { \"text\": \"Please hold while we connect you to an agent.\", \"voiceName\": \"Emma\" } } ] },\n\n          { \"command\": \"dial\", \"callName\": \"agent-1\",\n            \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n            \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A1}\" } },\n            \"dialTimeoutDurationSeconds\": 20,\n            \"events\": {\n              \"onAnswer\": [\n                { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                { \"command\": \"bridgeCall\",   \"bridgeName\": \"hunt-bridge\" }\n              ],\n              \"onTimeout\": [\n                { \"command\": \"dial\", \"callName\": \"agent-2\",\n                  \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n                  \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A2}\" } },\n                  \"dialTimeoutDurationSeconds\": 20,\n                  \"events\": {\n                    \"onAnswer\": [\n                      { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                      { \"command\": \"bridgeCall\",   \"bridgeName\": \"hunt-bridge\" }\n                    ],\n                    \"onTimeout\": [\n                      { \"command\": \"dial\", \"callName\": \"agent-3\",\n                        \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n                        \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A3}\" } },\n                        \"dialTimeoutDurationSeconds\": 20,\n                        \"events\": {\n                          \"onAnswer\": [\n                            { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                            { \"command\": \"bridgeCall\",   \"bridgeName\": \"hunt-bridge\" }\n                          ],\n                          \"onTimeout\": [\n                            { \"command\": \"hangup\", \"callName\": \"customer\" }\n                          ]\n                        }\n                      }\n                    ]\n                  }\n                }\n              ]\n            }\n          }\n        ],\n        \"onHangup\": [ { \"command\": \"hangup\" } ]\n      }\n    }\n  ]\n}\nEOF\n)\n\ncurl -s -X POST \\\n  -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/calls\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d \"$BODY\" | jq '.'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Key points"]}]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Each nested ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," is independent: when ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]}," fires on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["agent-1"]},", that leg has already been torn down, so the next ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," opens a fresh channel. (Per the spec: \"Commands that appear inside event handlers form independent sequences and execute in their own context.\")"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," with a shared ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]}," connects the answering agent to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["customer"]},". The first call to enter the bridge creates it; the second joins (auto-created on first reference, per the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," schema)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["customer"]}," leg keeps playing the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hold"]}," messages (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]}," is non-blocking) until the answering agent issues ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stopMessages"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"what-success-looks-like","__idx":8},"children":["What success looks like"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["customer"]}," number rings; on answer the caller hears \"Please hold…\"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["agent-1"]}," rings. Answer it → the hold message stops and the two legs are bridged into a single conversation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Let ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["agent-1"]}," ring out (or use a number you can leave unanswered) → after ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["agent-2"]}," rings, and so on."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If all three time out, the caller hears the apology message and the call hangs up."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"when-not-to-use-static-svaml","__idx":9},"children":["When ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["not"]}," to use static SVAML"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The hunt list changes per call (skill matching, agent presence, geolocation)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You need to record the result of each leg in your own systems before deciding the next number."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You need to retry the same agent later in the day."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use the webhook pattern below in those cases."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"pattern-2--sequential-hunt-webhook-driven-dynamic-agent-pool","__idx":10},"children":["Pattern 2 — Sequential hunt (webhook-driven, dynamic agent pool)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Here the platform calls ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["your"]}," backend on every lifecycle event, and your backend decides the next number. This costs an HTTP round-trip per hop but gives you full control over the agent list."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-configure-the-service-for-webhooks","__idx":11},"children":["1. Configure the service for webhooks"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -X PATCH \\\n  -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/services/$SERVICE_ID\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"callBehavior\": {\n      \"type\": \"WEBHOOK\",\n      \"webhook\": {\n        \"url\": \"'\"$CALLBACK_URL\"'/voice/events\",\n        \"fallbackUrl\": \"'\"$CALLBACK_URL\"'/voice/events\"\n      }\n    }\n  }'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-initiate-the-customer-call-without-inline-events","__idx":12},"children":["2. Initiate the customer call without inline ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," property is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["omitted"]}," on a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]},", Sinch falls back to the service-level webhook for that leg's lifecycle events, which is exactly what we want."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -s -X POST \\\n  -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/calls\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"commands\": [\n      {\n        \"command\": \"dial\",\n        \"callName\": \"customer\",\n        \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"'\"$SINCH_NUMBER\"'\" } },\n        \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"'\"$CUSTOMER_NUMBER\"'\" } },\n        \"dialTimeoutDurationSeconds\": 30\n      }\n    ]\n  }' | jq '.'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-the-webhook-requestresponse-contract","__idx":13},"children":["3. The webhook request/response contract"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Webhook delivery uses ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["CloudEvents 1.0 HTTP binary mode"]},". CloudEvent metadata travels in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ce-*"]}," headers (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ce-type"]}," is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["com.sinch.voice.call.control.v1"]},"); the JSON body is the event payload with ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["two"]}," properties: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"http","header":{"controls":{"copy":{}}},"source":"POST /voice/events\nce-specversion: 1.0\nce-type: com.sinch.voice.call.control.v1\nce-source: projects/<projectId>/services/<serviceId>\nce-id: <uuid>\nce-time: 2026-04-01T12:00:00Z\ncontent-type: application/json\n\n{\n  \"event\": \"call.answered\",\n  \"call\": { \"callId\": \"...\", \"sessionId\": \"01BX...\", \"direction\": \"OUTBOUND\", \"callResult\": \"...\", ... }\n}\n","lang":"http"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event"]}," enum (verified) includes: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.busy"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.rejected"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.timeout"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.hangup"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.failed"]},", plus ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.amd.*"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.message.*"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.recording.*"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Respond with HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," and a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"commands\": [...] }"]}," body. An empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," array means \"take no action.\""]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"4-the-webhook-server","__idx":14},"children":["4. The webhook server"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The server maintains an in-memory hunt cursor keyed by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," (a restart wipes in-flight hunts; persist the cursor in a real store for production). Its flow:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]}," for the customer leg → respond with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hold"]}," message plus the first agent ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," (no ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]},", so its events also flow back to the webhook)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.timeout"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.busy"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.rejected"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.failed"]}," for an agent leg → look up the hunt cursor for that ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]},", advance it, and respond with the next agent ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]},", or a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," if the list is exhausted."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]}," for an agent leg → respond with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stopMessages"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.hangup"]}," → clear the cursor and respond with an empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," array; the platform tears the session down."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Save this as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.js"]}," (it reads ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AGENT_NUMBERS"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," from the exported environment):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"// Sinch Voice API v2 — Webhook-driven sequential hunt (Pattern 2).\n// Maintains an in-memory cursor keyed by sessionId. Restart wipes state.\n//\n// Requirements: npm install express\n// Run:  node server.js\n// Expose: ngrok http $PORT  (then set the service's callBehavior.webhook.url)\n\nimport express from \"express\";\n\nconst SINCH_NUMBER = process.env.SINCH_NUMBER || (() => { throw new Error(\"SINCH_NUMBER not set\"); })();\nconst AGENT_LIST = (process.env.AGENT_NUMBERS || \"\")\n  .split(\",\")\n  .map((s) => s.trim())\n  .filter(Boolean);\nif (AGENT_LIST.length === 0) throw new Error(\"AGENT_NUMBERS not set\");\nconst PORT = process.env.PORT || 3000;\n\n// In-memory hunt cursor: sessionId -> { index }\nconst cursors = new Map();\n\nfunction dialAgent(number, name) {\n  return {\n    command: \"dial\",\n    callName: name,\n    from: { type: \"PHONE\", phone: { number: SINCH_NUMBER } },\n    to:   { type: \"PHONE\", phone: { number } },\n    dialTimeoutDurationSeconds: 20,\n    // No `events` => fall back to the service webhook for lifecycle events.\n  };\n}\n\nconst app = express();\napp.use(express.json());\n\napp.post(\"/voice/events\", (req, res) => {\n  const { event, call } = req.body || {};\n  if (!event || !call) return res.status(200).json({ commands: [] });\n\n  const sessionId = call.sessionId;\n  const callName  = call.callName;\n\n  console.log(`event=${event} sessionId=${sessionId} callName=${callName}`);\n\n  // Inbound call arrives — the inbound leg IS the customer; jump straight\n  // to dialing agent 1. The inbound leg joins the bridge here; the\n  // answering agent joins it too.\n  if (event === \"call.incoming\") {\n    cursors.set(sessionId, { index: 0 });\n    return res.status(200).json({\n      commands: [\n        { command: \"answer\" },\n        { command: \"messages\", messagesName: \"hold\",\n          messages: [{ type: \"SAY\", say: { text: \"Please hold while we find an agent.\", voiceName: \"Emma\" } }] },\n        { command: \"bridgeCall\", bridgeName: \"hunt-bridge\" },\n        dialAgent(AGENT_LIST[0], \"agent-0\"),\n      ],\n      events: { onHangup: [{ command: \"hangup\" }] },\n    });\n  }\n\n  // Outbound call answered (customer leg picked up after we dialed them).\n  if (event === \"call.answered\" && callName === \"customer\") {\n    cursors.set(sessionId, { index: 0 });\n    return res.status(200).json({\n      commands: [\n        { command: \"messages\", messagesName: \"hold\",\n          messages: [{ type: \"SAY\", say: { text: \"Please hold while we find an agent.\", voiceName: \"Emma\" } }] },\n        dialAgent(AGENT_LIST[0], \"agent-0\"),\n      ],\n    });\n  }\n\n  // Agent leg answered — bridge them in and stop the hold message.\n  if (event === \"call.answered\" && callName?.startsWith(\"agent-\")) {\n    return res.status(200).json({\n      commands: [\n        { command: \"stopMessages\", messagesName: \"hold\" },\n        { command: \"bridgeCall\",   bridgeName: \"hunt-bridge\" },\n      ],\n    });\n  }\n\n  // Agent leg failed to connect — advance the cursor and try the next agent.\n  const failed = [\"call.timeout\", \"call.busy\", \"call.rejected\", \"call.failed\"].includes(event);\n  if (failed && callName?.startsWith(\"agent-\")) {\n    const cursor = cursors.get(sessionId) || { index: 0 };\n    cursor.index += 1;\n    cursors.set(sessionId, cursor);\n\n    if (cursor.index >= AGENT_LIST.length) {\n      // No more agents — apologise and hang up.\n      cursors.delete(sessionId);\n      return res.status(200).json({\n        commands: [\n          { command: \"messages\", messagesName: \"noanswer\",\n            messages: [{ type: \"SAY\",\n              say: { text: \"We are sorry, no agent is available. Please try again later.\",\n                     voiceName: \"Emma\" } }],\n            events: { onFinish: [{ command: \"hangup\" }] } },\n        ],\n      });\n    }\n    return res.status(200).json({\n      commands: [dialAgent(AGENT_LIST[cursor.index], `agent-${cursor.index}`)],\n    });\n  }\n\n  // Cleanup on hangup of the customer leg.\n  if (event === \"call.hangup\") {\n    cursors.delete(sessionId);\n  }\n\n  return res.status(200).json({ commands: [] });\n});\n\napp.listen(PORT, () => {\n  console.log(`Hunt webhook server listening on :${PORT}`);\n  console.log(`Configure service callBehavior.webhook.url -> https://<your-ngrok>/voice/events`);\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]}," fires for an agent leg, the response it sends is:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"HTTP/1.1 200 OK\n{\n  \"commands\": [\n    { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n    { \"command\": \"bridgeCall\",   \"bridgeName\": \"hunt-bridge\" }\n  ]\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"5-run-the-server","__idx":15},"children":["5. Run the server"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"npm install express\nnode server.js               # logs the port it is listening on\nngrok http $PORT             # in a second terminal\n# Then run step 1 with CALLBACK_URL set to the ngrok HTTPS URL,\n# and step 2 to start the customer call.\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"pattern-3--simultaneous-ring-sim-ring","__idx":16},"children":["Pattern 3 — Simultaneous ring (sim-ring)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For a \"first available wins\" hunt, fan out multiple ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," commands in parallel from the customer leg, and let the first ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]}," race the others to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]},". Because ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," is non-blocking, all three calls start within a few milliseconds of each other."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"run-it-1","__idx":17},"children":["Run it"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Paste this into a shell where the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#setup"},"children":["Setup"]}," variables are exported:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"IFS=',' read -r A1 A2 A3 <<< \"$AGENT_NUMBERS\"\nA2=\"${A2:-$A1}\"; A3=\"${A3:-$A2}\"\n\nBODY=$(cat <<EOF\n{\n  \"commands\": [\n    {\n      \"command\": \"dial\",\n      \"callName\": \"customer\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${CUSTOMER_NUMBER}\" } },\n      \"dialTimeoutDurationSeconds\": 30,\n      \"events\": {\n        \"onAnswer\": [\n          { \"command\": \"messages\", \"messagesName\": \"hold\",\n            \"messages\": [ { \"type\": \"SAY\",\n              \"say\": { \"text\": \"Connecting you now.\", \"voiceName\": \"Emma\" } } ] },\n\n          { \"command\": \"dial\", \"callName\": \"agent-A\",\n            \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n            \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A1}\" } },\n            \"dialTimeoutDurationSeconds\": 25,\n            \"events\": {\n              \"onAnswer\": [\n                { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-B\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-C\" },\n                { \"command\": \"bridgeCall\",   \"bridgeName\":   \"hunt-bridge\" }\n              ]\n            }\n          },\n          { \"command\": \"dial\", \"callName\": \"agent-B\",\n            \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n            \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A2}\" } },\n            \"dialTimeoutDurationSeconds\": 25,\n            \"events\": {\n              \"onAnswer\": [\n                { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-A\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-C\" },\n                { \"command\": \"bridgeCall\",   \"bridgeName\":   \"hunt-bridge\" }\n              ]\n            }\n          },\n          { \"command\": \"dial\", \"callName\": \"agent-C\",\n            \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"${SINCH_NUMBER}\" } },\n            \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"${A3}\" } },\n            \"dialTimeoutDurationSeconds\": 25,\n            \"events\": {\n              \"onAnswer\": [\n                { \"command\": \"stopMessages\", \"messagesName\": \"hold\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-A\" },\n                { \"command\": \"hangup\",       \"callName\":     \"agent-B\" },\n                { \"command\": \"bridgeCall\",   \"bridgeName\":   \"hunt-bridge\" }\n              ]\n            }\n          }\n        ],\n        \"onHangup\": [ { \"command\": \"hangup\" } ]\n      }\n    }\n  ]\n}\nEOF\n)\n\ncurl -s -X POST \\\n  -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/calls\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d \"$BODY\" | jq '.'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Race-condition guards"]}]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Losing legs are torn down with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup callName: \"...\""]}," from inside the winner's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]},". A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," targeted at a leg that has already ended is a no-op (the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," schema permits targeting an ended leg; subsequent media commands simply do not execute), so two near-simultaneous answers cannot deadlock the session."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Only the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["first"]}," leg to enter the bridge is connected to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["customer"]},". Late answers are dropped by the explicit ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," calls."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"pattern-4--tiered-hybrid-hunt","__idx":18},"children":["Pattern 4 — Tiered (hybrid) hunt"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Combine the two: sim-ring tier 1, then on failure of ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["all"]}," tier-1 legs, fall through to sim-ring tier 2. The cleanest implementation is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["webhook-driven"]},": your backend keeps a tally of which tier-1 legs have ended, fires tier 2 when the count reaches the tier-1 size, and uses ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup callName:"]}," to clean up any tier-1 stragglers."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Pseudocode:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"on call.answered(customer)              -> commands: hold + fan out tier 1\non call.timeout|busy|rejected|failed(tier1 leg)\n                                        -> tier1_done++\n                                           if tier1_done == |tier1| -> commands: fan out tier 2\n                                           else                     -> commands: []\non call.answered(agent)                 -> commands: stopMessages + hangup-others + bridgeCall\non call.hangup(customer)                -> commands: [] (platform tears session down)\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"production-readiness-checklist","__idx":19},"children":["Production-readiness checklist"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Concern"},"children":["Concern"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What to do"},"children":["What to do"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["All-miss handling"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ensure every hunt terminates when no agent answers: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBusy"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onReject"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onFailure"]}," on the last sequential leg (or a service-level fallback / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["maxCallDurationSeconds"]}," for sim-ring). The static Pattern 1 above only chains on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]},"; add the other three."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Voicemail / AMD"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Insert an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["amd"]}," command before ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," so a voicemail pickup doesn't count as a successful hunt. AMD events are ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHuman"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onMachine"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBeep"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onUnknown"]}," (webhook: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.amd.machine"]}," etc.). On machine/beep, treat the leg as a miss."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Whisper before bridging"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Replace ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," in the winner's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]}," with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]}," (\"Incoming lead from Madrid…\") followed by a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook"]}," SVAML command that bridges only after the agent confirms."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Concurrent hunts to the same agent"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The platform does not deduplicate. Track agent occupancy in your backend and skip busy agents rather than relying on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.busy"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compliance / call recording"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Add ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["startRecording"]}," immediately after the winning ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/recording-and-transcription"},"children":["Recording & Transcription"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Rate / pacing"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["For mass outbound (one customer to one of N agents, with M customers in flight) use the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/call-pacing"},"children":["Batch API"]},": set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batchOptions.maxCps"]}," so you don't burst-dial your trunk."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Observability"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Each leg has its own ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},". Persist these keyed by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," for post-call reporting."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Cost"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," you fan out is a billable attempt even if unanswered. Sequential is cheaper than sim-ring; sim-ring is faster."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Cancellation"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["If the customer hangs up during the hunt, wire ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]}," on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["customer"]}," leg (Patterns 1 and 3 do this) or return a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.hangup"]}," webhook event so the remaining agent legs are torn down."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing","__idx":20},"children":["Testing"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use a sandboxed project and your own mobile number as a stand-in for \"agent 1\" with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds: 10"]},". Reject the call instead of answering; with full failure-event handling, the next agent should ring immediately. (With the static Pattern 1 as written, only ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["timeout"]}," advances the hunt; let it ring out to test progression.)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Run the Pattern 1 example, then pull the resulting session to verify which leg won:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# Note: session calls expose callId/callResult/callReason/answerTime — NOT callName.\ncurl -s -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/sessions/$SESSION_ID\" \\\n  | jq '.calls[] | {callId, callResult, callReason, answerTime, to}'\n","lang":"bash"},"children":[]}]},"headings":[{"value":"Call Hunting","id":"call-hunting","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Real-life examples","id":"real-life-examples","depth":2},{"value":"Setup","id":"setup","depth":2},{"value":"Prerequisites knowledge","id":"prerequisites-knowledge","depth":2},{"value":"How it works","id":"how-it-works","depth":2},{"value":"Pattern 1 — Sequential hunt (static SVAML, no backend required) ← start here","id":"pattern-1--sequential-hunt-static-svaml-no-backend-required--start-here","depth":2},{"value":"Run it","id":"run-it","depth":3},{"value":"What success looks like","id":"what-success-looks-like","depth":3},{"value":"When not to use static SVAML","id":"when-not-to-use-static-svaml","depth":3},{"value":"Pattern 2 — Sequential hunt (webhook-driven, dynamic agent pool)","id":"pattern-2--sequential-hunt-webhook-driven-dynamic-agent-pool","depth":2},{"value":"1. Configure the service for webhooks","id":"1-configure-the-service-for-webhooks","depth":3},{"value":"2. Initiate the customer call without inline events","id":"2-initiate-the-customer-call-without-inline-events","depth":3},{"value":"3. The webhook request/response contract","id":"3-the-webhook-requestresponse-contract","depth":3},{"value":"4. The webhook server","id":"4-the-webhook-server","depth":3},{"value":"5. Run the server","id":"5-run-the-server","depth":3},{"value":"Pattern 3 — Simultaneous ring (sim-ring)","id":"pattern-3--simultaneous-ring-sim-ring","depth":2},{"value":"Run it","id":"run-it-1","depth":3},{"value":"Pattern 4 — Tiered (hybrid) hunt","id":"pattern-4--tiered-hybrid-hunt","depth":2},{"value":"Production-readiness checklist","id":"production-readiness-checklist","depth":2},{"value":"Testing","id":"testing","depth":2}],"frontmatter":{"seo":{"title":"Call Hunting"}},"lastModified":"2026-08-31T13:48:15.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/voice-2.0/tutorials/call-hunting","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}