Inspiration
A small manufacturer or retailer issues a corrective notice, and someone has to phone every affected customer. Read the approved wording. Find out whether they still have the item. Explain the return options. Write down what each person said.
Today that is a spreadsheet and a phone. Afterwards, nobody can answer the questions that actually matter: who was reached, who understood the instructions, who still has the item, and which returns are genuinely pending. A generic robocall cannot help, because it cannot answer a question, cannot tell a voicemail from a person, and cannot produce a reliable outcome.
What RecallRoute does
A coordinator loads the wording their organisation approved, imports a small affected-customer list with the record that authorises each contact, and runs a preflight check. RecallRoute then places real CALL-E calls and returns structured, per-recipient outcomes: was this the right person, do they still have the item, did they understand the notice, which return step did they choose, when suits them, and what did they ask that we could not answer.
The part we actually care about
Recall software is very easy to make dishonest. Put "87% contacted" on a dashboard and everyone relaxes. RecallRoute cannot produce that number.
The dashboard reports five separate measures, each with its own definition and denominator: call completed, right person reached, notice understood, next step selected, and independently resolved. They are never merged. The headline states the three most important ones in prose so the honest reading is unavoidable.
The last measure — independently resolved — is reachable through exactly one code path, which
requires a named human and a note describing how they know the item physically came back. No
call result, no matter how confident CALL-E is about it, can set it. There is a test that
drives a maximally confident provider result (contact_outcome: resolved, certainty: high,
task_completed: true, confidence 0.99) and asserts the customer is recorded as contacted,
not resolved.
Safety, because this is a recall
- RecallRoute never writes recall language. Every sentence about the product comes from organisation-approved wording, versioned and attributed to a named approver.
- It never gives safety advice. The assistant is forbidden from telling anyone to inspect, open, repair, transport, or dispose of an item, or from assessing how dangerous it is.
- It answers only pre-approved questions. Everything else — injury, compensation, legal, or simply unanticipated — ends the call politely and becomes a human follow-up. There is no improvisation path.
- Voicemail says less. A separate minimal message is left on machines, and preflight warns when that message names the product, because you cannot know who plays it back.
- No customer name, order number, or serial number is ever spoken. A wave is dispatched as one CALL-E batch, so the task text is shared and structurally cannot contain them. The assistant asks the person to confirm who they are instead of asserting it.
- Consent, quiet hours, retry caps, opt-out, blocklists, and a kill switch are enforced server-side, re-evaluated at the instant of launch rather than trusted from the client.
How we built it
Next.js 15, TypeScript, Tailwind 4, PostgreSQL via Drizzle, and a reconciliation worker.
The CALL-E integration is the spine, not a feature:
POST /v1/callswith a stableIdempotency-Key, explicit E.164 recipients with locale and region hints, a strictrecipient_result_schema(additionalProperties: false, every field required), correlation metadata carrying no customer identity, and a deployedwebhook_url.- The real
call_idis persisted before anything is shown as started. /api/webhooks/callededuplicates onCALL-E-Event-Id, then re-fetches the call with the server's own key. The webhook body is treated as untrusted input; the fetched snapshot is what changes a customer's status. A test sends a body claiming success while the API says the call failed, and asserts the API wins.- A reconciler polls
GET /v1/calls/{call_id}for open waves, so a lost webhook costs latency rather than truth. - Correctness is enforced by database constraints, not conventions: unique
(campaign, phone)so nobody is called twice, unique idempotency key and provider call id per wave, the event id as a primary key so replays are no-ops, and unique(outcome, reason)so a retried webhook cannot queue duplicate human work.
There is no mock provider and no simulated-success mode. Without credentials the app blocks
launching and /api/health returns 503. Tests stub the HTTP boundary only; nothing in the
runtime can reach that stub.
Challenges
Batching versus correlation. Dispatching a wave as one CALL-E batch is the natural fit for the API, but the only join key that comes back per recipient is the phone number. We made that safe by enforcing phone uniqueness within a campaign as a database constraint, then matching on normalised digits with a dispatch-order fallback. The reward was unexpected: a shared task text cannot contain one customer's personal details, which turned a correlation problem into a privacy guarantee.
Deciding what "unknown" means. The schema lets CALL-E say unknown, and it should. The
work was making sure unknown is never quietly read as agreement anywhere in the funnel, the
recipient status machine, or the export. A completed call with a certainty: unknown result
counts toward call completed and nothing above it.
Not flooding the human queue. An early version sent every voicemail to a human, because a
call that reached nobody honestly reports certainty: unknown. We had to separate genuine
ambiguity from a clean miss: right_person_reached: no is a clean miss and belongs in not
reached; only unknown goes to a person.
What we learned
Building the honest version was mostly an exercise in refusing to write convenient code. The
tempting move — one contacted boolean, set from a completed call — would have been a fraction
of the work and would have made the product worthless to the only user who matters: the person
who has to sign off that the recall was actually carried out.
What's next
Return-logistics confirmation (so independently resolved can be fed by a scan rather than typed), multi-language campaigns using CALL-E's locale hints, and a supervisor view across several concurrent notices.
Built With
- call-e
- typescript
Log in or sign up for Devpost to join the conversation.