Volume Reach/Docs

Configure the AI to return predictable, structured data after every call so your integration can act on it without parsing free-form transcripts.


How it works

When you attach a JSON Schema to a Voice Agent, Volume Reach runs a second LLM pass after the call completes:

  1. Call ends → transcript is saved.
  2. Volume Reach sends the transcript + your schema to OpenAI using its function-call API.
  3. OpenAI returns a structured object matching your schema exactly.
  4. The object is written to the Call record as extraction and delivered in the call.completed webhook.

Failures (e.g. OpenAI rate-limit during a burst) are captured in extraction_error on the same webhook — your handler can fall back to reading the transcript manually.

Model: gpt-4.1-mini by default (fast, cheap, sufficient for 2–3 minute transcripts). Cost is bundled into aiCostUsd on the call record.


Authoring the schema

Pass a standard JSON Schema object to POST /agents/:id/extraction-schema:

curl -X POST https://api.volumereach.com/api/v1/public/agents/va_abc/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?" },
        "budgetRange": {
          "type": "string",
          "enum": ["under_5k", "5k_to_15k", "15k_to_50k", "50k_plus", "not_mentioned"]
        },
        "timelineMonths": {
          "type": "integer",
          "description": "When they want the work done, in months from today. 0 = ASAP. null = not mentioned."
        },
        "appointmentDateTimeISO": {
          "type": "string",
          "format": "date-time",
          "description": "Confirmed appointment in ISO-8601 UTC, or null if not confirmed."
        },
        "contactEmail": { "type": "string" },
        "objections": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Concerns the lead raised (e.g. price, timing, spouse approval)."
        }
      },
      "required": ["interested"]
    }
  }'

Pass { "schema": null } to disable extraction for an agent.


Best-practice guidelines

1. Use description liberally

Descriptions are the LLM's instructions for each field. The more specific you are, the better the extracted value.

"budgetRange": {
  "type": "string",
  "enum": ["under_5k", "5k_to_15k", "15k_to_50k", "50k_plus", "not_mentioned"],
  "description": "The lead's stated budget range. Map estimates carefully: \"around ten grand\" -> 5k_to_15k. Use not_mentioned if they avoided the question."
}

2. Prefer enums over free-form strings

enum constrains output to a known set → easier downstream routing. Without enum, the LLM might return "mid-range", "affordable", "cheap" for the same budget level.

3. Use null by including types in a union

To allow a field to be absent, add "null" to the type union:

"contactEmail": { "type": ["string", "null"] }

4. Keep required short

Only mark fields as required when you truly can't act without them. Everything required is forced to have a value, even if the call didn't cover it — the LLM will hallucinate something plausible. For optional fields, allow null.

5. Test with real transcripts before going live

Open a recent call in the Volume Reach app → copy the transcript → paste into the OpenAI playground with your schema as a function parameter. Verify the LLM fills the fields correctly before saving.


Schema structure rules

The following are enforced at save time (you'll get a 400 invalid_extraction_schema if violated):

  • schema must be a JSON object (not an array or primitive).
  • Top-level type must be one of: object, string, number, integer, boolean, array, null.
  • properties (when present) must be an object.
  • required (when present) must be an array of strings.
  • For type: "object", every name in required must be present in properties.
  • Total serialized schema size ≤ 32 KB.

OpenAI's own validation runs at call completion — invalid schemas that pass our structural check will surface as extraction_error rather than blocking the save, so complex validation issues are caught in the webhook payload.


Example: booking an appointment

Minimum schema to drive a Google Calendar booking flow:

{
  "type": "object",
  "properties": {
    "interested": { "type": "boolean" },
    "wantsAppointment": { "type": "boolean" },
    "appointmentDateTimeISO": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "Confirmed slot in ISO-8601. Null if not confirmed."
    },
    "appointmentDurationMinutes": {
      "type": ["integer", "null"],
      "description": "Length of the appointment. Default 60 minutes for site visits."
    },
    "contactEmail": { "type": ["string", "null"] },
    "notes": {
      "type": "string",
      "description": "Freeform notes for the sales rep — reference property details, concerns, urgency signals."
    }
  },
  "required": ["interested", "wantsAppointment"]
}

Your webhook handler:

if (data.extraction.interested && data.extraction.wantsAppointment && data.extraction.appointmentDateTimeISO) {
  await googleCalendar.events.insert({
    calendarId: "primary",
    requestBody: {
      summary: `Site visit: ${data.contact.name}`,
      start: { dateTime: data.extraction.appointmentDateTimeISO },
      end: { dateTime: addMinutes(data.extraction.appointmentDateTimeISO, data.extraction.appointmentDurationMinutes ?? 60) },
      attendees: data.extraction.contactEmail ? [{ email: data.extraction.contactEmail }] : undefined,
      description: data.extraction.notes,
    },
  });
}

Cost + latency

  • Typical extraction adds ~1–3 seconds to call completion before the webhook fires.
  • Typical extraction cost is < $0.001 per call (rolled into aiCostUsd on the call record).
  • Transcripts longer than 30 000 characters are truncated before extraction (the model has a 128K context window, but we cap to bound cost and latency for unusual outliers).

Failure modes

SituationBehavior
Agent has no extractionSchema configuredNo extraction runs; webhook extraction field is null.
Transcript is empty (e.g. no-answer call)No extraction runs; extraction is null.
OpenAI returns 429 (rate limit)Up to 3 retries with 1–3 second backoff before surfacing extraction_error.
OpenAI returns 5xx (transient outage)Up to 3 retries.
OpenAI returns 400 (invalid schema / content policy)No retry; extraction_error contains the message.
OpenAI returns malformed JSONextraction_error: "Failed to parse extraction JSON: ...". Very rare — report to support with the call_id if it happens.

Extraction is considered non-critical: failures never block the call from completing or the webhook from firing. Your integration should handle extraction === null and extraction_error !== null gracefully.