We will be undergoing planned maintenance on Oct 7th 6:00AM UTC / Oct 7th 2:00AM ET

Inspiration

Small service businesses — contractors, clinics, law firms, agencies — buy leads all day and call them back hours later. By then the homeowner has already talked to the competitor who picked up first. The fix everyone knows is "call back in the first minute", and nobody can staff it.

That is phone-shaped work with a hard deadline, which is exactly what an outbound voice agent is for.

What it does

  1. A lead fills in any web form — LeadPulse's hosted form, or Typeform, Webflow, HubSpot or plain HTML pointed at one webhook URL. The form records explicit consent to a call and text.
  2. LeadPulse immediately asks CALL-E to call them. The lead's card moves from Pending to Calling on a live pipeline as soon as CALL-E accepts the call — about four seconds in the demo.
  3. Riley, the business's virtual assistant, introduces itself as an assistant, confirms the project, and weaves the business's own qualification questions (budget, timeline, decision-maker, goal) into a natural conversation. It never quotes prices or pushes.
  4. CALL-E returns a schema-validated result. LeadPulse turns it into a 0–100 score, moves the card to Qualified, flags hot leads, texts a pre-filled Cal.com booking link if the lead asked for one, and posts a Slack alert for scores ≥ 80.
  5. The lead page shows the full transcript, budget, timeline, decision-maker, CALL-E's own evidence and confidence, and the speed-to-lead. Analytics tracks the funnel and average response time.

How I built it

CALL-E is the load-bearing wall. One POST /v1/calls per lead carries a natural-language task built from the business's settings and the form answers, an explicit E.164 recipient, a strict result_schema, correlation metadata, and a stable Idempotency-Key (leadpulse:lead:<id>:qualify:v1) so a retried trigger can never dial a lead twice.

  • The schema is the contract. Every business decision is a string enum with an unknown value (interest_level, timeline_urgency, budget_clarity, decision_maker, wants_booking_link, reached_lead …), each carrying its selection rules in description. Only features CALL-E supports are used, and a test walks the schema to keep it that way.
  • The score is computed, not guessed. LeadPulse never asks a model for a number. It scores CALL-E's validated enums with a fixed rubric — interest 35, timeline 25, budget 20, decision-maker 15, sentiment 5 — so the same answers always give the same score and every point is explainable. A lead that was never actually reached gets no score at all and lands in No Answer.
  • Two delivery paths, one interpretation. In production (BASE_URL is HTTPS) CALL-E posts terminal webhooks. The receiver treats them as untrusted — CALL-E webhooks are unsigned — so it serves on an unguessable path, requires CALL-E-Event-Id to match the body, claims the event id before any side effect, and re-fetches GET /v1/calls/{id} and applies that snapshot. Running locally with no public URL, the backend polls the same endpoint instead. Both call the same idempotent apply_call_snapshot.
  • Honest failure states. failure_code has no published enum, so a failed call is shown as failed with CALL-E's raw reason — never guessed into "no answer". A completed call with no schema-valid result is recorded as result_validation_failed, not as a qualified lead.
  • Dial safety. Numbers are normalised to E.164 and must be on CALLE_DIAL_ALLOWLIST; anything else is recorded as not dialed with the reason. Numbers are masked in logs. The API key lives only in the backend .env and is never sent to the browser.
  • Runs anywhere. Supabase (Postgres + Realtime) when configured, a local SQLite file otherwise — so the whole loop runs on a laptop with nothing but a CALL-E key.

Stack: CALL-E Developer API · FastAPI · Next.js 14 (Apple-style UI, Tailwind) · Supabase or SQLite · Twilio SMS · Cal.com · Slack · Polar.sh (paywall with admin bypass) · Remotion for the video.

Challenges I ran into

The first draft was written against a CALL-E API that doesn't exist — api.calle.ai, a skill field, a hand-rolled callback payload. It compiled, and it could never have placed a call. I rewrote the integration against the published OpenAPI contract (0.7.0), and the real API turned out to be better than the imagined one: result_schema meant the app didn't need a second model to read transcripts.

I verified the real integration without dialing anyone: the key is accepted by GET /v1/goals, and CALL-E validates LeadPulse's exact task and result_schema on POST /v1/calls — a deliberately broken schema is rejected with result_schema_invalid, while ours passes and is stopped only by an intentionally invalid phone number. [After your first real call, replace this sentence with what you observed: time to first ring, time to result, and whether any schema field came back unknown.]

Accomplishments I'm proud of

  • A complete loop — form → call → validated result → score → SMS → Slack → analytics — in under a minute, with every state visible live on one board.
  • The same qualification questions a business types in Settings go straight into the call task.
  • 11 offline, credential-free backend tests with CALL-E faked at the HTTP layer: the request contract, the schema staying inside CALL-E's supported subset, the scoring rubric, the allowlist refusal, polling, and the webhook trust boundary (mismatched event id, a forged body ignored in favour of the re-fetch, duplicate delivery).

What I learned

Speed-to-lead is not an AI problem, it's a latency problem — the AI only has to be good enough to not lose the lead in the first ninety seconds. Designing for that meant keeping the call short, honest ("I'm the company's virtual assistant"), and ending in one concrete next step: a booking link.

What's next

  • Two-way SMS follow-up when a lead doesn't answer, and a second call attempt at a better time.
  • CRM write-back (HubSpot, Pipedrive) of the transcript and score.
  • Per-industry question templates (legal intake, real estate, clinics).

Built With

  • call-e
Share this project:

Updates

Submission history