Setting Up Your Cloud Call Centre with Vonage from Scratch
← Day 2: Architecture Deep Dive | Day 4: Vonage Client SDK →
🎯 What You'll Build Today
By the end of this post your local machine will be fully wired to Vonage's platform. You'll have:
- ✅ A Vonage API account with credentials
- ✅ The Vonage CLI installed and authenticated
- ✅ A real virtual phone number provisioned
- ✅ ngrok tunnelling your localhost to the internet
- ✅ A Node.js webhook server that answers a real phone call
- ✅ Your phone ringing and hearing "Hello from your Cloud Call Centre"
This is the foundation everything else in this series is built on. Let's go.
🗺️ What We're Building Today
┌─────────────────────────────────────────────────────────────────┐
│ TODAY'S SETUP GOAL │
│ │
│ Your Phone │
│ (caller) │
│ │ │
│ │ dials virtual number │
│ ▼ │
│ ┌─────────────┐ │
│ │ Vonage │ ── fires webhook ──► ngrok tunnel │
│ │ Platform │ │ │
│ └─────────────┘ │ forwards to │
│ ▼ │
│ ┌──────────────────┐ │
│ │ localhost:3000 │ │
│ │ (your Node.js │ │
│ │ server) │ │
│ └────────┬─────────┘ │
│ │ │
│ │ returns NCCO │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Vonage executes │ │
│ │ "Hello from │ │
│ │ your Cloud │ │
│ │ Call Centre" │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
📋 Prerequisites
Before we start, make sure you have these installed:
| Tool | Version | Check command |
|---|---|---|
| Node.js | 18+ | node --version |
| npm | 9+ | npm --version |
| Git | Any | git --version |
| A terminal | — | — |
| A real phone | — | For testing calls |
STEP 1 — Create Your Vonage API Account
1.1 Sign Up
Head to dashboard.nexmo.com/sign-up and create a free account. You'll receive:
- €2 free credit to make test calls
- A test virtual number (in some regions)
- Full API access immediately — no credit card required for trial
1.2 Find Your API Credentials
Once logged in, your API credentials are on the dashboard home page:
┌──────────────────────────────────────────────────────┐
│ VONAGE API DASHBOARD │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ API key: a1b2c3d4 │ │
│ │ API secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ These are your master credentials. │
│ NEVER commit these to git. │
│ Store them in a .env file (shown below). │
└──────────────────────────────────────────────────────┘
⚠️ Security rule from Day 1: API keys go in
.envfiles only. Add.envto your.gitignorebefore your first commit. We'll cover secrets management properly in Week 6.
STEP 2 — Install the Vonage CLI
The Vonage CLI is your command-line control panel for managing numbers, applications, and credentials.
2.1 Install
npm install -g @vonage/cli
2.2 Verify installation
vonage --version
# Expected output: @vonage/cli/x.x.x ...
2.3 Authenticate with your credentials
vonage config:set --apiKey=YOUR_API_KEY --apiSecret=YOUR_API_SECRET
You'll see:
✔ API key and secret saved to ~/.vonage/credentials
2.4 Verify authentication works
vonage balance
# Expected output:
# Current balance: 2.00000 EUR
If you see your balance, you're authenticated. ✅
STEP 3 — Create a Vonage Application
A Vonage Application is a configuration object that links your phone numbers and webhook URLs together. Think of it as the "container" for your call centre.
┌──────────────────────────────────────────────────────────────┐
│ VONAGE APPLICATION │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Application ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx│ │
│ │ Name: "my-cloud-call-centre" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ WEBHOOK │ │ PRIVATE │ │ LINKED │ │
│ │ URLS │ │ KEY │ │ NUMBERS │ │
│ │ │ │ │ │ │ │
│ │ answer │ │ RSA key │ │ +44 xxx │ │
│ │ event │ │ for JWT │ │ +1 xxx │ │
│ │ fallback │ │ auth │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
3.1 Create the application via CLI
vonage apps:create \
--name="my-cloud-call-centre" \
--voice_answer_url="https://placeholder.ngrok.io/webhooks/answer" \
--voice_event_url="https://placeholder.ngrok.io/webhooks/event" \
--voice_fallback_url="https://placeholder.ngrok.io/webhooks/fallback"
You'll see output like:
Application created: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Private key saved to: private.key
📝 Save your Application ID. You'll need it in your
.envfile.
The CLI automatically generates an RSA key pair and saves private.key to your current directory. This key is used to generate JWTs for authenticating API calls. Never commit this file to git.
3.2 List your applications to confirm
vonage apps:list
# Output:
# ID Name
# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx my-cloud-call-centre
STEP 4 — Buy a Virtual Phone Number
4.1 Search for available numbers
# Search for UK numbers
vonage numbers:search GB
# Search for US numbers
vonage numbers:search US
# Search for a specific area code (US example)
vonage numbers:search US --pattern=212
You'll see output like:
Country MSisdn Type Monthly Cost
GB 447700900000 mobile 1.25 EUR
GB 447700900001 mobile 1.25 EUR
GB 441212345678 landline 0.50 EUR
4.2 Buy a number
vonage numbers:buy 447700900000 GB
# Output: Number 447700900000 successfully purchased
💡 Trial accounts: On a free trial account, Vonage gives you a test number and limits calls to verified numbers only. To call any number, add a payment method to upgrade.
4.3 Link the number to your application
vonage apps:link --number=447700900000 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Output: Number 447700900000 linked to application xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
The flow now looks like this:
Someone dials 447700900000
│
▼
Vonage looks up: which application owns this number?
│
▼
Application: my-cloud-call-centre
│
▼
Fire webhook to: answer_url
(currently pointing to placeholder — we'll fix this next)
STEP 5 — Set Up ngrok
ngrok creates a secure tunnel from a public URL to your local machine. Vonage needs a public HTTPS URL to send webhooks — ngrok provides that during development.
┌──────────────────────────────────────────────────────────────┐
│ HOW NGROK WORKS │
│ │
│ Vonage ngrok servers Your machine │
│ Platform │
│ │ │ │
│ │ POST https:// │ │
│ │ abc123.ngrok.io ──────────────────────► │ │
│ │ /webhooks/answer localhost │
│ │ :3000 │
│ │ │ │
│ │ ◄──────────────────────────────── 200 OK │
│ │ + NCCO JSON │
└──────────────────────────────────────────────────────────────┘
5.1 Install ngrok
# macOS
brew install ngrok
# Windows / Linux — download from https://ngrok.com/download
# or via npm:
npm install -g ngrok
5.2 Sign up for a free ngrok account
Go to ngrok.com and sign up. Get your auth token from the dashboard.
ngrok config add-authtoken YOUR_NGROK_TOKEN
5.3 Start the tunnel
We'll start our Node.js server on port 3000, so:
ngrok http 3000
You'll see:
ngrok
Session Status online
Account your@email.com
Version 3.x.x
Region Europe (eu)
Latency -
Web Interface http://127.0.0.1:4040
Forwarding https://abc123.ngrok.io -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy your ngrok URL — e.g. https://abc123.ngrok.io. You'll use this in your .env and Vonage application config.
⚠️ Free ngrok URLs change every restart. Each time you restart ngrok on the free plan, you get a new URL and must update Vonage. We'll cover using a stable domain in Day 29 (CI/CD). For now, just update the URL when needed.
STEP 6 — Update Your Vonage Application Webhook URLs
Now that you have a real ngrok URL, update your Vonage application:
vonage apps:update xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--voice_answer_url="https://abc123.ngrok.io/webhooks/answer" \
--voice_event_url="https://abc123.ngrok.io/webhooks/event" \
--voice_fallback_url="https://abc123.ngrok.io/webhooks/fallback"
Confirm the update:
vonage apps:show xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
STEP 7 — Build Your First Webhook Server
Now we write code. Let's build the Node.js server that answers calls.
7.1 Create the project
mkdir cloud-call-centre
cd cloud-call-centre
npm init -y
npm install express @vonage/server-sdk dotenv
7.2 Create your .env file
touch .env
Add your credentials:
# .env
VONAGE_API_KEY=your_api_key
VONAGE_API_SECRET=your_api_secret
VONAGE_APPLICATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
VONAGE_PRIVATE_KEY_PATH=./private.key
VONAGE_NUMBER=447700900000
PORT=3000
7.3 Create .gitignore
touch .gitignore
# .gitignore
.env
private.key
node_modules/
✅ Always create
.gitignorebefore your firstgit commit.
7.4 Create the server
// server.js
import 'dotenv/config';
import express from 'express';
const app = express();
app.use(express.json());
// ──────────────────────────────────────────────
// ANSWER WEBHOOK
// Vonage calls this when someone dials your number
// You must return an NCCO array
// ──────────────────────────────────────────────
app.get('/webhooks/answer', (req, res) => {
console.log('📞 Inbound call received:', {
from: req.query.from,
to: req.query.to,
uuid: req.query.uuid,
timestamp: new Date().toISOString()
});
// NCCO — the script for this call
const ncco = [
{
action: 'talk',
text: 'Hello! Welcome to your Cloud Call Centre, powered by Vonage. Your setup is working perfectly. Well done!',
language: 'en-GB',
style: 1 // Natural voice style
},
{
action: 'talk',
text: 'This is Day 3 of the Building Cloud Call Centres series. Stay tuned for more.',
language: 'en-GB',
style: 1
}
];
res.json(ncco);
});
// ──────────────────────────────────────────────
// EVENT WEBHOOK
// Vonage sends call status updates here
// Always return 200 OK
// ──────────────────────────────────────────────
app.post('/webhooks/event', (req, res) => {
const event = req.body;
console.log(`📊 Call event [${event.status}]:`, {
uuid: event.uuid,
status: event.status,
direction: event.direction,
from: event.from,
to: event.to,
timestamp: event.timestamp
});
// Log specific events we care about
switch (event.status) {
case 'started':
console.log(' → Call started, waiting for answer');
break;
case 'ringing':
console.log(' → Phone is ringing');
break;
case 'answered':
console.log(' → Call answered!');
break;
case 'completed':
console.log(` → Call completed. Duration: ${event.duration}s`);
break;
case 'failed':
console.error(' → Call failed:', event.reason);
break;
}
res.status(200).end();
});
// ──────────────────────────────────────────────
// FALLBACK WEBHOOK
// Vonage calls this if the answer webhook fails
// Keep this simple and reliable
// ──────────────────────────────────────────────
app.get('/webhooks/fallback', (req, res) => {
console.error('⚠️ Fallback webhook triggered — answer webhook failed');
const ncco = [
{
action: 'talk',
text: 'We are sorry, we are experiencing technical difficulties. Please call back shortly.',
language: 'en-GB'
}
];
res.json(ncco);
});
// ──────────────────────────────────────────────
// HEALTH CHECK
// Useful for monitoring and load balancers
// ──────────────────────────────────────────────
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'cloud-call-centre',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// ──────────────────────────────────────────────
// START SERVER
// ──────────────────────────────────────────────
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`
╔══════════════════════════════════════════════╗
║ Cloud Call Centre — Day 3 Setup ║
╠══════════════════════════════════════════════╣
║ Server running on port ${PORT} ║
║ ║
║ Webhook endpoints: ║
║ GET /webhooks/answer ← inbound calls ║
║ POST /webhooks/event ← call events ║
║ GET /webhooks/fallback ← error fallback ║
║ GET /health ← health check ║
╚══════════════════════════════════════════════╝
`);
});
7.5 Update package.json to use ES modules
{
"name": "cloud-call-centre",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"@vonage/server-sdk": "^3.x.x",
"dotenv": "^16.x.x",
"express": "^4.x.x"
}
}
STEP 8 — Test Everything End-to-End
8.1 Start your server
In terminal 1:
node server.js
You should see:
╔══════════════════════════════════════════════╗
║ Cloud Call Centre — Day 3 Setup ║
╠══════════════════════════════════════════════╣
║ Server running on port 3000 ║
...
8.2 Start ngrok
In terminal 2:
ngrok http 3000
8.3 Test the health endpoint
In terminal 3:
curl https://abc123.ngrok.io/health
# Output:
# {"status":"ok","service":"cloud-call-centre","timestamp":"...","uptime":12.3}
8.4 Test the answer webhook manually
Before calling, verify the NCCO response looks correct:
curl "https://abc123.ngrok.io/webhooks/answer?from=447700900001&to=447700900000&uuid=test-uuid-123"
Expected output:
[
{
"action": "talk",
"text": "Hello! Welcome to your Cloud Call Centre, powered by Vonage. Your setup is working perfectly. Well done!",
"language": "en-GB",
"style": 1
},
{
"action": "talk",
"text": "This is Day 3 of the Building Cloud Call Centres series. Stay tuned for more.",
"language": "en-GB",
"style": 1
}
]
8.5 Make the real call
Call your Vonage virtual number from your phone.
Watch your server terminal — you should see:
📞 Inbound call received: {
from: '+447700900001',
to: '+447700900000',
uuid: '63f61863-4a51-4f6b-86e1-46edebio0391',
timestamp: '2024-01-15T09:00:00.000Z'
}
📊 Call event [started]: { uuid: '63f...', status: 'started', ... }
📊 Call event [ringing]: { uuid: '63f...', status: 'ringing', ... }
📊 Call event [answered]: { uuid: '63f...', status: 'answered', ... }
→ Call answered!
📊 Call event [completed]: { uuid: '63f...', status: 'completed', ... }
→ Call completed. Duration: 18s
And on your phone you'll hear the welcome message spoken aloud. 🎉
STEP 9 — Understanding What Just Happened
Let's trace the exact sequence of what occurred:
YOUR PHONE VONAGE YOUR SERVER (localhost)
│ │ │
│ dials virtual number │ │
│───────────────────────►│ │
│ │ │
│ │ GET /webhooks/answer │
│ │ ?from=...&to=... │
│ │──────────────────────────►│
│ │ │
│ │ │ Build NCCO
│ │ │ based on query params
│ │ │
│ │ 200 OK + NCCO JSON │
│ │◄──────────────────────────│
│ │ │
│ │ Executes NCCO: │
│ │ • Converts text to speech│
│ │ • Streams audio to caller│
│ │ │
│ hears: "Hello! │ │
│ Welcome to your..." │ │
│◄───────────────────────│ │
│ │ │
│ call ends │ │
│───────────────────────►│ │
│ │ │
│ │ POST /webhooks/event │
│ │ {status: "completed"} │
│ │──────────────────────────►│
│ │ │
│ │ 200 OK │
│ │◄──────────────────────────│
STEP 10 — Project Structure & Next Steps
Your project should now look like this:
cloud-call-centre/
│
├── .env ← credentials (NEVER commit)
├── .gitignore ← excludes .env and private.key
├── private.key ← RSA key (NEVER commit)
├── package.json
├── server.js ← main webhook server
│
└── (coming in future days)
├── routes/
│ ├── voice.js ← Day 6: Voice routing
│ ├── sms.js ← Day 7: SMS channel
│ └── chat.js ← Day 8: Web chat
├── ncco/
│ └── builder.js ← Day 11: IVR builder
├── agents/
│ └── router.js ← Day 5: Skills routing
└── analytics/
└── cdr.js ← Day 21: CDR processing
We'll grow this folder structure day by day throughout the series.
🛠️ Troubleshooting Common Issues
ISSUE: ngrok URL not reachable
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Symptom: curl to ngrok URL times out
Fix: Check your server is running on the correct port
Check ngrok is pointing to the same port
Try: curl http://localhost:3000/health first
─────────────────────────────────────────────────────────
ISSUE: Vonage webhook not firing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Symptom: Phone rings but server logs nothing
Fix 1: Check your application's answer_url is updated
to the current ngrok URL
vonage apps:show YOUR_APP_ID
Fix 2: Check the number is linked to the application
vonage numbers:list
─────────────────────────────────────────────────────────
ISSUE: "Trial account" error on outbound
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Symptom: "Non-Vonage number" error for inbound from
unverified numbers on trial account
Fix: Add your personal number to the verified list
at dashboard.vonage.com → Test numbers
─────────────────────────────────────────────────────────
ISSUE: NCCO not executing (silent call)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Symptom: Call connects but no audio plays
Fix 1: Check your NCCO JSON is valid
(no trailing commas, proper array format)
Fix 2: Check Content-Type is application/json
(Express json() middleware handles this)
Fix 3: Check the answer webhook returns 200
not 201 or 204
─────────────────────────────────────────────────────────
ISSUE: private.key not found error
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Symptom: SDK throws ENOENT error on private.key
Fix: Make sure private.key is in your project root
The CLI saves it to wherever you ran
vonage apps:create
Copy it to your project directory
📦 Making an Outbound Call (Bonus)
Want to make Vonage call YOU instead of the other way around? Here's a quick bonus snippet:
// make-call.js — run with: node make-call.js
import Vonage from '@vonage/server-sdk';
import 'dotenv/config';
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH
});
const response = await vonage.voice.createOutboundCall({
to: [{
type: 'phone',
number: 'YOUR_PERSONAL_PHONE_NUMBER' // e.g. 447700900001
}],
from: {
type: 'phone',
number: process.env.VONAGE_NUMBER
},
ncco: [
{
action: 'talk',
text: 'Hello! This is an outbound call from your Cloud Call Centre. It works!'
}
]
});
console.log('Outbound call created:', response);
Run it:
node make-call.js
Your phone will ring within seconds. When you answer, you'll hear the message. This is how automated outbound notifications, call-backs, and proactive outreach work — we'll use this extensively in later days.
✅ Day 3 Checklist
Before moving to Day 4, confirm:
□ Vonage API account created
□ vonage CLI installed and authenticated
□ vonage balance shows credit
□ Vonage Application created (note the App ID)
□ private.key saved and in .gitignore
□ Virtual phone number purchased
□ Number linked to application
□ ngrok installed and tunnel running
□ Vonage application webhook URLs updated to ngrok URL
□ Node.js server running on port 3000
□ GET /health returns 200 OK
□ GET /webhooks/answer returns valid NCCO JSON
□ Real phone call received and voice played ← THE MAGIC MOMENT
□ Server logs show call events (started, ringing, answered, completed)
□ .env and private.key are in .gitignore
📁 Full File Reference
Here's every file from today in one place for reference:
.env
VONAGE_API_KEY=your_api_key
VONAGE_API_SECRET=your_api_secret
VONAGE_APPLICATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
VONAGE_PRIVATE_KEY_PATH=./private.key
VONAGE_NUMBER=447700900000
PORT=3000
.gitignore
.env
private.key
node_modules/
package.json
{
"name": "cloud-call-centre",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"@vonage/server-sdk": "^3.x.x",
"dotenv": "^16.x.x",
"express": "^4.x.x"
}
}
🚀 What's Next
In Day 4 we go deeper on the Vonage Client SDK — building a browser-based softphone so your agents can answer calls directly in a web page. No phone hardware required.
You'll build a React component that:
- Logs an agent in to Vonage
- Listens for inbound calls
- Answers / hangs up via browser UI
- Shows the caller's number
This is the foundation of your agent desktop.
💬 Discussion
- Did you get your first call working? Drop a 📞 in the comments!
- What language are you using for your backend — Node.js or Python?
- Any setup issues not covered in the troubleshooting section?
Series: Building Cloud Call Centres with Vonage APIs
← Day 2: Architecture Deep Dive | Day 3 of 30 | Day 4: Vonage Client SDK →
Top comments (0)