Deliver agent-sourced leads to your CRM

Subscribe to signed lead, booking and call webhooks and land every agent-confirmed request in HubSpot, GoHighLevel, Slack, a spreadsheet or any of 6,000+ apps through Zapier or Make.

Deliver agent-sourced leads to your CRM

When a request on your site is completed (a consultation, a quote, a callback, a booking), confirmed by the person on your card or by their own agent with their go-ahead, the lead already lives in your Inception Agents Leads inbox and in the email your team receives. This recipe puts the same lead where your team actually works: the CRM, the scheduling tool, Slack, a sheet. You subscribe to signed lead.*, booking.* and call.* webhooks, verify the signature, and hand the payload to your system of record.

Two things make these different from the Revenue Moment webhooks: a lead envelope can carry the person’s contact details (only to subscriptions created for that purpose, only once the request was completed with their consent), and it carries the eleven layers the business pays for: what they typed, what they said in their own words, which tool made the request, whether the contact was verified, the qualification verdict, the value estimate, where the lead came from, how it should be routed, how the person wants to be contacted, and (later) what happened.

What you need before starting:

  • A webhook subscription of kind lead or booking (below). Creation, signing and delivery semantics are the same as Webhooks.
  • A receiver: your own endpoint, a Zapier Catch Hook, or a Make custom webhook.

Subscription kinds

The kind you choose decides what the envelope carries:

KindReceivesContact block
leadlead.*, call.*, and booking.*Yes — name, email, phone, decrypted at delivery
bookingbooking.* onlyNever — slot, status and the reference only
moment (default)Revenue Moment lifecycle eventsNot applicable (pseudonymous subject_ref)

Create a lead subscription:

curl -X POST https://api.inceptionagents.com/api/v1/webhooks/subscriptions \
  -H "Authorization: Bearer $INCEPTION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
    "kind": "lead",
    "event_types": ["lead.captured", "lead.outcome_reported", "booking.confirmed"],
    "label": "Zapier → HubSpot"
  }'

The response carries signing_secret exactly once. Store it as INCEPTION_WEBHOOK_SECRET in the receiving service. Event types must belong to the kind (lead.captured on a booking subscription is a 400). An empty event_types means every event the kind receives.

Events

EventFires whenExtra block
lead.capturedThe request was completed with contact details and consent, confirmed by the person or relayed by their own agent — the lead is deliverable. The event to wire to a CRM.—
lead.qualifiedA vertical pack recorded a qualification verdict for the lead (sent right after lead.captured).— (the verdict is in data.lead.qualification)
lead.verifiedVerification strengthened past the agent relay: the phone was verified by SMS code.—
lead.deliveredYour team was notified (email, Slack, staff SMS).—
lead.outcome_reportedYour team, a Y/N SMS reply, or a connected CRM recorded what happened.outcome
booking.requestedA time window was requested; nothing is on a calendar yet.booking
booking.confirmedA slot is on the calendar (a connected scheduler created the booking).booking
booking.cancelledThe booking was cancelled by the vendor system.booking
call.startedA tracked call from the lead’s number began.call
call.completedThe tracked call ended (answered, completed or missed).call

The envelope

Every delivery is a POST with the headers from the webhooks reference (X-Inception-Signature, X-Webhook-Event, Webhook-Id, Webhook-Timestamp). A lead.captured delivery to a lead subscription:

{
  "apiVersion": "2026-07",
  "event": "lead.captured",
  "webhookId": "evt_1f6d2c58-90ab-4c3e-8f21-7a4b5d6e9c01",
  "timestamp": "2026-09-10T15:04:22.000Z",
  "data": {
    "lead": {
      "ref": "IA-7KQ2ZM",
      "status": "delivered",
      "intent": "request_consultation",
      "tool_name": "request_consultation",
      "vertical_slug": "legal-personal-injury",
      "stated_facts": { "incident_type": "motor_vehicle", "incident_date": "2026-08-14", "state": "TX", "treatment_started": true },
      "verbatim_ask": "I was rear-ended two weeks ago and my neck still hurts. What are my options?",
      "derived_context": { "tool_sequence": ["request_consultation"] },
      "verification": { "status": "verified", "method": "agent_confirm", "approval": { "method": "card_tap", "source": "stated", "step": "confirm_call" }, "dedupe_hit": false },
      "qualification": { "verdict": "may_qualify", "reasons": ["incident within statute window"] },
      "value_estimate_cents": 250000,
      "provenance": { "surface": "webmcp", "agent_client": "chatgpt", "session_id": "sess_…", "consent_basis": "express_written", "approval_method": "card_tap" },
      "routing": { "practice_area": "personal_injury", "location": "Austin, TX" },
      "contact_prefs": { "channels": ["phone", "email"], "consent": { "contact": true, "sms": true, "marketing": false } },
      "created_at": "2026-09-10T14:59:10.000Z",
      "verified_at": "2026-09-10T15:04:22.000Z"
    },
    "contact": { "name": "Ada Lovelace", "email": "ada@example.com", "phone": "+14155550100" },
    "attribution": { "params": "utm_source=inception_lead&utm_campaign=lead.captured" }
  }
}

