DEV Community

Cover image for Build, Operate, and Automate Sinch APIs with the Sinch CLI
Gunnar Grosch
Gunnar Grosch

Posted on Originally published at sinch.com

Build, Operate, and Automate Sinch APIs with the Sinch CLI

The Sinch CLI (@sinch/cli) puts the Sinch platform in one terminal command: sinch. You can get a phone number, route calls to a Voice API v2 service, build and deploy a Sinch Function, send a message over the Sinch Conversation API, and then script the same steps in CI with --json output and predictable exit codes.

The same commands work for coding agents. Claude Code, Kiro, Cursor, and GitHub Copilot all work through a terminal, so they can run sinch just like you do. They use --help to find commands, --json to read structured output, and exit codes to decide what to do next, without assembling raw API requests. Ask your agent to rent a number and point it at your function, and it can do that with the commands in this post.

This post walks one path from start to finish. You'll get a number, create a Voice service, scaffold a voice function, test it locally, deploy it, and place a call that speaks to you. After that comes messaging, the rest of the products the CLI covers, automation, and how to give your coding agent Sinch-specific skills.

You'll need

  • Node.js 24 or newer
  • A Sinch account and a project in the Sinch Build Dashboard
  • A project ID, access key ID, and access key secret from the project's Access Keys page
  • A Conversation App with at least one channel configured, for the messaging section

Use a development project for this walkthrough. The steps below repoint a Voice service's webhook, and you don't want that happening to a service that takes real customer calls.

Install and authenticate

npm install -g @sinch/cli
sinch auth login
Enter fullscreen mode Exit fullscreen mode

auth login asks for your project ID, access key ID, and access key secret, then verifies the key pair. That one login covers Numbers, Voice API v2, Functions, the Sinch Conversation API, fax, SIP trunking, and porting.

Credentials go into your OS keychain: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Short-lived session tokens go in ~/.sinch/. Check what's stored:

sinch auth status
Enter fullscreen mode Exit fullscreen mode

Get a phone number

Search for a number with voice capability, rent it, and confirm it's active:

sinch numbers available search --region US --type LOCAL --capabilities SMS VOICE
sinch numbers available rent +12025550134
sinch numbers active list
Enter fullscreen mode Exit fullscreen mode

active list shows each number with its capabilities:

+1 202-555-0134  [US] LOCAL  SMS, VOICE
Enter fullscreen mode Exit fullscreen mode

Some regions need supporting documents, such as proof of address or business identity, before a number can go live. Search results tag those numbers [docs required]. If a rental needs documents the CLI switches to a guided KYC order and runs that region's questionnaire, which it fetches from the API at run time. You can also start an order directly:

sinch numbers order create --region DE --type LOCAL
Enter fullscreen mode Exit fullscreen mode

Create a Voice service

Voice API v2 routes inbound calls through a service. A service holds one setting that matters here: The webhook URL that receives call events. List what your project already has:

sinch voice services list
Enter fullscreen mode Exit fullscreen mode

The list marks one service as the project default. Calls you place with sinch voice calls create run through the default service, so for this walkthrough use that one. If the project has no services yet, create one:

sinch voice services create --name "Support line"
Enter fullscreen mode Exit fullscreen mode

The CLI prints the new service ID, a sinch voice services update command for setting the webhook, and the VOICE_SERVICE_ID=<id> line that routes a function to the service. You don't need to set a webhook yourself: The function steps below do it for you.

Build a voice function

Sinch Functions run your call-handling code on Sinch. The CLI covers the whole loop: Scaffold, test locally, deploy, and watch the logs.

sinch functions init simple-voice-ivr --name my-function
cd my-function
Enter fullscreen mode Exit fullscreen mode

