JavaScript Examples
Corrected 2026-08-24 — the base URL, error shape, and one event name below were wrong in the previous version of this file. Corrected again 2026-08-25 — the webhook signature check hashed the wrong bytes and would have rejected every real delivery (see ../webhooks.md). See ../changelog.md. There is no official VoiceMatrix npm SDK yet (see ../sdks-and-openapi.md); this wraps axios directly against the real REST surface.
Installation
npm install axiosSetup
import axios from 'axios';
const voicematrix = axios.create({
baseURL: 'https://api.voicematrix.ai/api/v1/ext',
headers: {
'X-API-Key': process.env.VOICEMATRIX_API_KEY,
'Content-Type': 'application/json'
}
});List Calls
async function listCalls(options = {}) {
const { limit = 20, offset = 0, status, direction } = options;
const params = new URLSearchParams({ limit, offset });
if (status) params.append('status', status);
if (direction) params.append('direction', direction);
const response = await voicematrix.get(`/calls?${params}`);
return response.data;
}
// Usage
const calls = await listCalls({ limit: 10, status: 'completed' });
console.log(`Found ${calls.pagination.total} calls`);Make Outbound Call
async function makeCall(to, agentId, metadata = {}) {
const response = await voicematrix.post('/calls', {
to,
agent_id: agentId,
metadata
});
return response.data;
}
// Usage
const call = await makeCall('+972501234567', 'agent-uuid', {
campaign: 'summer_sale',
source: 'crm'
});
console.log(`Call initiated: ${call.id}`);Create Lead
async function createLead(leadData) {
const response = await voicematrix.post('/leads', leadData);
return response.data;
}
// Usage
const lead = await createLead({
name: 'John Doe',
phone: '+972501234567',
email: '[email protected]',
source: 'website',
notes: 'Interested in premium plan'
});
console.log(`Lead created: ${lead.id}`);Webhook Handler (Express.js)
import express from 'express';
import crypto from 'crypto';
const app = express();
// Keep the raw bytes on this route: the signature is computed over them, not over a
// re-serialized object. express.json() would parse and discard them.
app.use('/webhooks/voicematrix', express.raw({ type: '*/*' }));
// X-Webhook-Signature = "v1=" + hex(HMAC-SHA256(secret, `${X-Webhook-Timestamp}.${rawBody}`))
// The key is the whole whsec_... string you were given on create, prefix included.
function verifyWebhookSignature(rawBody, headers, secret, toleranceSec = 300) {
const sig = headers['x-webhook-signature'] || '';
const ts = headers['x-webhook-timestamp'] || '';
if (!sig.startsWith('v1=') || !/^\d+$/.test(ts)) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; // replay window
const expected = 'v1=' + crypto.createHmac('sha256', secret)
.update(ts + '.')
.update(rawBody)
.digest('hex');
return expected.length === sig.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post('/webhooks/voicematrix', (req, res) => {
const secret = process.env.WEBHOOK_SECRET; // returned exactly once, by POST /webhooks
if (!verifyWebhookSignature(req.body, req.headers, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse only after the bytes have been verified.
const { event, data } = JSON.parse(req.body.toString('utf8'));
switch (event) {
case 'call.ended': // NOT "call.completed" — that event was never real, see ../webhooks.md
console.log(`Call ended: ${data.id}`);
// Handle completed call
break;
case 'call.started':
console.log(`Call started: ${data.id}`);
break;
case 'lead.created':
console.log(`New lead: ${data.name}`);
// Handle new lead
break;
case 'lead.updated':
case 'agent.error':
// See ../webhooks.md for the full 5-event catalog — this is all of it.
break;
}
res.json({ received: true });
});
app.listen(3000);Error Handling
Every error response is a nested {"error": {"code", "message", "hint", "fields"}} object — see ../errors.md — not a flat data.error string.
async function safeApiCall(fn) {
try {
return await fn();
} catch (error) {
if (error.response) {
const { status, data } = error.response;
const apiError = data?.error ?? {}; // { code, message, hint, fields, details }
switch (status) {
case 401:
throw new Error(`Invalid API key: ${apiError.code}`); // MISSING_API_KEY | INVALID_API_KEY
case 403:
throw new Error(`Missing scope: ${apiError.details?.your_scopes ?? '(see apiError.message)'}`);
case 429:
throw new Error(`Rate limited (${apiError.code})`); // RATE_LIMIT_EXCEEDED
case 400:
if (apiError.fields) {
throw new Error(apiError.fields.map(f => `${f.field} ${f.reason}`).join('; '));
}
throw new Error(apiError.message ?? 'Bad request');
default:
throw new Error(apiError.message || 'API error');
}
}
throw error;
}
}
// Usage
const calls = await safeApiCall(() => listCalls());