Field notes:

  • data.lead.status is read when the event is delivered, which is after we notify your team: delivered once an email, Slack or SMS notification reached you, verified if none did. Route on the event name, not on the status.
  • data.lead.derived_context.tool_sequence holds the tool that made the request. The questions the person’s agent asked before it are not in the webhook: they are in the lead email and your Leads inbox under “What their agent asked first”, with a link to replay the conversation in your dashboard.
  • data.lead.ref is the reference the agent read back to the customer. Use it as the external id in your CRM so a later lead.outcome_reported or a call event lands on the same record.
  • data.contact is null on booking subscriptions and before the person confirmed. Never assume it is present.
  • data.lead.stated_facts is PII-redacted at intake (emails and phone numbers inside free text are masked). Contact lives only in data.contact.
  • data.lead.provenance.agent_client is the source you have never had before: chatgpt, claude, gemini, … Put it on the CRM record; it is the “agent-sourced vs aggregator” number on day one.
  • data.lead.provenance.approval_method says how the person’s approval reached us: card_tap (they confirmed on the card on your page), agent_authorized (their own agent relayed their details and agreement, in one call or at the confirm step), express_form, or sms_confirmed (a text-message code was verified). sms_confirmed is recorded only when the verified number is the number the request was completed with. consent_basis is express_written when the person worked a form themselves (the card on your page, or the express form) and agent_authorized when the consent flags arrived in an agent’s relay, verified number or not. Marketing consent is kept only when the person ticked it on the card: for every other request contact_prefs.consent.marketing is stored as false even when it was asserted, because an agent’s word, a form post or a text code is not the person’s own tick.
  • data.outcome (on lead.outcome_reported): { "outcome": "closed_won", "reason": "retained after consult", "value_cents": 250000, "source": "inbox", "reported_at": "…" }.
  • data.booking (on booking.*): { "appointment_id", "status", "slot_source", "starts_at", "ends_at", "tz", "platform", "external_url", "meeting_url" }.
  • data.call (on call.*): { "call_sid", "status", "duration_s", "dial_status" }.

Signature verification

The signature is the same scheme as every Inception Agents webhook: t=<unix seconds>,v1=<hex HMAC-SHA256 of "${t}.${rawBody}"> with your whsec_… secret. Verify the raw body before parsing.

Node.js receiver

import crypto from "node:crypto";
import express from "express";

const TOLERANCE_SECONDS = 5 * 60;

function verifySignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries((signatureHeader ?? "").split(",").map((kv) => kv.split("=", 2)));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const provided = Buffer.from(parts.v1 ?? "", "hex");
  const computed = Buffer.from(expected, "hex");
  return provided.length === computed.length && crypto.timingSafeEqual(provided, computed);
}

