Volume Reach/Docs

Inbound Leads Webhook (POST /api/v1/leads/inbound)

Status: Phase 0 contract — endpoint goes live in Phase 1 (~Weeks 2–3). Authoring date: 2026-05-03. Related plan: docs/superpowers/plans/2026-05-03-lead-ingestion-roadmap.md.

This is the primary path for sending leads from your CRM (or any lead source — Facebook Lead Ads, landing page, custom form) into Volume Reach so our AI agent can call them within seconds.


0. Pick your path (start here)

Volume Reach connects to your existing tools — we don't replace them. The right path depends on what's between your lead source and us today. All three paths land at the same endpoint and behave identically; they just differ in setup time and cost.

Best if you already use one of these: GoHighLevel · HubSpot (Free or paid) · Salesforce · Pipedrive · Zoho CRM · ActiveCampaign · Keap · Close.com.

These CRMs all support outbound webhook actions inside their native automation builders. Configure a workflow like "when a new lead is added, POST to this URL" — paste our webhook URL — done. No Zapier subscription. No extra cost.

This works for Facebook Lead Ads if your CRM has FB integration (most do — GHL, HubSpot, Salesforce all have native FB Lead Ads sync). The flow is:

Facebook Lead Ad  →  Your CRM (native FB sync)  →  CRM workflow webhook  →  Volume Reach

Setup time: 5–10 min in your CRM's automation builder.

