Volume Reach/Docs

Complete endpoint reference for the Volume Reach Public API.

Base URL: https://api.volumereach.com/api/v1/public Auth: Authorization: Bearer vr_live_... Content-Type: application/json Rate limits: 60 req/min per tenant on write routes, 120 req/min per tenant on read routes. Limits are shared across all API keys on a single tenant — a customer with 3 keys still has one shared budget. Exceeding → 429 rate_limited with a Retry-After header. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.


Conventions

  • All request/response bodies are JSON.
  • Timestamps are ISO-8601 in UTC.
  • Phone numbers are E.164 (+15551234567).
  • All routes are tenant-scoped — objects created with one API key are invisible to API keys from other tenants, even if you guess the ID.
  • Error envelope shape is consistent: { error: { code, message, details? } }.
  • idempotencyKey is opt-in but recommended on write routes to protect against CRM retries.

Calls

POST /calls — Trigger an outbound qualification call

Scope: calls:place

Request body:

FieldTypeRequiredDescription
agentIdstringCUID of a Voice Agent you own.
phoneNumberstring (E.164)Destination number.
fromPhoneNumberIdstringForce a specific outbound number. Omit for health-weighted auto-selection.
variablesobjectKey/value data injected into the agent's <context> block. Max 100 entries × 4 KB each.
metadataobjectOpaque passthrough (e.g. your CRM's lead ID). Max 16 KB. Echoed in every webhook. Never shown to the LLM.
idempotencyKeystringUp to 255 chars. Scoped to your API key. Replays of the same key + body return the stored call record. 24h retention.
knownConsentboolDefault false. You attest that documented TCPA consent exists for this call. See compliance.md.
consentRecordIdstringYour own reference to the consent record. Stored + echoed in every webhook.

Response 201 Created:

{
  "call": {
    "id": "call_xyz",
    "status": "queued",
    "agentId": "va_abc",
    "phoneNumber": "+15551234567",
    "fromPhoneNumber": "+15559876543",
    "direction": "ai_outbound",
    "outcome": null,
    "endedReason": null,
    "durationSeconds": null,
    "startedAt": null,
    "endedAt": null,
    "createdAt": "2026-04-22T14:03:21.000Z",
    "transcript": null,
    "extraction": null,
    "extractionError": null,
    "variables": { "leadName": "Jane Smith" },
    "metadata": { "crmLeadId": "hubspot-987" },
    "knownConsent": true,
    "consentRecordId": null
  }
}

Error codes:

HTTPCodeWhen
400invalid_requestA required field is missing, has the wrong type, or exceeds size limits. details lists specifics.
400invalid_phone_numberphoneNumber could not be parsed as E.164.
400missing_variablesThe agent's prompt references {{varName}} tokens not provided in variables. details.missing lists them.
401invalid_api_key / UnauthorizedMissing or invalid Authorization header.
402insufficient_balancePAYG tenant: wallet balance too low to cover estimated call cost. Top up and retry.
403insufficient_scope / ForbiddenAPI key lacks calls:place.
403public_api_disabledPublic API not yet enabled for your tenant. Contact support.
404agent_not_foundagentId doesn't exist or isn't active in your tenant.
404phone_number_not_foundfromPhoneNumberId not owned by your tenant.
409idempotency_conflictSame idempotencyKey was used previously with a different request body.
429rate_limitedPer-tenant rate limit exceeded (60 writes/min or 120 reads/min, shared across all your API keys). Retry after Retry-After seconds.
429concurrent_limit_exceededTenant's concurrent-call limit reached. Wait for in-progress calls to finish.
500placement_failedSomething went wrong inside Volume Reach. Safe to retry with the same idempotencyKey.
503idempotency_in_progressSame key + same body while the original request is still dispatching. Retry after ~1s (we set Retry-After: 1).

GET /calls/:id — Read a call

Scope: calls:read

Response 200 OK — same shape as the POST /calls response, with fields populated as the call progresses:

  • status: queuedringingin-progresscompleted / no_answer / busy / failed / canceled.
  • transcript: full transcript text (populated after call ends).
  • extraction: structured output (populated if the agent has extractionSchema configured).
  • recordingUrl: only returned via this endpoint (not in webhook). Signed GCS URL, expires in 15 minutes. Fetch fresh when you need to download.
  • durationSeconds: conversation duration. null if the call was never answered.

Error codes:

HTTPCodeWhen
404call_not_foundNo call with this ID in your tenant or the call wasn't created via the Public API (dialer calls are not exposed here; use internal Call Logs instead).

POST /calls/:id/end — Force-terminate a call

Scope: calls:place

Hang up an in-progress call. Idempotent — safe to call multiple times; if the call has already ended, returns the current state.

Response 200 OK:

{ "id": "call_xyz", "status": "ending" }

Agents

GET /agents — List voice agents

Scope: read

Response 200 OK:

{
  "agents": [
    {
      "id": "va_abc",
      "name": "Roofing Lead Qualifier",
      "isDefault": true,
      "extractionSchema": { "type": "object", "properties": { "interested": { "type": "boolean" } }, "required": ["interested"] },
      "updatedAt": "2026-04-22T13:00:00.000Z"
    }
  ]
}

Only public-facing agent fields are exposed. Full configuration (system prompt, voice settings, LLM tuning) is managed in the Volume Reach app.


GET /agents/:id — Read a single agent

Scope: read

Same shape as the list item. 404 agent_not_found if the ID doesn't exist or isn't in your tenant.


POST /agents/:id/extraction-schema — Save / update extraction schema

Scope: write

Request body:

{ "schema": { "type": "object", "properties": {...}, "required": [...] } }

Pass { "schema": null } to clear the schema (disable extraction for this agent).

See extraction.md for schema authoring tips and examples.

Error codes:

HTTPCodeWhen
400invalid_extraction_schemaSchema is not an object, has unknown type, references a missing required field, or exceeds 32 KB. details.path indicates the problem.
404agent_not_foundNo such agent in your tenant.

Webhooks

See webhooks.md for event types, payload shapes, and signature verification.

GET /webhooks — List webhook endpoints

Scope: webhooks

Response:

{
  "endpoints": [
    {
      "id": "whe_abc",
      "name": "Production",
      "url": "https://your-app.com/webhooks",
      "method": "POST",
      "signingSecret": "****xxxx",
      "isActive": true,
      "createdAt": "2026-04-22T..."
    }
  ]
}

Signing secrets are masked (last 4 chars only). The full secret is only returned once, at creation or regeneration time.


POST /webhooks — Create an endpoint

Scope: webhooks

Request body:

{
  "name": "Production webhook",
  "url": "https://your-app.com/webhooks/volumereach",
  "method": "POST",
  "headers": { "X-Custom": "value" }
}
FieldRequiredDescription
name1–100 chars. Human label.
urlHTTPS in production. Must not resolve to a private IP.
methodPOST (default) or PUT.
headersExtra headers to include on every delivery. Security-critical headers (Host, Authorization, Content-Type, X-Webhook-*, etc.) are stripped.

Response 201 Created:

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

⚠️ Save signingSecret now — it's only shown once.

Errors:

HTTPReason
400Invalid URL / private-IP URL / non-HTTPS in production / too many endpoints (max 10 per tenant)

DELETE /webhooks/:id — Delete an endpoint

Scope: webhooks

Removes the endpoint and any pending retry deliveries targeting it. Triggers referencing this endpoint are also cleaned up.


Webhook deliveries (DLQ inspection + replay)

Every webhook attempt creates a WebhookDeliveryLog row. Successful deliveries are marked success; failures retry on the schedule documented in webhooks.md. After 6 failed attempts the row is marked failed and lands in the DLQ. These endpoints let you inspect and replay deliveries programmatically (all require webhooks scope).

GET /webhook-deliveries — List deliveries

Scope: webhooks

Query params:

ParamTypeDefaultDescription
statusstringFilter to success, retrying, failed, or pending.
sinceISO-8601Bound the lookback. Max 30 days — anything older is silently clamped.
pageint11-indexed page.
limitint20Max 50.

Response 200 OK:

{
  "deliveries": [
    {
      "id": "dlv_abc123",
      "createdAt": "2026-04-22T14:05:47.123Z",
      "eventType": "call.completed",
      "status": "failed",
      "attempts": 6,
      "responseStatus": 500,
      "errorMessage": "HTTP 500",
      "nextRetryAt": null,
      "lastAttemptAt": "2026-04-23T14:08:02.100Z",
      "endpoint": { "id": "whe_abc", "name": "Production", "url": "https://your-app.com/webhooks" },
      "trigger": { "id": "trg_xyz", "name": "All call completions" }
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 4, "totalPages": 1 }
}

GET /webhook-deliveries/:id — Single delivery

Scope: webhooks

Returns the same shape as the list endpoint PLUS payload — the full JSON body we sent (useful for forensics when your handler logs were insufficient).

POST /webhook-deliveries/:id/retry — Replay a failed delivery

Scope: webhooks

Re-dispatches the stored payload to the original endpoint using the same X-Webhook-Delivery-Id so your idempotent handler sees it as a retry, not a new event.

Response 200 OK — returns the updated delivery record (status flipped to retrying or success).

Error codes:

HTTPCodeWhen
404delivery_not_foundID doesn't exist, isn't owned by your tenant, or is older than the retention window.
400invalid_requestDelivery is still in pending/retrying state — manual replay only makes sense after the retry loop exhausted or explicitly for failed / success entries. See the error message for specifics.

Automation triggers (event → endpoint subscriptions)

A webhook endpoint is a URL; an automation trigger subscribes an endpoint to an event type.

POST /automation-triggers — Subscribe

Scope: webhooks

Request body:

{
  "name": "All call completions",
  "eventType": "call.completed",
  "webhookEndpointId": "whe_abc",
  "conditions": { "outcome": ["INTERESTED", "APPOINTMENT"] }
}
FieldRequiredDescription
name1–100 chars.
eventTypeCurrently allowed: call.completed. See webhooks.md.
webhookEndpointIdMust belong to your tenant.
conditions.outcomeArray of outcome strings; only fires when the call outcome matches one of them. Omit to fire on every outcome.

Max 25 triggers per tenant.


GET /automation-triggers — List

Scope: webhooks

{
  "triggers": [
    {
      "id": "at_abc",
      "name": "...",
      "eventType": "call.completed",
      "webhookEndpointId": "whe_abc",
      "conditions": {},
      "isActive": true,
      "webhookEndpoint": { "id": "whe_abc", "name": "Production", "url": "https://..." }
    }
  ]
}

GET /automation-triggers/:id — Read

Scope: webhooks

DELETE /automation-triggers/:id — Unsubscribe

Scope: webhooks


Future additions

Reserved space — will be added when we receive consistent customer requests. Documented so integrators can plan around eventual support.

FeatureStatus
Mid-call tool execution (agent calls your HTTPS endpoint during the conversation)Deferred
scheduledAt to delay a call to a future timeDeferred
Inbound agent routing (phone number → voice agent)Deferred
Webhook signing-secret rotation with overlap windowDeferred
Webhook delivery-log replay API (POST /webhook-deliveries/:id/retry)Deferred
More granular call lifecycle events (call.ringing, call.answered, etc.)Deferred; currently you receive call.started + call.completed
Platform-enforced TCPA floors (hard-stop midnight-6am, per-number daily cap)Deferred; MVP is tenant-attestation only