{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/voice-2.0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"product":"Voice v2 (preview)","type":"markdown"},"seo":{"title":"Mask a Phone Call (Number Masking)","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":"mask-a-phone-call-number-masking","__idx":0},"children":["Mask a Phone Call (Number Masking)"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Number masking lets two parties speak over a bridged phone call without either party ever seeing the other's real phone number. Both parties see only your Sinch virtual number as the caller ID. The Voice API v2 builds this from three primitives:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Answer"]}," an inbound call from Party A on your Sinch number (delivered as a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," webhook)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}]}," Party B with the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sinch number"]}," as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},", so Party B sees the Sinch number, not Party A."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}]}," both legs into a shared ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]}," so audio flows A ↔ B."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When either party hangs up, the other leg is torn down too (via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]},")."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Masking is webhook-driven, so first success is a three-part loop."]}," Unlike a one-shot outbound call, you can't trigger a masked bridge with a single curl and watch it work. First success requires three things running together: (1) your webhook server is up, (2) it's reachable from the internet (ngrok), (3) your Sinch ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["service"]}," points its webhook at it. The ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#quick-start-minimal-end-to-end"},"children":["Quick start"]}," below gets all three in place, then you dial your Sinch number. If you'd rather see something work ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["before"]}," wiring up a phone, jump to ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#step-0-smoke-test-the-svaml-no-phone-needed"},"children":["Step 0"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For the inbound webhook contract itself (CloudEvents headers, the response shape, signature verification), see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/inbound-pstn"},"children":["Handle Inbound PSTN Calls"]},". This tutorial focuses on the masking pattern layered on top of it."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"success","name":"Build this with AI or the CLI"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com/functions/templates/node/number-masking/deploy"},"children":["Deploy the number-masking template"]}]},": it implements exactly this pattern — it answers ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," from Party A, dials Party B with the Sinch number as the caller ID, bridges both legs, and tears the other leg down on hangup. Fill in the values, then either deploy it as it is or take it into Assistant and change it in chat."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Describe your own instead:"]}," ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com/functions/builder/new?type=voice"},"children":["open Assistant"]}," and describe what the call should do. It writes, runs and deploys the function for you."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Or use the CLI:"]}," Execute ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch functions init number-masking"]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch functions dev"]}," so real calls reach your server without a tunnel. Execute ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch functions deploy"]}," to put it in production once you're satisfied. See the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/functions/functions/quickstart"},"children":["Sinch Functions quickstart"]},"."]}]},{"$$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":["Ride-sharing"]},": Driver and passenger talk without sharing personal numbers. The app hands out a masked Sinch number per ride."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Marketplace transactions"]},": Buyer and seller call each other through the platform without revealing real numbers."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Healthcare"]},": A patient calls a Sinch number to reach their doctor, who sees only the clinic's virtual number."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Delivery services"]},": Agent and recipient coordinate through a disposable Sinch number that expires after delivery."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"setup","__idx":3},"children":["Setup"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every command and server in this tutorial reads its configuration from ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["environment variables"]},". Export them in the shell you'll use to run the server and the curl commands:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"export PROJECT_ID=\"your-project-id\"\nexport KEY_ID=\"your-key-id\"\nexport KEY_SECRET=\"your-key-secret\"\nexport SERVICE_ID=\"your-service-id\"\nexport SINCH_NUMBER=\"+14045001000\"        # the masking number both parties see\nexport DESTINATION_NUMBER=\"+15551234567\"  # Party B for the single-pair demo\nexport CALLBACK_URL=\"\"                     # fill in after you start ngrok (Quick start step 2)\nexport PORT=\"3000\"\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Exports live only in the current shell session. Re-export them in any new terminal you open (or add them to your shell profile). Every server below reads these variables directly from the environment and exits with an error if a required one is missing."]}]},{"$$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":"What it is"},"children":["What it is"]},{"$$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":["Your Voice project ID"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com"},"children":["Sinch Dashboard"]}," → Voice → 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":["API credentials for HTTP Basic auth"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Dashboard → Access keys"]}]},{"$$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":["The service that owns your Sinch number"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Dashboard → Voice → Services (or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v2/projects/{projectId}/services"]},"). This is the service whose ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callBehavior"]}," you switch to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WEBHOOK"]},"."]}]},{"$$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":["Your Sinch virtual number, E.164 (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+14045001000"]},"). This is the masking number both parties will see."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Dashboard → Numbers, routed to the service above"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Party B, the number the inbound caller gets bridged to, E.164"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Any phone you can answer"]}]},{"$$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":["Your public webhook ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["base"]}," URL (the ngrok URL)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Generated by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok"]}," in the Quick start"]}]},{"$$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":["Local server port (defaults to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3000"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["(optional)"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Tools and dependencies (pick one server language):"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Node.js 18+"]}," (ES modules + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]},"): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install express"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Python 3.8+"]}," with Flask: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pip install flask"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PHP 8+"]}," with Slim 4: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["composer require slim/slim slim/psr7 nyholm/psr7 php-di/php-di"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Java 11+"]}," with Spring Boot: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["spring-boot-starter-web"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://ngrok.com"},"children":["ngrok"]}]}," (or any tunnel) to expose your local server."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["HTTP Basic auth"]},": every API/PATCH call authenticates with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["-u \"$KEY_ID:$KEY_SECRET\""]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The webhook servers in this tutorial all listen at ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /webhook"]}]},". So your full webhook URL is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CALLBACK_URL"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/webhook"]},"."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"step-0-smoke-test-the-svaml-no-phone-needed","__idx":4},"children":["Step 0: Smoke-test the SVAML (no phone needed)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before any tunneling, confirm the masking SVAML you intend to return is valid. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /svaml/validate"]}," checks a full SVAML payload (commands, optional ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]},", optional ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]},") with the same rules as a live call and returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"isValid\": true | false, \"errors\": [...] }"]},". You can optionally pass ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"validationType\": \"STRICT\""]}," to catch unrecognized properties."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The request wraps the SVAML payload inside a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["svaml"]}," property:"]},{"$$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/svaml/validate\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"$(printf '{\n  \"validationType\": \"STRICT\",\n  \"svaml\": {\n    \"callName\": \"caller\",\n    \"commands\": [\n      { \"command\": \"answer\" },\n      {\n        \"command\": \"messages\",\n        \"messagesName\": \"greeting\",\n        \"messages\": [\n          { \"type\": \"SAY\", \"say\": { \"text\": \"Please hold while we connect your call.\", \"voiceName\": \"Emma\" } }\n        ]\n      },\n      { \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" },\n      {\n        \"command\": \"dial\",\n        \"callName\": \"callee\",\n        \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n        \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n        \"dialTimeoutDurationSeconds\": 30,\n        \"events\": {\n          \"onAnswer\": [{ \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" }],\n          \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"caller\" }]\n        }\n      }\n    ],\n    \"events\": {\n      \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"callee\" }]\n    }\n  }\n}' \"$SINCH_NUMBER\" \"$DESTINATION_NUMBER\")\"\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Expected: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"isValid\":true}"]},". A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," means validation ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["ran"]},", not that the payload passed, so always read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isValid"]},"."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"quick-start-minimal-end-to-end","__idx":5},"children":["Quick start: minimal end-to-end"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-start-the-webhook-server","__idx":6},"children":["1. Start the webhook server"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Save one of the servers below to a file and run it. Each reads ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," from the environment (see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#setup"},"children":["Setup"]},") and exits with an error if either is missing. On ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," it answers, plays a hold greeting, bridges Party A, and dials ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," (Party B) from the Sinch number. On any other event it acknowledges with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"commands\": []}"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Node.js"]},": save as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.mjs"]}," (the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".mjs"]}," extension enables ES modules), then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install express"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["node server.mjs"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"js","header":{"controls":{"copy":{}}},"source":"// server.mjs: Sinch Number Masking webhook server (Express).\n// Requires: npm install express\n// Env: SINCH_NUMBER, DESTINATION_NUMBER, PORT (optional, defaults to 3000)\nimport express from \"express\";\n\nconst sinchNumber       = process.env.SINCH_NUMBER;\nconst destinationNumber = process.env.DESTINATION_NUMBER;\nconst PORT              = process.env.PORT || 3000;\n\nif (!sinchNumber || !destinationNumber) {\n  console.error(\"ERROR: SINCH_NUMBER and DESTINATION_NUMBER must be set.\");\n  process.exit(1);\n}\n\nconst app = express();\napp.use(express.json());\n\n// POST /webhook: receives Sinch call events and responds with SVAML\napp.post(\"/webhook\", (req, res) => {\n  const event = req.body?.event;\n  const call  = req.body?.call;\n\n  console.log(`Received webhook event: ${event}`, JSON.stringify(call, null, 2));\n\n  if (event === \"call.incoming\") {\n    // Inbound call to the Sinch number from Party A. Respond with SVAML to:\n    // answer, play a hold greeting, bridge Party A, dial Party B from the Sinch\n    // number (masking Party A), and bridge Party B in when they answer.\n    // Commands run directly at the top level. \"callName\" names the inbound\n    // (caller) leg; \"events.onHangup\" handles the caller hanging up.\n    return res.status(200).json({\n      callName: \"caller\",\n      commands: [\n        { command: \"answer\" },\n        {\n          command: \"messages\",\n          messagesName: \"greeting\",\n          messages: [\n            { type: \"SAY\", say: { text: \"Please hold while we connect your call.\", voiceName: \"Emma\" } }\n          ]\n        },\n        // Add Party A to a named bridge (auto-created if it does not exist)\n        { command: \"bridgeCall\", bridgeName: \"main-bridge\" },\n        {\n          command: \"dial\",\n          callName: \"callee\",\n          // Party B sees the Sinch number, not Party A's real number\n          from: { type: \"PHONE\", phone: { number: sinchNumber } },\n          // In production, look up Party B from your DB keyed on call.to.phone.number\n          to:   { type: \"PHONE\", phone: { number: destinationNumber } },\n          dialTimeoutDurationSeconds: 30,\n          events: {\n            onAnswer: [{ command: \"bridgeCall\", bridgeName: \"main-bridge\" }],\n            onHangup: [{ command: \"hangup\", callName: \"caller\" }]\n          }\n        }\n      ],\n      // Caller hangs up -> end the outbound (callee) leg too\n      events: { onHangup: [{ command: \"hangup\", callName: \"callee\" }] }\n    });\n  }\n\n  console.log(`Unhandled event: ${event}`);\n  res.status(200).json({ commands: [] });\n});\n\napp.listen(PORT, () => {\n  console.log(`Number masking webhook server listening on port ${PORT}`);\n});\n","lang":"js"},"children":[]},{"$$mdtype":"Tag","name":"details","attributes":{},"children":[{"$$mdtype":"Tag","name":"summary","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Python (Flask)"]},": save as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pip install flask"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["python server.py"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"# server.py: Sinch Number Masking webhook server (Flask).\n# Requires: pip install flask\n# Env: SINCH_NUMBER, DESTINATION_NUMBER, PORT (optional, defaults to 3000)\nimport os\nimport sys\nimport json\nfrom flask import Flask, request, jsonify\n\nsinch_number       = os.environ.get(\"SINCH_NUMBER\")\ndestination_number = os.environ.get(\"DESTINATION_NUMBER\")\n\nif not sinch_number or not destination_number:\n    print(\"ERROR: SINCH_NUMBER and DESTINATION_NUMBER must be set.\", file=sys.stderr)\n    sys.exit(1)\n\napp = Flask(__name__)\n\n@app.route(\"/webhook\", methods=[\"POST\"])\ndef webhook():\n    \"\"\"Receives Sinch call events and responds with SVAML commands.\"\"\"\n    body  = request.get_json(force=True)\n    event = body.get(\"event\")\n    call  = body.get(\"call\", {})\n\n    print(f\"Received webhook event: {event}\")\n    print(json.dumps(call, indent=2))\n\n    if event == \"call.incoming\":\n        # Commands run directly at the top level; \"callName\" names the inbound\n        # (caller) leg and \"events.onHangup\" handles the caller hanging up.\n        return jsonify({\n            \"callName\": \"caller\",\n            \"commands\": [\n                {\"command\": \"answer\"},\n                {\n                    \"command\": \"messages\",\n                    \"messagesName\": \"greeting\",\n                    \"messages\": [\n                        {\"type\": \"SAY\", \"say\": {\"text\": \"Please hold while we connect your call.\", \"voiceName\": \"Emma\"}}\n                    ]\n                },\n                # Add Party A to a named bridge\n                {\"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\"},\n                {\n                    \"command\": \"dial\",\n                    \"callName\": \"callee\",\n                    # Party B sees the Sinch number\n                    \"from\": {\"type\": \"PHONE\", \"phone\": {\"number\": sinch_number}},\n                    # In production, look up the destination from your DB (call[\"to\"])\n                    \"to\":   {\"type\": \"PHONE\", \"phone\": {\"number\": destination_number}},\n                    \"dialTimeoutDurationSeconds\": 30,\n                    \"events\": {\n                        \"onAnswer\": [{\"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\"}],\n                        \"onHangup\": [{\"command\": \"hangup\", \"callName\": \"caller\"}]\n                    }\n                }\n            ],\n            \"events\": {\"onHangup\": [{\"command\": \"hangup\", \"callName\": \"callee\"}]}\n        }), 200\n\n    print(f\"Unhandled event: {event}\")\n    return jsonify({\"commands\": []}), 200\n\n\nif __name__ == \"__main__\":\n    port = int(os.environ.get(\"PORT\", 3000))\n    print(f\"Number masking webhook server listening on port {port}\")\n    app.run(host=\"0.0.0.0\", port=port)\n","lang":"python"},"children":[]}]},{"$$mdtype":"Tag","name":"details","attributes":{},"children":[{"$$mdtype":"Tag","name":"summary","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PHP (Slim 4)"]},": save as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.php"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["composer require slim/slim slim/psr7 nyholm/psr7 php-di/php-di"]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["php -S 0.0.0.0:3000 server.php"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"php","header":{"controls":{"copy":{}}},"source":"<?php\n// server.php: Sinch Number Masking webhook server (Slim 4).\n// Requires: composer require slim/slim slim/psr7 nyholm/psr7 php-di/php-di\n// Env: SINCH_NUMBER, DESTINATION_NUMBER, PORT (optional, defaults to 3000)\n\nuse Psr\\Http\\Message\\ResponseInterface as Response;\nuse Psr\\Http\\Message\\ServerRequestInterface as Request;\nuse Slim\\Factory\\AppFactory;\n\nrequire __DIR__ . '/vendor/autoload.php';\n\n$sinchNumber       = getenv('SINCH_NUMBER')       ?: die(\"ERROR: SINCH_NUMBER not set.\\n\");\n$destinationNumber = getenv('DESTINATION_NUMBER') ?: die(\"ERROR: DESTINATION_NUMBER not set.\\n\");\n\n$app = AppFactory::create();\n$app->addBodyParsingMiddleware();\n\n// POST /webhook: receives Sinch call events and responds with SVAML\n$app->post('/webhook', function (Request $request, Response $response) use ($sinchNumber, $destinationNumber) {\n    $body  = $request->getParsedBody();\n    $event = $body['event'] ?? null;\n    $call  = $body['call']  ?? [];\n\n    error_log(\"Received webhook event: {$event}\");\n    error_log(json_encode($call, JSON_PRETTY_PRINT));\n\n    if ($event === 'call.incoming') {\n        // Commands run directly at the top level; \"callName\" names the inbound\n        // (caller) leg and \"events.onHangup\" handles the caller hanging up.\n        $svaml = [\n            'callName' => 'caller',\n            'commands' => [\n                ['command' => 'answer'],\n                [\n                    'command'      => 'messages',\n                    'messagesName' => 'greeting',\n                    'messages' => [\n                        ['type' => 'SAY', 'say' => ['text' => 'Please hold while we connect your call.', 'voiceName' => 'Emma']],\n                    ],\n                ],\n                // Add Party A to a named bridge\n                ['command' => 'bridgeCall', 'bridgeName' => 'main-bridge'],\n                [\n                    'command'  => 'dial',\n                    'callName' => 'callee',\n                    // Party B sees the Sinch number\n                    'from'    => ['type' => 'PHONE', 'phone' => ['number' => $sinchNumber]],\n                    // In production, look up the destination from your DB ($call['to'])\n                    'to'      => ['type' => 'PHONE', 'phone' => ['number' => $destinationNumber]],\n                    'dialTimeoutDurationSeconds' => 30,\n                    'events'  => [\n                        'onAnswer' => [['command' => 'bridgeCall', 'bridgeName' => 'main-bridge']],\n                        'onHangup' => [['command' => 'hangup', 'callName' => 'caller']],\n                    ],\n                ],\n            ],\n            'events' => [\n                'onHangup' => [['command' => 'hangup', 'callName' => 'callee']],\n            ],\n        ];\n\n        $response->getBody()->write(json_encode($svaml));\n        return $response->withHeader('Content-Type', 'application/json')->withStatus(200);\n    }\n\n    error_log(\"Unhandled event: {$event}\");\n    $response->getBody()->write(json_encode(['commands' => []]));\n    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);\n});\n\n$app->run();\n","lang":"php"},"children":[]}]},{"$$mdtype":"Tag","name":"details","attributes":{},"children":[{"$$mdtype":"Tag","name":"summary","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Java (Spring Boot)"]},": save as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Server.java"]}," in a Spring Boot project with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["spring-boot-starter-web"]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mvn spring-boot:run"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"// Server.java: Sinch Number Masking webhook server (Spring Boot).\n// Maven: spring-boot-starter-web\n// Env: SINCH_NUMBER, DESTINATION_NUMBER, PORT (optional, defaults to 3000)\n\npackage com.sinch.tutorials.numbermasking;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\nimport java.util.Map;\n\n@SpringBootApplication\n@RestController\npublic class Server {\n\n    private final String sinchNumber;\n    private final String destinationNumber;\n\n    public Server() {\n        this.sinchNumber       = requireEnv(\"SINCH_NUMBER\");\n        this.destinationNumber = requireEnv(\"DESTINATION_NUMBER\");\n    }\n\n    public static void main(String[] args) {\n        String port = System.getenv().getOrDefault(\"PORT\", \"3000\");\n        System.setProperty(\"server.port\", port);\n        SpringApplication.run(Server.class, args);\n        System.out.println(\"Number masking webhook server listening on port \" + port);\n    }\n\n    /** POST /webhook: receives Sinch call events and responds with SVAML */\n    @PostMapping(\"/webhook\")\n    public ResponseEntity<Map<String, Object>> webhook(@RequestBody Map<String, Object> body) {\n        String event = (String) body.getOrDefault(\"event\", \"\");\n        Object call  = body.getOrDefault(\"call\", Map.of());\n\n        System.out.println(\"Received webhook event: \" + event);\n        System.out.println(\"Call: \" + call);\n\n        if (\"call.incoming\".equals(event)) {\n            // Commands run directly at the top level; \"callName\" names the inbound\n            // (caller) leg and \"events.onHangup\" handles the caller hanging up.\n            Map<String, Object> svaml = Map.of(\n                \"callName\", \"caller\",\n                \"commands\", List.of(\n                    Map.of(\"command\", \"answer\"),\n                    Map.of(\n                        \"command\", \"messages\",\n                        \"messagesName\", \"greeting\",\n                        \"messages\", List.of(\n                            Map.of(\"type\", \"SAY\", \"say\", Map.of(\n                                \"text\", \"Please hold while we connect your call.\",\n                                \"voiceName\", \"Emma\"))\n                        )\n                    ),\n                    // Add Party A to a named bridge\n                    Map.of(\"command\", \"bridgeCall\", \"bridgeName\", \"main-bridge\"),\n                    Map.of(\n                        \"command\", \"dial\",\n                        \"callName\", \"callee\",\n                        // Party B sees the Sinch number, not Party A's real number\n                        \"from\", Map.of(\"type\", \"PHONE\", \"phone\", Map.of(\"number\", sinchNumber)),\n                        // In production, look up from your DB based on the called Sinch number\n                        \"to\",   Map.of(\"type\", \"PHONE\", \"phone\", Map.of(\"number\", destinationNumber)),\n                        \"dialTimeoutDurationSeconds\", 30,\n                        \"events\", Map.of(\n                            \"onAnswer\", List.of(Map.of(\"command\", \"bridgeCall\", \"bridgeName\", \"main-bridge\")),\n                            \"onHangup\", List.of(Map.of(\"command\", \"hangup\", \"callName\", \"caller\"))\n                        )\n                    )\n                ),\n                \"events\", Map.of(\n                    \"onHangup\", List.of(Map.of(\"command\", \"hangup\", \"callName\", \"callee\"))\n                )\n            );\n            return ResponseEntity.ok(svaml);\n        }\n\n        System.out.println(\"Unhandled event: \" + event);\n        return ResponseEntity.ok(Map.of(\"commands\", List.of()));\n    }\n\n    private static String requireEnv(String name) {\n        String value = System.getenv(name);\n        if (value == null || value.isBlank()) {\n            System.err.println(\"ERROR: \" + name + \" is not set.\");\n            System.exit(1);\n        }\n        return value;\n    }\n}\n","lang":"java"},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-expose-it-with-ngrok","__idx":7},"children":["2. Expose it with ngrok"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"ngrok http 3000\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Copy the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://<id>.ngrok-free.app"]}," URL ngrok prints and export it as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CALLBACK_URL"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"export CALLBACK_URL=\"https://<id>.ngrok-free.app\"\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your full webhook URL is that base ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["plus ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/webhook"]}]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-point-the-service-webhook-at-your-server","__idx":8},"children":["3. Point the service webhook at your server"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set the service's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callBehavior.type"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WEBHOOK"]}," via the API (or the Dashboard):"]},{"$$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\"'/webhook\",\n        \"fallbackUrl\": \"'\"$CALLBACK_URL\"'/webhook\"\n      }\n    }\n  }'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fallbackUrl"]}," is optional but recommended. When the primary ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["url"]}," fails, Sinch immediately re-sends ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["that same event"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fallbackUrl"]},". After several consecutive primary failures, Sinch bypasses the primary entirely and sends all requests to the fallback until the primary recovers (retried once every 60 seconds). See the Webhooks ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["Timeouts and failover"]}," section in the API reference for the authoritative algorithm."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can also set this from the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com/voice/services"},"children":["Sinch Dashboard"]}," (Voice → Services → your service → Call behavior). The dashboard is the quickest path for a one-off test."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"4-dial-your-sinch-number","__idx":9},"children":["4. Dial your Sinch number"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]}," from a phone (this is Party A)."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"what-success-looks-like","__idx":10},"children":["What success looks like"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Party A hears ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["\"Please hold while we connect your call,\""]}," then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," (Party B) rings."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Party B's phone shows the Sinch number as the caller ID"]},", not Party A's number."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Party A and Party B are bridged; audio flows both ways."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Party A only ever dialed the Sinch number"]},", so Party A never sees Party B's number either."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["When either party hangs up, the other leg drops."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your server terminal logs the incoming event and the SVAML it returned."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"reference-the-inbound-webhook-flow","__idx":11},"children":["Reference: the inbound webhook flow"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When Party A calls your Sinch number:"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["1. Sinch POSTs a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," event"]}," to your webhook (CloudEvents ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ce-*"]}," headers + JSON body):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"event\": \"call.incoming\",\n  \"call\": {\n    \"callId\":          \"01AN4Z07BY79KA1307SR9X4MV3\",\n    \"sessionId\":       \"01AN4Z07BY79KA1307SR9X4MV2\",\n    \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1PARTY_A_NUMBER\" } },\n    \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1SINCH_NUMBER\" } },\n    \"direction\":       \"INBOUND\",\n    \"originationType\": \"PHONE\",\n    \"callType\":        \"PHONE\",\n    \"callResult\":      \"INITIATED\",\n    \"startTime\":       \"2025-06-01T10:00:00Z\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.from.phone.number"]}," is Party A. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.to.phone.number"]}," is the Sinch number Party A dialed. ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["This is your routing key"]}," (see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#number-mapping-in-production"},"children":["Number mapping in production"]},")."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["2. Your server responds with the masking flow."]}," Commands run ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["directly at the top level"]},"; there is no wrapper command. Top-level ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," names the inbound (caller) leg; top-level ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events.onHangup"]}," fires when Party A hangs up:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"callName\": \"caller\",\n  \"commands\": [\n    { \"command\": \"answer\" },\n    {\n      \"command\": \"messages\",\n      \"messagesName\": \"greeting\",\n      \"messages\": [\n        { \"type\": \"SAY\",\n          \"say\": { \"text\": \"Please hold while we connect your call.\", \"voiceName\": \"Emma\" } }\n      ]\n    },\n    { \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" },\n    {\n      \"command\": \"dial\",\n      \"callName\": \"callee\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1SINCH_NUMBER\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1PARTY_B_NUMBER\" } },\n      \"dialTimeoutDurationSeconds\": 30,\n      \"events\": {\n        \"onAnswer\": [{ \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" }],\n        \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"caller\" }]\n      }\n    }\n  ],\n  \"events\": {\n    \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"callee\" }]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["3."]}," Party B receives a call from the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sinch number"]}," (the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," leg), not Party A's real number."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["4."]}," When Party B answers, the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," joins them to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["main-bridge"]},"; audio flows A ↔ B."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["5."]}," When either party hangs up, the matching ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]}," terminates the other named leg."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Why this masks both parties."]}," Party A only ever dialed the Sinch number, so A never learns B's number. Party B's caller ID is set to the Sinch number via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," on the outbound ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]},", so B never learns A's number. The masking number (CLI) is therefore set ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["per leg via the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," command's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}]},". There is no separate \"CLI\" field; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["is"]}," the presented caller ID."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"how-bridgecall-works","__idx":12},"children":["How ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," works"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Bridges are auto-created by name: the first leg into a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]}," creates the bridge, subsequent legs join it. That's why Party A's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," runs immediately (creating ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["main-bridge"]},") and Party B's runs in its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]}," (joining it)."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"inline-events-suppress-the-per-leg-webhook","__idx":13},"children":["Inline ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," suppress the per-leg webhook"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Because the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," defines an inline ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," block, the platform runs those commands and ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["no"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.hangup"]}," webhook fires for the Party B leg. Omit ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," to receive those webhooks instead; an explicit ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events: {}"]}," suppresses them without running anything."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"number-mapping-in-production","__idx":14},"children":["Number mapping in production"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The example servers always dial ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," from the environment, which is fine for a single-pair demo. A real masking service maps ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["the Sinch number that was called"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.to.phone.number"]},") to a Party B."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Typical schema:"]},{"$$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":"Sinch DID"},"children":["Sinch DID"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Party A"},"children":["Party A"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Party B"},"children":["Party B"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Expires"},"children":["Expires"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+14045001001"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+15551110001"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+15552220001"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["2026-06-01"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+14045001002"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+15551110002"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+15552220002"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["2026-06-15"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," handler:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.to.phone.number"]}," (the Sinch DID dialed)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Look up the row; resolve Party B."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["If no mapping exists or it has expired, reject the call"]}," by returning an empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," array:"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{ \"commands\": [] }\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Robustness gap in the examples (flagged):"]}," the sample servers do ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," implement the lookup or the no-mapping rejection. They unconditionally dial ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," for any ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]},". Add the lookup-and-reject logic above before going to production. (For non-",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," events the servers already return ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"commands\": []}"]},", which is the correct \"take no action\" response.)"]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"trigger-a-fully-outbound-masked-bridge","__idx":15},"children":["Trigger a fully outbound masked bridge"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can also build a masked bridge your platform initiates, dialing ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["both"]}," parties programmatically. This is the right shape for outreach where neither party started the call."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The script below POSTs to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/projects/$PROJECT_ID/calls"]}," a single ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," for Party A; inside that leg's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]}," it bridges Party A and issues a second ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," for Party B. Both dials use the Sinch number as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," and share ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName: masked-bridge"]},", so each party sees only the Sinch number."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Save as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-call.sh"]}," and run with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bash test-call.sh"]},". It reads the same exported variables from ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#setup"},"children":["Setup"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"#!/bin/bash\n# test-call.sh: trigger a programmatic masked bridge call via the API.\n# Dials Party A and Party B and bridges them, masking each other's number.\n# Env: PROJECT_ID, KEY_ID, KEY_SECRET, SINCH_NUMBER, DESTINATION_NUMBER\n#      PARTY_A_NUMBER / PARTY_B_NUMBER (optional overrides)\n\nset -e\n\n: \"${PROJECT_ID:?ERROR: PROJECT_ID is not set.}\"\n: \"${KEY_ID:?ERROR: KEY_ID is not set.}\"\n: \"${KEY_SECRET:?ERROR: KEY_SECRET is not set.}\"\n: \"${SINCH_NUMBER:?ERROR: SINCH_NUMBER is not set.}\"\n: \"${DESTINATION_NUMBER:?ERROR: DESTINATION_NUMBER is not set.}\"\n\n# Default the two legs; override with two real phones for a proper test.\nPARTY_A_NUMBER=\"${PARTY_A_NUMBER:-${DESTINATION_NUMBER}}\"\nPARTY_B_NUMBER=\"${PARTY_B_NUMBER:-${SINCH_NUMBER}}\"\n\nBASE_URL=\"https://voice.api.sinch.com/v2\"\n\necho \"Initiating masked bridge call between ${PARTY_A_NUMBER} and ${PARTY_B_NUMBER} ...\"\necho \"(Both parties will see ${SINCH_NUMBER} as the caller ID)\"\n\nBODY=$(printf '{\n  \"commands\": [\n    {\n      \"command\": \"dial\",\n      \"callName\": \"party-a\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n      \"dialTimeoutDurationSeconds\": 30,\n      \"events\": {\n        \"onAnswer\": [\n          {\n            \"command\": \"messages\",\n            \"messagesName\": \"greeting\",\n            \"messages\": [\n              { \"type\": \"SAY\",\n                \"say\": { \"text\": \"Please hold while we connect the other party.\", \"voiceName\": \"Emma\" } }\n            ]\n          },\n          { \"command\": \"bridgeCall\", \"bridgeName\": \"masked-bridge\" },\n          {\n            \"command\": \"dial\",\n            \"callName\": \"party-b\",\n            \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n            \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"%s\" } },\n            \"dialTimeoutDurationSeconds\": 30,\n            \"events\": {\n              \"onAnswer\": [{ \"command\": \"bridgeCall\", \"bridgeName\": \"masked-bridge\" }],\n              \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"party-a\" }]\n            }\n          }\n        ],\n        \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"party-b\" }]\n      }\n    }\n  ]\n}' \"${SINCH_NUMBER}\" \"${PARTY_A_NUMBER}\" \"${SINCH_NUMBER}\" \"${PARTY_B_NUMBER}\")\n\nRESPONSE=$(curl -s -w \"\\n%{http_code}\" \\\n  -X POST \\\n  -u \"${KEY_ID}:${KEY_SECRET}\" \\\n  \"${BASE_URL}/projects/${PROJECT_ID}/calls\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"${BODY}\")\n\nHTTP_BODY=$(echo \"${RESPONSE}\" | head -n -1)\nHTTP_CODE=$(echo \"${RESPONSE}\" | tail -n 1)\n\nif [ \"${HTTP_CODE}\" -eq 201 ]; then\n  echo \"Bridge call created successfully (HTTP ${HTTP_CODE}):\"\n  echo \"${HTTP_BODY}\" | (command -v jq > /dev/null && jq '.' || cat)\nelse\n  echo \"ERROR: API returned HTTP ${HTTP_CODE}:\" >&2\n  echo \"${HTTP_BODY}\" >&2\n  exit 1\nfi\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Note on the demo defaults:"]}," the script defaults ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PARTY_A_NUMBER"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PARTY_B_NUMBER"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]},", so out of the box one leg dials your Sinch number itself. For a real two-party test, set both explicitly to phones you can answer:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"PARTY_A_NUMBER=+1... PARTY_B_NUMBER=+1... bash test-call.sh\n","lang":"bash"},"children":[]}]},{"$$mdtype":"Tag","name":"details","attributes":{},"children":[{"$$mdtype":"Tag","name":"summary","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Browser JS equivalent"]}," (demonstration only; see the CORS and credentials caveats)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Browsers cannot read exported environment variables and calling the Sinch API directly from a browser hits CORS. Replace the placeholders with values injected by your backend, and in production proxy these calls through your server so API keys never reach the client."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"js","header":{"controls":{"copy":{}}},"source":"// Demonstration only. Do not ship API keys to the browser.\n(async function sinchMaskedBridgeCall() {\n  const projectId    = \"YOUR_PROJECT_ID\";\n  const keyId        = \"YOUR_KEY_ID\";\n  const keySecret    = \"YOUR_KEY_SECRET\";\n  const sinchNumber  = \"+1XXXXXXXXXX\";\n  const partyANumber = \"+1AAAAAAAAAA\";  // First person to call\n  const partyBNumber = \"+1BBBBBBBBBB\";  // Second person to connect\n\n  const baseUrl    = \"https://voice.api.sinch.com/v2\";\n  const authHeader = \"Basic \" + btoa(`${keyId}:${keySecret}`);\n\n  const payload = {\n    commands: [\n      {\n        command: \"dial\",\n        callName: \"party-a\",\n        from: { type: \"PHONE\", phone: { number: sinchNumber } },\n        to:   { type: \"PHONE\", phone: { number: partyANumber } },\n        dialTimeoutDurationSeconds: 30,\n        events: {\n          onAnswer: [\n            {\n              command: \"messages\",\n              messagesName: \"greeting\",\n              messages: [\n                { type: \"SAY\", say: { text: \"Please hold while we connect the other party.\", voiceName: \"Emma\" } }\n              ]\n            },\n            { command: \"bridgeCall\", bridgeName: \"masked-bridge\" },\n            {\n              command: \"dial\",\n              callName: \"party-b\",\n              from: { type: \"PHONE\", phone: { number: sinchNumber } },\n              to:   { type: \"PHONE\", phone: { number: partyBNumber } },\n              dialTimeoutDurationSeconds: 30,\n              events: {\n                onAnswer: [{ command: \"bridgeCall\", bridgeName: \"masked-bridge\" }],\n                onHangup: [{ command: \"hangup\", callName: \"party-a\" }]\n              }\n            }\n          ],\n          onHangup: [{ command: \"hangup\", callName: \"party-b\" }]\n        }\n      }\n    ]\n  };\n\n  const response = await fetch(`${baseUrl}/projects/${projectId}/calls`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\", Authorization: authHeader },\n    body: JSON.stringify(payload)\n  });\n\n  const data = await response.json();\n  if (response.status === 201) {\n    console.log(\"Bridge call created successfully:\", data);\n  } else {\n    console.error(`ERROR ${response.status}:`, data);\n  }\n})();\n","lang":"js"},"children":[]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /calls"]}," body is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callRequest"]},": a top-level ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," array (the same SVAML primitives), optionally with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["parameters"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batchOptions"]}," for batches. A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["201"]}," returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ projectId, serviceId, sessionId }"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"production-readiness-checklist","__idx":16},"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":["Number mapping"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Look up Party B from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.to.phone.number"]},". The example hard-codes ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]},"; replace it."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Mapping expiry"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Disposable masks should expire. Reject calls to expired DIDs by returning an empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," array."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Two-way masking"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Each direction needs a Sinch DID. Allocate one per pair; rotate when the relationship ends."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No-answer fallback"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Add ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBusy"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onReject"]}," handlers on the Party B ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," to leave a voicemail or fall through to ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/call-hunting"},"children":["Call Hunting"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Recording"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Insert ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["startRecording"]}," after the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," if compliance requires an audit log. 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":["Webhook latency"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch enforces a per-webhook response timeout (treat ~5 s as the budget). Cache your mapping in memory and respond fast."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Header / signature validation"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Verify the CloudEvents headers and the request signature before acting. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/inbound-pstn"},"children":["Handle Inbound PSTN Calls"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Idempotent webhook handlers"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A failed primary delivery is re-sent to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fallbackUrl"]},", so the same event can arrive more than once. Deduplicate on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ce-id"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ce-source"]}," header pair."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Carrier caller-ID rules"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["If you ever pass Party A's real caller ID through, confirm your carrier accepts it. Most don't."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"what-the-openapi-spec-says-at-a-glance","__idx":17},"children":["What the OpenAPI spec says: at a glance"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," response (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhookResponse"]},") is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"commands\": [...], \"callName\"?, \"events\"?: { \"onHangup\": [...] } }"]},". Commands run directly; no wrapper command. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," are honored ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["only"]}," in responses to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," requires ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]},"; the bridge is auto-created on first use and joined thereafter."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," requires ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]},"; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]}," are typed endpoints (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["type: PHONE"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["phone.number"]}," in E.164). The presented caller ID is the leg's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},". Lifecycle is handled via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBusy"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onReject"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onFailure"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hangup"]}," accepts a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," to drop a specific named leg while keeping the session and other legs alive."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/projects/{projectId}/calls"]}," takes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callRequest"]}," (top-level ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]},") and returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ projectId, serviceId, sessionId }"]}," on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["201"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH /v2/projects/{projectId}/services/{serviceId}"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["updateService"]},") sets ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callBehavior"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["NONE"]}," | ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WEBHOOK"]}," | ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STATIC"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/projects/{projectId}/svaml/validate"]}," validates a full SVAML payload (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"svaml\": { \"commands\": [...], \"callName\"?: ..., \"events\"?: {...} }, \"validationType\"?: \"NORMAL\" | \"STRICT\" }"]},") and returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"isValid\", \"errors\" }"]},". A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," means validation ran, not that the payload is valid; always read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isValid"]},"."]}]}]},"headings":[{"value":"Mask a Phone Call (Number Masking)","id":"mask-a-phone-call-number-masking","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":"Step 0: Smoke-test the SVAML (no phone needed)","id":"step-0-smoke-test-the-svaml-no-phone-needed","depth":2},{"value":"Quick start: minimal end-to-end","id":"quick-start-minimal-end-to-end","depth":2},{"value":"1. Start the webhook server","id":"1-start-the-webhook-server","depth":3},{"value":"2. Expose it with ngrok","id":"2-expose-it-with-ngrok","depth":3},{"value":"3. Point the service webhook at your server","id":"3-point-the-service-webhook-at-your-server","depth":3},{"value":"4. Dial your Sinch number","id":"4-dial-your-sinch-number","depth":3},{"value":"What success looks like","id":"what-success-looks-like","depth":3},{"value":"Reference: the inbound webhook flow","id":"reference-the-inbound-webhook-flow","depth":2},{"value":"How bridgeCall works","id":"how-bridgecall-works","depth":3},{"value":"Inline events suppress the per-leg webhook","id":"inline-events-suppress-the-per-leg-webhook","depth":3},{"value":"Number mapping in production","id":"number-mapping-in-production","depth":2},{"value":"Trigger a fully outbound masked bridge","id":"trigger-a-fully-outbound-masked-bridge","depth":2},{"value":"Production-readiness checklist","id":"production-readiness-checklist","depth":2},{"value":"What the OpenAPI spec says: at a glance","id":"what-the-openapi-spec-says-at-a-glance","depth":2}],"frontmatter":{"seo":{"title":"Mask a Phone Call (Number Masking)"}},"lastModified":"2026-09-24T14:07:04.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/voice-2.0/tutorials/number-masking","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}