init uses the Node.js runtime for this template (add --runtime csharp for C#) and asks four questions:

  • Company name. This goes into the voice prompts.
  • Voice service. This is where the function receives calls. Pick the default service from the previous step.
  • Auto-configure voice integration on deploy. Say yes. This is what lets deploy point the service at your function.
  • Database. Pick No database for this example.

Then it installs dependencies and prints the next steps. Run sinch templates list to see the rest of the catalog: IVRs, call bridging, number masking, SMS and RCS responders, and realtime AI voice agents.

Make it speak on an outbound call

The template's voiceWebhook handles inbound calls with an incoming handler that plays a menu. For outbound calls, add an answered handler to the same onCall in function.ts:

export const voiceWebhook = onCall({
  // incoming, manage, and completed from the template stay as they are
  answered: (_call, builder) =>
    builder.say('Your package is arriving today.').hangup(),
});
Enter fullscreen mode Exit fullscreen mode

When a call you place is answered, Voice API v2 sends a call.answered event to the service webhook. The function replies with the commands to run: Say the message, then hang up. That's why calls create has no --say flag. The function decides what happens on the call.

Test it locally

sinch functions dev
Enter fullscreen mode Exit fullscreen mode

dev builds the function, runs it on localhost:3000, and reloads on every save. Send it the event Voice API v2 would send and look at the commands it returns:

curl -X POST http://localhost:3000/ \
  -H 'Content-Type: application/json' \
  -d '{"event":"call.answered","callId":"01TEST","sessionId":"01TEST"}'
Enter fullscreen mode Exit fullscreen mode
{
  "commands": [
    {
      "command": "messages",
      "messages": [
        { "type": "SAY", "say": { "text": "Your package is arriving today.", "voiceName": "Emma" } }
      ],
      "events": {
        "onFinish": [{ "command": "hangup" }],
        "onFailure": [{ "command": "hangup" }]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

dev --tunnel goes one step further. It opens a Sinch tunnel and points your Voice service's webhook at your laptop, so real calls reach the local server before you deploy anything. The service keeps pointing at the tunnel after you stop dev, so run deploy afterwards to move it back to the function.

Deploy it

Stop dev with Ctrl+C and deploy:

sinch functions deploy
Enter fullscreen mode Exit fullscreen mode

deploy packages the source, audits the npm dependencies, and rolls it out. Because auto-configuration is on, it also points the Voice service webhook at the deployed function's URL. Check it with sinch voice services list.

Place the call

To see the request before it costs anything, add --dry-run. It prints the exact request body without placing the call:

sinch voice calls create +15551234567 --from +12025550134 --dry-run
Enter fullscreen mode Exit fullscreen mode
{
  "commands": [
    {
      "command": "dial",
      "callName": "origin",
      "from": { "type": "PHONE", "phone": { "number": "+12025550134" } },
      "to": { "type": "PHONE", "phone": { "number": "+15551234567" } }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

bridge, hangup, and patch take --dry-run, too. SIP destinations take the scheme, such as sip:agent@pbx.example.com, and the dry run shows how they're sent. Drop the flag to place the call:

sinch voice calls create +15551234567 --from +12025550134
Enter fullscreen mode Exit fullscreen mode

Use your own phone as the destination. --from is the voice-capable number from earlier. Answer and you'll hear "Your package is arriving today."

create prints a session ID, the service that handled the call, and the command to look the call up:

Session ID: 01M3MWAGB1JPKE7VVBVN36BYG1
Service:    YOUR_SERVICE_ID

Inspect with: sinch voice calls list --service-id YOUR_SERVICE_ID
Enter fullscreen mode Exit fullscreen mode

The session ID is not a call ID. To inspect a single call, list the service's calls and use the ULID call ID from that list:

sinch voice calls list --service-id YOUR_SERVICE_ID
sinch voice calls get YOUR_CALL_ID
Enter fullscreen mode Exit fullscreen mode
Call ID                     Dir  Type   From          To            Result     Duration
---------------------------------------------------------------------------------------
01M3MWAGB6HH85Y9GMT8V79D57  out  PHONE  +12025550134  +15551234567  COMPLETED  4s
Enter fullscreen mode Exit fullscreen mode

calls list filters with --call-result, --call-type, --from, --to, --start-time, and --end-time, all optional and combinable. sinch voice calls also has bridge to connect two parties, hangup to end legs of a live call, and patch to add a party to a call that's already up.

Watch the logs

sinch functions logs --follow
Enter fullscreen mode Exit fullscreen mode

--follow streams each request the function handles, with its response and any console.log output. The call you just placed shows up as two events:

Time        Method   Status  Duration    URL
────────────────────────────────────────────────────────────────────────────────
22:47:02  POST    204  1ms        /webhook/voice [call.hangup]
22:46:59  POST    200  1ms        /webhook/voice [call.answered]
Enter fullscreen mode Exit fullscreen mode

For an interactive view where the arrow keys move between requests and Enter expands one, use sinch functions logs --interactive. Filter with --level, --search, or --since.

The rest of the lifecycle: functions list, functions status, functions download to pull the deployed source back down, functions delete, and functions docs to generate a README from the source.

Secrets

Secrets live in your OS keychain and go up with the deployment, so the value never sits in your project. Run this from the function directory:

sinch secrets add CRM_API_KEY YOUR_SECRET_VALUE
Enter fullscreen mode Exit fullscreen mode

The CLI stores the value in the keychain and adds an empty CRM_API_KEY= line to the function's .env. That empty line tells dev and deploy to fill the value in from the keychain. Your code reads it as process.env.CRM_API_KEY, locally and in production.

For a longer build on this same template, Build a Voice IVR with Sinch Functions walks through the menu logic and a live phone call.

Stream call audio or connect the Voice Relay

calls create can also bridge the call to a WebSocket endpoint instead of a function:

sinch voice calls create +15551234567 --stream wss://example.com/audio
sinch voice calls create +15551234567 --relay wss://example.com/relay --tts-voice Emma
Enter fullscreen mode Exit fullscreen mode

--stream sends the raw call audio to your endpoint. --relay connects the Voice Relay, where Sinch runs the speech-to-text and text-to-speech and your endpoint only exchanges text. Both need a public wss:// endpoint. Add --dry-run to see the full command list either one sends. To build on them, start from the voice-relay-agent, voice-relay-echo, or realtime-* templates in sinch templates list.

Send an SMS with the Sinch Conversation API

sinch conversation send +15551234567 "Hello from Sinch" --channel SMS
Enter fullscreen mode Exit fullscreen mode

send goes through your Conversation App and supports eight message types with --type: text, media, template, card, carousel, choice, location, and list. If the app doesn't have the channel you pass, the CLI lists the channels it does have. The rest of the Sinch Conversation API is in the same group: messages, contacts, conversations, apps, webhooks, and templates.

More in the CLI

  • Fax. sinch fax send --to +12025550134 --file ./document.pdf sends a fax, and list, get, status --wait, and download track it. Fax services, cover pages, and fax-to-email have their own subcommands.
  • SIP trunking. sinch sip manages trunks, endpoints, acls, credential-lists, countries, and calls.
  • Porting. sinch porting check +12025550134 tells you whether a number can move to Sinch. orders, documents, and activation handle the port-in.
  • Voice API v1. Projects built on a Voice application use sinch voice v1: applications, callouts, calls, and conferences. These commands authenticate with the Voice application key and secret. Set SINCH_APPLICATION_KEY and SINCH_APPLICATION_SECRET.

Automate it

Most commands take --json and print a single JSON document with no table and no spinner, so the output pipes straight into jq:

sinch functions list --json | jq -r '.functions[] | select(.status != "Running") | .name'
sinch functions deploy --non-interactive --json | jq -r '.url'
Enter fullscreen mode Exit fullscreen mode

In a pipeline, authenticate with environment variables instead of the keychain. They take precedence over stored credentials:

export SINCH_PROJECT_ID=YOUR_PROJECT_ID
export SINCH_KEY_ID=YOUR_KEY_ID
export SINCH_KEY_SECRET=YOUR_KEY_SECRET
sinch functions deploy --non-interactive
Enter fullscreen mode Exit fullscreen mode

--non-interactive skips every prompt and uses defaults, and setting CI=true has the same effect on deploy. A non-interactive deploy is public unless you pass --private.

Exit codes follow BSD sysexits conventions, plus a Sinch range starting at 100. Two are worth branching on in a script, because retrying won't help with either:

Exit code Meaning
0 Success
1 General failure
100 Authentication required: No credentials found
101 Authentication failed: Credentials were rejected

Profiles let one install work across several projects. Create a profile, switch to it, and log in:

sinch config profile create staging
sinch config profile use staging
sinch auth login
Enter fullscreen mode Exit fullscreen mode

Or leave the active profile alone and pass --profile staging on a single command.

Updates are opt-in. Turn them on with sinch config set autoUpdate true, or check on demand with sinch upgrade. Install shell completions with sinch completion --install.

Use the Sinch CLI with coding agents

Every command in this post works the same when an agent runs it. These are the parts an agent relies on:

  • --help on every command and group, to discover what exists
  • --json, to read results as structured data
  • Exit codes, to tell an auth problem from a failed call
  • --non-interactive, to run without prompts
  • --dry-run on call commands, to check a request before it costs anything

To give your assistant Sinch-specific knowledge, install Sinch's developer skills:

sinch skills install
Enter fullscreen mode Exit fullscreen mode

This installs skills from github.com/sinch/skills into your coding assistants. It includes a sinch-cli skill that teaches the commands in this post, plus skills for Functions, Voice API v2, the Sinch Conversation API, Numbers, and the other Sinch APIs. After that you can describe what you need ("place a test call to my phone and show me the call log") and let the agent run the commands.

Troubleshooting

  • calls get returns 404 for the ID from create. create returns a session ID. calls get takes the ULID call ID from sinch voice calls list --service-id YOUR_SERVICE_ID.
  • Inbound calls stopped reaching your function after a dev session. The Voice service webhook still points at the tunnel. Run sinch functions deploy, or set it by hand with sinch voice services update YOUR_SERVICE_ID --webhook-url YOUR_FUNCTION_URL. Check where it points with sinch voice services list.
  • A secret is empty in production. Check that .env has the empty CRM_API_KEY= line and that the key shows up in sinch secrets list, then deploy again. sinch secrets add only writes the .env line when you run it from the function directory.
  • A SIP destination is rejected. Voice API v2 wants the scheme: sip:agent@pbx.example.com or sips: for TLS. Run the command with --dry-run to see what's sent.
  • A sinch voice v1 command asks for credentials. v1 uses the Voice application key and secret, not the project access key. Set SINCH_APPLICATION_KEY and SINCH_APPLICATION_SECRET.
  • A script exits with 100 or 101. 100 means no credentials were found and 101 means they were rejected. Run sinch auth status, or check the SINCH_KEY_ID and SINCH_KEY_SECRET values in your pipeline.

Command reference

Task Command
Install npm install -g @sinch/cli
Log in sinch auth login
Find a number sinch numbers available search --region US --type LOCAL --capabilities VOICE
Rent a number sinch numbers available rent +12025550134
List your numbers sinch numbers active list
List Voice services sinch voice services list
Create a Voice service sinch voice services create --name "Support line"
Point a service at a URL sinch voice services update YOUR_SERVICE_ID --webhook-url YOUR_URL
Scaffold a function sinch functions init simple-voice-ivr --name my-function
Run locally sinch functions dev
Route real calls to your laptop sinch functions dev --tunnel
Deploy sinch functions deploy
Stream logs sinch functions logs --follow
Add a secret sinch secrets add CRM_API_KEY YOUR_SECRET_VALUE
Place a call sinch voice calls create +15551234567 --from +12025550134
Preview a call request sinch voice calls create +15551234567 --dry-run
List calls sinch voice calls list --service-id YOUR_SERVICE_ID
Send an SMS sinch conversation send +15551234567 "Hello" --channel SMS
Send a fax sinch fax send --to +12025550134 --file ./document.pdf
Check portability sinch porting check +12025550134
Switch projects sinch config profile use staging
Install agent skills sinch skills install

Where to go next

You now have a number, a Voice service, a function that answers and places calls, and the commands to script all of it. Build a Voice IVR with Sinch Functions goes deeper on the function side. An upcoming post builds an AI voice agent on --stream and --relay.

Use the dashboard for configuration you set once. Use the CLI for everything you repeat, debug, or run in a pipeline.

Additional resources

What's the first thing you'd script with the Sinch CLI? Let us know in the comments.

Top comments (1)

Collapse
 
contentclips_st profile image
ContentClips •

The --help / --json / exit-codes trio is the right agent contract - what usually breaks it after launch is drift: the README shows flags the binary dropped, --help lags a new subcommand. What holds long-term: treat the CLI's doc surface as a tested artifact. Golden-file tests in CI that execute every example from the docs against a dry-run profile mean a flag change and a docs change must land in the same PR or CI stays red. The KYC piece is the same principle on the server side: fetching the questionnaire from the API at runtime is right - server state owns the truth, the binary just renders it. Agents mostly die on ambiguity, not on missing commands: keeping 'auth expired' and 'docs required' as distinct exit codes is what lets them recover without reading prose.