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
- 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.
- 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.
- 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.
- 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.
- 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
unknownvalue (interest_level,timeline_urgency,budget_clarity,decision_maker,wants_booking_link,reached_lead…), each carrying its selection rules indescription. 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_URLis HTTPS) CALL-E posts terminal webhooks. The receiver treats them as untrusted — CALL-E webhooks are unsigned — so it serves on an unguessable path, requiresCALL-E-Event-Idto match the body, claims the event id before any side effect, and re-fetchesGET /v1/calls/{id}and applies that snapshot. Running locally with no public URL, the backend polls the same endpoint instead. Both call the same idempotentapply_call_snapshot. - Honest failure states.
failure_codehas 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 asresult_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.envand 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
Log in or sign up for Devpost to join the conversation.