Volume Reach/Docs

End-to-end walkthrough — from zero to your first AI-qualified call — in about 15 minutes.

What you'll build: when a new lead fills out your Facebook / web form, Volume Reach dials them, qualifies them via an AI voice agent, and posts the structured result (interested? budget? appointment time?) to your webhook so your system can book the calendar slot automatically.


Prerequisites

  • A Volume Reach account (sign up at volumereach.com).
  • At least one phone number purchased and A2P-registered (Settings → Phone Numbers).
  • An HTTPS endpoint you control that can receive webhooks (Zapier / Make / your backend).

Step 1 — Create a Voice Agent (5 min)

  1. In the Volume Reach app, go to Settings → Voice Agents.
  2. Click Create Agent and choose the Custom prompt mode.
  3. Fill in:
    • Name — e.g. "Roofing Lead Qualifier"
    • Voice — pick a Cartesia or ElevenLabs voice
    • Greeting — e.g. Hi {{leadName}}, this is Alex from Acme Roofing — is now still a good time to chat about the estimate request you submitted?
    • System Prompt — the agent's full instructions. Use {{variableName}} placeholders for any data you want to pass in at call time.
    • Call Objective (optional) — e.g. Confirm they own the home, confirm interest, capture budget range and timeline, offer an in-person estimate appointment within the next 7 days.
  4. Click Save. The agent detail page shows an Agent ID at the top like va_abc123...copy this.

About {{variable}} placeholders

Any {{varName}} token in the systemPrompt, greeting, callObjective, etc. becomes a required field when triggering a call. The API validates that every placeholder has a value before dialing, so missing a variable returns a 400 missing_variables error instead of wasting a call on an unresolved placeholder.

In addition, any variables you pass in the variables payload are made available to the agent as reference data in a delimited <context> block at the end of the system prompt. The agent can reference them naturally (e.g. greet the lead by name, mention their property) but is instructed to treat them as data, not as instructions.


Step 2 — Generate an API Key (1 min)

  1. Go to Settings → API Keys.
  2. Click Create Key.
  3. Name it something descriptive like "Production CRM integration".
  4. Select the scopes you need:
    • calls:place — trigger + terminate calls. Required.
    • calls:read — read call records (transcript, extraction, recording URL). Required.
    • read — read agent config (so your integration can list/verify agents).
    • webhooks — manage webhook endpoints + triggers.
    • write — modify extraction schema (only needed if your integration configures it programmatically).
  5. Click Create. A key starting with vr_live_ appears oncecopy it now. It cannot be retrieved later; if lost, rotate by creating a new key.

Step 3 — Enable the Public API for your tenant

