Volume Reach

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

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.

GoHighLevel specifically. Use GoHighLevel's free Webhook action — not "Custom Webhook". The free action has no header field, so your API key goes in its Custom Data section instead: click + Add item, name it anything, and paste the key as the value. GoHighLevel then sends the contact's name, phone, email, tags and address by itself; there is no request body to write. We find the key by its value, remove it the moment it arrives, and never store it on the contact. (The paid Custom Webhook action also works, with an Authorization header and a body you write — GoHighLevel bills it at about 1 cent per lead.) The step-by-step is in the app under Automations → Lead Sources → GoHighLevel.

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's free Webhook action, key in Custom Data)$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: your API key. That is all most integrations need. Create it in the app under Automations → Lead Sources — pick your CRM's tile and it generates a key with the right permission. (Settings → API Keys cannot create a lead-source key.)

Send the key in one of two ways:

WhereHowUse it when
HeaderAuthorization: Bearer <api-key>Zapier, custom code, HubSpot, or any tool that lets you set headers
Request bodyany field whose value is the key — top level, or inside customDataGoHighLevel's free Webhook action, or any tool that cannot set headers

A key sent in the body is matched by its value, so the field's name does not matter. We delete it from the payload as soon as it arrives; it is never saved on the contact, shown in your activity feed, or read to your voice agent.

Optional, advanced: request signing. Keys created from a Lead Sources tile do not require a signature, and CRMs like GoHighLevel cannot compute one. If you write your own integration and want replay and tamper protection, turn signing on for that key and send X-VR-Signature: t=<unix_ts>,v1=<hmac_hex>, where the HMAC is hmac_sha256(signing_secret, "{unix_ts}.{request_body_string}") and the timestamp is within 5 minutes of server time. The signing secret lives in Settings → API Keys → Webhook security.

If you are not signing requests, do not add an X-VR-Signature header. A signature header that is present is always checked, and a made-up one is rejected with a 401.


1b. Per-key settings: where leads land, whether they are called

Two choices ride the API key and are applied to every lead it sends (set in Lead Sources when you connect, editable under Settings → API keys):

SettingEffect
Where should these leads land?Every accepted lead becomes a contact. Pick a pipeline stage and it is also placed on that board (first placement only — a contact that is already an opportunity is never moved by a re-push). A "Moved to a stage" automation can chain off it.
Call these leads automatically?Yes: the Lead Arrived trigger dials within seconds. No: the lead is saved, listed under Leads → Not called yet, and the activity row shows Saved, not called (AUTO_CALL_OFF). Automations still run either way.

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"
  },
  "tags": ["Transactions", "Hot"],
  "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.
addressobjectline1 (or address1) is required for the address to count; city, state, zip (or postalCode) optional. Stored on the contact record we create for every accepted lead. Sent without a street line, the address is ignored and the contact shows no address.
tagsstring[]Up to 10 tags applied to the contact. Send your CRM stage here (e.g. "Transactions") — an Automation with the "Tag added" trigger can then act on it. Duplicates are collapsed case-insensitively.
customFieldsobjectJSONB passthrough. Treated as untrusted user input — see §7 on safety.
sourcePlatformstringFree-text annotation (e.g. specific list/campaign in your CRM).

Every accepted lead becomes a contact immediately (visible under Contacts with the address and tags above), whether or not the Lead Arrived call trigger is on. A later payload for the same lead does not overwrite an existing contact's address.


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. phone → phoneNumber)
  • 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 with a signature reasonEither you send an X-VR-Signature header you don't need, or you rotated your signing secret and didn't update your CRMNot signing requests? Remove the X-VR-Signature header. Signing on purpose? Copy the current secret from Settings → API Keys → Webhook security into your CRM.
Your CRM shows 401 and nothing appears in Recent activityThe API key never reached us, or was mistyped. A request we cannot tie to your account cannot appear in your feedRe-copy the key. With GoHighLevel's free Webhook action it goes under Custom Data as a value; with anything else it goes in an Authorization: Bearer header.
400 placeholder_valueThe body still holds our example values (REPLACE_WITH_…, or the sample lead Carlos Mendez)Map your CRM's own fields into the body.
400 malformed_jsonA hand-written body has an unescaped quote or a line break inside a merge fieldFix the body, or use a tool that builds the JSON for you.
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:

  • Optional HMAC signing for replay/integrity protection (off by default for keys created in Lead Sources).
  • 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