Skip to main content

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

bash
npm install axios

Setup

javascript
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

javascript
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

javascript
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

javascript
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)

javascript
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.

javascript
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());
All pages