The /api/v1/public/* routes are gated behind a per-tenant feature flag (publicApiEnabled). For new customers this is enabled during onboarding; if you're self-serve or migrating, open a support ticket to flip it on.

You can verify by making any authenticated request — if you receive 403 public_api_disabled, ask support to enable the flag.


Step 4 — Register a Webhook Endpoint (2 min)

Create the destination that will receive call.completed events.

curl -X POST https://api.volumereach.com/api/v1/public/webhooks \
  -H "Authorization: Bearer vr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production webhook",
    "url": "https://your-app.com/webhooks/volumereach",
    "method": "POST"
  }'

Response (sample):

{
  "endpoint": {
    "id": "whe_abc123",
    "userId": "user_abc",
    "name": "Production webhook",
    "url": "https://your-app.com/webhooks/volumereach",
    "method": "POST",
    "signingSecret": "<HEX_SECRET_SHOWN_ONCE>",
    "isActive": true
  }
}

Save signingSecret somewhere secure now — it's needed to verify webhook signatures, and it will not be displayed again. Treat it like a password. If lost, call POST /webhooks/:id/regenerate-secret from the in-app settings to rotate.

Your webhook endpoint URL must:

  • Use HTTPS in production.
  • Not resolve to private / reserved IP ranges (defeats DNS rebinding attacks — we resolve and verify at dispatch time).
  • Return a 2xx status within 10 seconds. Non-2xx responses trigger retry.

Step 5 — Subscribe to call.completed events (1 min)

A webhook endpoint is just a URL — you also need a trigger that wires an event type to the endpoint.

curl -X POST https://api.volumereach.com/api/v1/public/automation-triggers \
  -H "Authorization: Bearer vr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "All call completions",
    "eventType": "call.completed",
    "webhookEndpointId": "whe_abc123"
  }'

Optionally filter by outcome:

{
  "name": "Interested leads only",
  "eventType": "call.completed",
  "webhookEndpointId": "whe_abc123",
  "conditions": { "outcome": ["INTERESTED", "APPOINTMENT"] }
}

Step 6 — Configure structured extraction (optional, 2 min)

Tell the agent exactly what data to extract from the call so your webhook receives it in a predictable shape.

curl -X POST https://api.volumereach.com/api/v1/public/agents/va_abc123/extraction-schema \
  -H "Authorization: Bearer vr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "schema": {
      "type": "object",
      "properties": {
        "interested": { "type": "boolean", "description": "Did the lead express interest in a quote?" },
        "budgetRange": { "type": "string", "description": "e.g. \"under $5k\", \"$5k-15k\", \"$15k+\"; null if not mentioned." },
        "timelineMonths": { "type": "integer", "description": "When do they want the work done? Months from today. Null if unclear." },
        "appointmentDateTimeISO": { "type": "string", "format": "date-time", "description": "Confirmed appointment slot in ISO-8601, or null." },
        "contactEmail": { "type": "string" }
      },
      "required": ["interested"]
    }
  }'

On call.completed, the webhook payload will include an extraction field populated from this schema (or extraction_error if the extractor couldn't run). See extraction.md for details.


Step 7 — Trigger your first call (1 min)

curl -X POST https://api.volumereach.com/api/v1/public/calls \
  -H "Authorization: Bearer vr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "va_abc123",
    "phoneNumber": "+15551234567",
    "variables": {
      "leadName": "Jane Smith",
      "propertyAddress": "123 Oak St",
      "leadSource": "facebook-roofing-campaign"
    },
    "metadata": { "crmLeadId": "hubspot-987" },
    "idempotencyKey": "hubspot-lead-987",
    "knownConsent": true
  }'

Response (201 on success):

{
  "call": {
    "id": "call_xyz",
    "status": "queued",
    "agentId": "va_abc123",
    "phoneNumber": "+15551234567",
    "createdAt": "2026-04-22T14:03:21.000Z",
    "variables": { "leadName": "Jane Smith", "propertyAddress": "123 Oak St", "leadSource": "facebook-roofing-campaign" },
    "metadata": { "crmLeadId": "hubspot-987" },
    "knownConsent": true
  }
}

Volume Reach will now:

  1. Route through your configured SIP trunk and phone number.
  2. Dial the lead.
  3. When answered, run the agent according to your system prompt. The agent sees variables as reference data in a <context> block.
  4. Transcribe the conversation, classify the outcome, and run structured extraction.
  5. Deliver the result to your webhook within a few seconds of call completion.

Every webhook request includes these headers:

X-Webhook-Signature: sha256=<HEX>
X-Webhook-Timestamp: <UNIX_SECONDS>

Verify before trusting the payload:

import crypto from "node:crypto";

function verifyWebhook(req, signingSecret) {
  const signature = req.headers["x-webhook-signature"]; // "sha256=<hex>"
  const timestamp = req.headers["x-webhook-timestamp"];
  const rawBody = req.rawBody; // capture raw bytes, not req.body (JSON-parsed)

  if (!signature || !timestamp) return false;

  // Reject timestamps older than 5 minutes to prevent replay
  const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (Number.isNaN(ageSeconds) || ageSeconds > 300) return false;

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", signingSecret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  // Use timing-safe compare to prevent timing attacks
  const sigBuf = Buffer.from(signature);
  const expectedBuf = Buffer.from(expected);
  if (sigBuf.length !== expectedBuf.length) return false;
  return crypto.timingSafeEqual(sigBuf, expectedBuf);
}

Full code samples in multiple languages: webhooks.md#verifying-signatures.


Step 9 — Handle the webhook

The payload for call.completed looks like this:

{
  "event": "call.completed",
  "timestamp": "2026-04-22T14:05:47.123Z",
  "data": {
    "call_id": "call_xyz",
    "outcome": "INTERESTED",
    "duration_seconds": 127,
    "started_at": "2026-04-22T14:03:41.000Z",
    "transcript": "Agent: Hi Jane...\nJane: Yes, I'm interested...\n(full transcript)",
    "recording_url": "https://storage.googleapis.com/...?X-Goog-Signature=...",
    "contact": { "name": "Jane Smith", "phone": "+15551234567", "address": "123 Oak St", "city": "...", "state": "...", "zip": "..." },
    "campaign": null,
    "callback": null,
    "variables": { "leadName": "Jane Smith", "propertyAddress": "123 Oak St", "leadSource": "facebook-roofing-campaign" },
    "metadata": { "crmLeadId": "hubspot-987" },
    "known_consent": true,
    "consent_record_id": null,
    "extraction": {
      "interested": true,
      "budgetRange": "$5k-15k",
      "timelineMonths": 3,
      "appointmentDateTimeISO": "2026-04-29T18:00:00.000Z",
      "contactEmail": "jane@example.com"
    },
    "extraction_error": null
  }
}

Your handler typically:

  1. Verifies the signature (above).
  2. Reads data.extraction for the structured fields.
  3. If extraction.interested === true and extraction.appointmentDateTimeISO is set → create the Google Calendar event.
  4. Updates your CRM with the outcome + transcript.
  5. Returns 200 OK (important — non-2xx triggers retry).

See integrations/raw-http.md for a complete sample handler.


Next steps