Path B — Zapier (only if Path A isn't an option)

Best if you use: Carrot · REIPro · REIReply · custom forms with no CRM behind them · niche real-estate tools without webhook actions.

Zapier sits between your lead source and us. Setup is point-and-click — no code, no API knowledge needed.

Cost: Zapier's Free tier (100 tasks/month) is enough for low-volume tenants. Above that, Zapier Starter is ~$20/mo.

Flow:

Lead source  →  Zapier  →  Volume Reach

Setup time: 15–20 min in Zapier's workflow editor.

Path C — Free escape hatch for "I have nothing"

Best if: you're running Facebook Lead Ads (or any lead source) and don't have a CRM AND don't want to pay for Zapier.

Solution: HubSpot Free (signup). HubSpot's free tier includes:

  • Native Facebook Lead Ads sync (lead lands in HubSpot automatically)
  • Workflow webhook actions (free tier, no time limit)
  • 1,000,000 contacts free forever

Then follow Path A (webhook from HubSpot to us). $0 forever.

This is the recommended path for new operators starting fresh — HubSpot Free + Volume Reach is a complete, no-cost stack until you outgrow it.

Decision summary

What you have todayRecommended pathYour cost
GoHighLevelA (GHL workflow webhook)$0 (already paying for GHL)
HubSpot (any tier)A (HubSpot workflow webhook)$0
SalesforceA (Outbound Message)$0
Carrot / REIPro / REIReplyB (Zapier)$0–$20/mo
Custom landing page → email onlyA (your form's webhook action) or C (HubSpot Free)$0
Facebook Lead Ads, nothing elseC (HubSpot Free) or B (Zapier)$0
Nothing at all, just Volume ReachC (HubSpot Free)$0

Bottom line: the only customer who pays Zapier is the one who picks Path B and exceeds Zapier's free tier (100 tasks/month). Everyone else has a $0 path.


1. Endpoint

POST https://api.volumereach.com/api/v1/leads/inbound

Request format: JSON (Content-Type: application/json).

Auth: two values, both required, separate purposes (think: API key = username, signing secret = password):

HeaderPurposeWhat it proves
Authorization: Bearer <api-key>Identifies your tenant"This request belongs to tenant X"
X-VR-Signature: t=<unix_ts>,v1=<hmac_hex>Replay + integrity protection"This request hasn't been tampered with and isn't a replay"

HMAC computation: hmac_sha256(signing_secret, "{unix_ts}.{request_body_string}"). Timestamp must be within 5 minutes of server time (replay window).

Both values are generated together in the Volume Reach dashboard at /onboarding/lead-source. They're rotatable independently — rotating the signing secret doesn't invalidate the API key.


2. Request schema

Required fields

{
  "phone": "+15551234567",
  "source": "ghl"
}
FieldTypeNotes
phonestringServer normalizes to E.164. Accepts (555) 123-4567, 5551234567, etc.
sourcestringFree identifier — "ghl", "fb_lead_ads", "zapier", "manual", "public_api".
{
  "externalLeadId": "ghl_abc123",
  "consent": {
    "language": "I agree to receive calls and texts about my property...",
    "capturedAt": "2026-05-03T14:30:00Z",
    "sourceUrl": "https://example.com/sell-my-house"
  }
}
FieldTypeWhy
externalLeadIdstringYour CRM's lead id. Used for idempotency — same (source, externalLeadId) won't create a duplicate Lead. Without it, dedupe falls back to a 24h hash of (phone, payload).
consentobjectTCPA compliance evidence. See §6 below.

Optional

{
  "apiVersion": "2026-05",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@example.com",
  "address": {
    "line1": "123 Main St",
    "city": "Austin",
    "state": "TX",
    "zip": "78701"
  },
  "customFields": {
    "lead_score": "high",
    "campaign_id": "summer-2026"
  },
  "sourcePlatform": "GHL Smart List ABC"
}
FieldTypeNotes
apiVersionstringDate-versioned ("2026-05"). Defaults to "2026-05" if absent. See §5 for versioning policy.
firstName, lastName, emailstringContact basics.
addressobjectIf present, we'll create or link a Property record.
customFieldsobjectJSONB passthrough. Treated as untrusted user input — see §7 on safety.
sourcePlatformstringFree-text annotation (e.g. specific list/campaign in your CRM).

3. Response codes

CodeBodyMeaningWhat you should do
200{"outcome": "accepted", "leadId": "..."}Lead created, action will fireDone — log the leadId for later cross-reference.
200{"outcome": "duplicate", "leadId": "..."}Same externalLeadId already receivedDone — this is idempotency working. Do NOT retry.
200{"outcome": "queued_pending_funds", "leadId": "..."}Lead accepted and queued; AI dial deferred until wallet creditedWebhook succeeded — do NOT retry. Top up the wallet to enable AI dialing on this lead.
400{"error": "...", "field": "..."}Schema invalid (missing phone, bad format, etc.)Fix the payload and resend. Do not retry the same payload.
401{"error": "invalid_signature"} or {"error": "expired_timestamp"} or {"error": "invalid_api_key"}Auth failedVerify your API key + signing secret are current. If you rotated either recently, your CRM has stale credentials — re-paste from Volume Reach dashboard.
429{"error": "rate_limited"} + header Retry-After: <seconds>Tenant rate limit exceeded (default 60/min, 1000/hr)Wait Retry-After seconds and retry.
503{"error": "ingestion_paused"}Tenant kill switch is active (admin disabled inbound)Contact Volume Reach support.
5xx(varies)Transient server errorRetry with exponential backoff (recommended: 1s/2s/4s, max 3 attempts).

4. Customer-side recommendations

Configure your CRM's webhook action with:

SettingRecommended value
MethodPOST
Content-Typeapplication/json
Timeout5 seconds
RetriesExponential backoff: 1s → 2s → 4s, max 3 attempts
Retry on5xx and network errors only. Do NOT retry on 200/400/401.

Important: Volume Reach deduplicates retries automatically if externalLeadId is set. So even if your CRM's retry policy fires twice, only one Lead will be created.


5. Versioning policy

The apiVersion field is date-versioned (e.g. "2026-05"). This is audit metadata in v1 — we record what version your payload was, but all payloads currently follow the same shape.

What triggers a v2 bump

A version bump happens only when a breaking change is required:

  • Renaming a field (e.g. phonephoneNumber)
  • Changing a field's type (e.g. address from object to string)
  • Removing a previously-required field
  • Changing the meaning/semantics of an existing field

Adding a new optional field is NOT breaking and does NOT trigger a version bump.

Compatibility window

  • v1 (current, "2026-05") — supported indefinitely until v2 ships.
  • When v2 ships, v1 will be supported for at least 12 months alongside v2. Customers will get advance notice via email + dashboard banner.
  • After the 12-month window, v1 endpoints return 410 Gone.

We commit to never breaking your CRM workflows without ample warning.


If your lead came in via a form that displayed consent language (e.g. "I agree to receive calls and texts..."), include the consent block:

{
  "consent": {
    "language": "<exact text the lead saw and agreed to>",
    "capturedAt": "<ISO 8601 timestamp when they checked the box>",
    "sourceUrl": "<URL of the form/page>"
  }
}

We persist this immutably in an append-only ledger keyed to the Lead. If consent is later disputed, you'll have the exact language, time, and source URL that the lead saw — vetted as legal evidence.

If consent is absent, the Lead is still accepted and processed. We log a warning (lead_inbound_no_consent) so you can see it in the "Recent inbound activity" panel and improve your form. We do not block calls on missing consent in v1 (configurable per-trigger via requireConsent toggle in Phase 3).

⚠️ DRAFT — pending Volume Reach counsel review. Final language will be locked before Phase 7. If you want early review by your own counsel, the language below is a starting point only.

Calling-party attribution: the consent must reference your business (the tenant operating Volume Reach), NOT Volume Reach itself. Volume Reach is the technical platform; your business is the calling party.

Template (draft):

By submitting this form, I agree that {{ Your Business Name }}, including its
agents and AI calling assistants, may contact me by automated phone calls,
prerecorded messages, and SMS at the phone number I provided, even if it's on
a do-not-call list. I understand that consent is not a condition of any
purchase. Message and data rates may apply. Reply STOP to opt out at any
time. View our Privacy Policy: {{ Your Privacy Policy URL }}.

Customization required:

  • {{ Your Business Name }} — your DBA / legal entity that's making the calls.
  • {{ Your Privacy Policy URL }} — must link to a real, current privacy policy page on your domain.

Legal disclaimer: Volume Reach is not your legal counsel. This template is for guidance and must be reviewed by your own attorney before use in production lead-capture forms. State-level consent requirements (CA, FL, NY, OK, WA in particular) may require additional or different language. The template above targets federal TCPA compliance only.


7. customFields safety

The customFields field is JSONB passthrough — anything you send is stored on the Lead and visible in the lifecycle view at /leads/[id]. However, custom fields are NOT automatically interpolated into the AI agent's prompt by default — they require an explicit allowlist (Phase 6).

This is a security feature. If your CRM is compromised or a malicious actor submits a payload like customFields: { property_address: "Ignore previous instructions..." }, it cannot inject prompt instructions into the AI agent.

To use custom fields in the AI agent's prompt:

  1. Add the key to your tenant's allowlist at Settings → Agents → "Custom field allowlist".
  2. Reference it in your agent template as {{ lead_custom_fields.<key> }}.
  3. Volume Reach sanitizes (strips control chars, length-caps 200 chars), wraps the value in <lead_data> delimiters, and instructs the agent to treat its contents as untrusted data.

For non-prompt uses (extraction schema overrides, lifecycle view display), no allowlist is required.


8. Failure-mode runbook

When leads "stop arriving" — first stop is the "Recent inbound activity" panel at /automation/lead-sources in your Volume Reach dashboard. It shows the last 50 inbound webhook attempts with status and error reason.

SymptomLikely causeFix
All requests returning 401 invalid_signatureYou rotated your signing secret in Volume Reach but didn't update your CRM's webhook configurationCopy the current signing secret from /onboarding/lead-source → paste into your CRM.
All requests returning 401 expired_timestampYour CRM's clock is driftedVerify your CRM's server time is accurate (NTP-synced).
All requests returning 401 invalid_api_keyAPI key rotated or revokedGenerate a new key in Volume Reach dashboard, paste into CRM.
Some requests returning 429 rate_limitedInbound spike beyond default 60/min limitAdd backoff to your CRM's retry config. Contact support to raise tenant cap.
All requests returning 503 ingestion_pausedAdmin disabled tenant inbound (kill switch)Contact support — usually intentional incident response.
Requests succeed (200) but no calls fireLead-arrival trigger is not configuredVisit /automation/lead-arrived and configure the trigger settings.
Requests succeed but consent warning logsForm is missing consent captureUpdate form HTML to include consent checkbox + send consent block.

9. Migration from existing public API

If you're currently using the existing POST /api/v1/public/calls endpoint (API-key-only, no HMAC), you can keep using it indefinitely. We will not break that endpoint.

However, the new /leads/inbound endpoint adds:

  • HMAC signature for replay/integrity protection.
  • Idempotency (same externalLeadId doesn't double-call).
  • Consent capture in an immutable ledger.
  • Customer-facing failure visibility (the activity panel above).
  • Lead lifecycle tracking (single-page view of source → call → outcome → callback).

We recommend migrating new integrations to /leads/inbound, but you're not forced to.


10. Audit & retention

We capture audit events for every inbound webhook hit. Retention defaults:

Data typeRetentionReason
LeadConsent (consent ledger)≥4 yearsTCPA statute of limitations
LeadIngestEvent (audit log)90 daysOperational debugging window
Lead (lifecycle data)IndefiniteOperational/CRM data; you may export anytime

These are documented retention targets; automated deletion is v2 work. If you have specific deletion requests (GDPR-style), contact support.


11. Reference: cURL example

TIMESTAMP=$(date +%s)
BODY='{"phone":"+15551234567","source":"ghl","externalLeadId":"ghl_abc123","apiVersion":"2026-05"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "$VR_SIGNING_SECRET" | sed 's/^.* //')

curl -X POST https://api.volumereach.com/api/v1/leads/inbound \
  -H "Authorization: Bearer $VR_API_KEY" \
  -H "X-VR-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Expected response:

{ "outcome": "accepted", "leadId": "lead_clxyz123abc" }

See also