const app = express();
app.post("/webhooks/inception-leads", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf-8");
  if (!verifySignature(raw, req.headers["x-inception-signature"], process.env.INCEPTION_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  res.status(200).json({ received: true }); // acknowledge fast; retries are keyed on webhookId

  const event = JSON.parse(raw);
  if (await alreadyProcessed(event.webhookId)) return;
  await upsertCrmLead(event); // your CRM call — see the relays below
});

Path A: Zapier Catch Hook

  1. Trigger: Webhooks by Zapier → Catch Raw Hook. Use Catch Raw Hook (not Catch Hook) so the raw body is available for signature verification. Paste the hook URL as the subscription url above, kind lead.

  2. Action: Code by Zapier → Run JavaScript. Verify the signature and parse the body. Input data: raw = the raw body, sig = the X-Inception-Signature header, secret = your signing secret (store it in the Zap’s input, never in the code).

    const crypto = require("crypto");
    const parts = Object.fromEntries(inputData.sig.split(",").map((kv) => kv.split("=", 2)));
    const t = Number(parts.t);
    if (!t || Math.abs(Date.now() / 1000 - t) > 300) throw new Error("stale or missing timestamp");
    const expected = crypto.createHmac("sha256", inputData.secret).update(`${t}.${inputData.raw}`).digest("hex");
    if (expected !== parts.v1) throw new Error("bad signature");
    const event = JSON.parse(inputData.raw);
    output = {
      event: event.event,
      webhook_id: event.webhookId,
      ref: event.data.lead.ref,
      status: event.data.lead.status,
      intent: event.data.lead.intent,
      verbatim_ask: event.data.lead.verbatim_ask,
      agent_client: event.data.lead.provenance.agent_client,
      name: event.data.contact?.name ?? "",
      email: event.data.contact?.email ?? "",
      phone: event.data.contact?.phone ?? "",
      value_dollars: (event.data.lead.value_estimate_cents ?? 0) / 100,
      outcome: event.data.outcome?.outcome ?? "",
      booking_starts_at: event.data.booking?.starts_at ?? "",
    };
    

    A thrown error stops the Zap and shows up in Zap History; nothing reaches your CRM on a bad signature.

  3. Filter by event (lead.captured creates; lead.outcome_reported and booking.confirmed update).

  4. Action: your CRM. HubSpot → Create or Update Contact (email as the match key, ref in a custom property, agent_client in a custom property) or GoHighLevel → Add/Update Contact with the same mapping. Put verbatim_ask in the notes field so the first call opens already knowing the problem.

Path B: Make custom webhook

  1. Webhooks → Custom webhook. In the webhook’s advanced settings, enable “Get request headers” and set the data structure to a raw string (turn JSON pass-through off) so both the header and the raw body reach the next module. Use the webhook address as the subscription url.
  2. Tools → Set variable with a Crypto → Create HMAC module: algorithm SHA-256, key = your signing secret, data = {{t}}.{{rawBody}} where t is parsed from the x-inception-signature header (t=… is the first comma-separated part, v1=… the second). Follow with a Router / Filter whose condition is v1 equals the computed HMAC; the failing branch ends the scenario.
  3. JSON → Parse JSON on the raw body, then map data.lead.ref, data.contact.email, data.lead.verbatim_ask, data.lead.provenance.agent_client into your CRM module (HubSpot CRM → Create/Update a Contact, GoHighLevel → Create/Update Contact), a Slack message, or a Google Sheets row.

HubSpot and GoHighLevel via Zapier: the mapping that matters

Both platforms match on email. Map the rest so the lead is worth more than a form fill:

Envelope fieldHubSpot propertyGoHighLevel field
data.contact.name / email / phonefirst name, email, phonename, email, phone
data.lead.refcustom inception_ref (text)custom field Inception ref
data.lead.provenance.agent_clientcustom lead_source_agent (dropdown)tag agent:chatgpt
data.lead.verbatim_asknote on the contactnote / first message
data.lead.qualification.verdictcustom qualification_verdicttag
data.lead.value_estimate_cents / 100deal amountopportunity value
data.outcome.outcome (on lead.outcome_reported)lifecycle stage / deal stagepipeline stage

Route by agent_client and qualification.verdict; never paste them into copy the customer reads.

Close the loop

Outcomes are the reward signal. Record them where you already are:

  • In the inbox (Revenue › Leads): Accept / Reject / Booked / Won / Lost / Spam with a reason and a value.
  • By SMS: with a staff number configured and the 10DLC campaign verified, the team gets “New client? Reply Y or N” after each lead.
  • From your CRM: post the outcome back to POST /api/v1/leads/:ref/outcome ({ "outcome": "closed_won", "value_cents": 250000 }) from a workflow when the deal closes — or let a connected system of record report it.

Every recorded outcome fires lead.outcome_reported to your lead subscriptions, so a Zap can update the CRM stage even when the outcome was recorded elsewhere.

Test before you enable

  1. Create the subscription (it is active by default; PATCH { "active": false } while wiring).
  2. POST /api/v1/webhooks/subscriptions/:id/test — a lead subscription gets a synthetic lead.captured (reference IA-TEST00, contact test-lead@example.com); a booking subscription gets booking.confirmed with no contact. Pass { "event": "lead.outcome_reported" } or { "event": "call.completed" } to see the extra blocks.
  3. Confirm your Zap or scenario verified the signature and would have created the contact, then PATCH { "active": true }.

Path C: agent-driven setup

Connect the Inception Agents MCP server (mcp.inceptionagents.com) to your own Claude and ask: “create a lead webhook to my Zapier hook and send a test” — create_webhook_subscription with kind: "lead", send_test_webhook, list_webhook_deliveries. The same session can show the funnel (get_lead_funnel), list the leads (list_leads) and record an outcome (record_lead_outcome).