-
-
The 3-step wizard — nature, then script, then people. Each kind arrives with the calling policy that suits it.
-
The first-yes cascade: one freed slot, a waiting list called one at a time. The first yes ends it; nobody else is disturbed.
-
The same product in English — one button in the banner switches the screens, the agent's briefing, the spoken dates and the voice itself.
-
A real practice week: 86 appointments in blue, free slots in green. Every green square is an hour nobody is paying for.
-
Five kinds of campaign running side by side — each with its own script, its own closed set of outcomes, and its own progress.
-
One campaign, contact by contact: accepted, cancelled, and one excluded — never dialled, because they asked not to be called.
-
Phone numbers are masked everywhere, "do not call again" is honoured, contacts without a number are flagged rather than dialled.
Inspiration
For a solo professional — a physiotherapist, a hairdresser, a music teacher — a missed appointment isn't a statistic. It's an empty chair and a day's income gone. A cancellation at 9 a.m. for a 2 p.m. slot is worse: somebody on the waiting list would gladly take it, but calling twelve people one by one, during working hours, between two clients, simply never happens.
They don't need a dashboard. They need someone to pick up the phone.
What it does
RingBack is a small web app you run on your own machine. You give it a situation; it runs a calling campaign and brings back structured answers.
- Five kinds of campaign, each with its own script and its own closed set of outcomes: freed slot, appointment reminder, confirmation, rescheduling, booking. Five, not more, and that is the point: every one of them writes its answer back into the appointment book. Three earlier kinds were cut for failing that test — a campaign whose result has nowhere to go is a demo, not a tool.
- A guided setup on the first run — the same settings as the ⚙ Settings page, one page at a time, in an order that makes sense, with a red ✗ per part until it is done and a green ✓ once it is. Nothing new to learn, and skippable.
- Three steps to launch: pick the situation → the message writes itself (and stays editable) → load the people (paste, CSV, ICS calendar, or straight from your own planner and client list).
- The planner is a launch pad. Click a free slot → a "freed slot" campaign is half-written. Click an appointment → move it or cancel it, and the app offers the campaign that refills the place. No call ever leaves from those clicks.
- Nothing is forgotten. Every call that doesn't conclude schedules a follow-up that keeps its campaign's script and settings — due in working hours only, attempts counted, deadline editable. Follow-ups never fire on their own: a human presses the button.
- Every answer comes back structured, is validated before anything is written, and updates the appointment book by itself: a rescheduled date lands in the planner, a filled slot closes the campaign, a refusal moves to the next person.
- Phone numbers are masked everywhere — screen, logs, audit trail
(
+33 6 •• •• •• 42). Enforced in one place, asserted throughout the test suite.
The signature move: the first-yes cascade
A slot frees up at 11:20. RingBack calls the waiting list one person at a time, in order. The first person who says yes gets the slot — and the cascade stops dead. Everyone below is marked spared — never called, and a dial counter in the test suite proves their number was never composed.
Somebody who says "yes, but another day"? They get their own date, and the hunt for a taker of the original slot continues. Nobody says yes? The screen says so, plainly, instead of pretending.
A web form cannot do this. A bulk SMS cannot do this. It needs a caller who knows when to stop — which is exactly what a phone agent is for.
How it uses CALL-E
RingBack talks to the CALL-E REST API directly with urllib — no SDK, no dependency.
POST /v1/callscarries a three-part briefing, and the split matters:- the opening, spoken word for word (who is calling, and why);
- the goal, the useful facts and the constraints — between parts 1 and 3 the agent is free to converse, answer questions, repeat a date, reassure;
- the closed outcomes it must come back with, and nothing else.
- A strict
result_schemaandrecipient_result_schema:appointment_status(confirmed | rescheduled | canceled | to_reschedule),new_datetime(ISO 8601, and it must be null when nothing was agreed),notes. The cascade uses its own:outcome(accepted | refused | moved | to_reschedule), same discipline. to_rescheduleis the interesting one. It means "he wants to move but no date was agreed" — the answer a form can't express and a human secretary writes on a post-it. In RingBack it is a first-class outcome that feeds the follow-up scheduler.- The result is re-validated locally against the same schema before a single row is written. A malformed answer never touches the appointment book.
CALL-E does the one thing only it can do — hold the conversation. RingBack supplies everything around it: who to call, in what order, what to say, when to stop, what to write down, and what to do next.
It really calls, and it really works
This is not a mock-up wired to a simulator. Real calls have been placed to a real French mobile through CALL-E. On one of them the agent proposed a new slot, the person asked it to repeat the date, the agent repeated it, the person accepted — and the new appointment appeared in the planner, by itself, at the right place.
Two lessons made the product better:
- An agent that only reports is useless — it must be able to answer. The briefing was rewritten so the agent may talk freely between the opening and the outcome. And all five scripts now end on a question, because a text ending on an explanation leaves a silence, while a question gets the answer the agent needs to conclude.
- A failure on our side must never be blamed on the client. One day the API key was wrong, and RingBack marked people "unreachable — attempt limit reached". Three lies at once: nobody had been called, no attempt had happened, and the fault was ours. So failures are now told apart, and they behave differently:
| The contact's failure | Our failure | The call went out, the result didn't come back | |
|---|---|---|---|
| e.g. | no answer, voicemail | key refused, credit exhausted, service down | read timeout, server hung up |
| attempt counted | yes | no | no |
| the contact | eventually "unreachable" | untouched — still "to call" | "called, result unknown" |
| the campaign | carries on | pauses, resumes as it was | pauses, and never calls twice |
The line between the last two columns is the moment the call is created. Before it, nothing left. After it, the call id is kept — and a "fetch pending results" button goes and reads the outcome later, applying it exactly as if it had arrived on time. That button dials nothing; it only knows how to read.
Safe by default, on purpose
- Simulation is the default. No key, no network, scripted conversations, fictional numbers. You can explore the whole product without a single real call.
- Three independent locks to go real, none of them reachable from the web
interface: the
CALLE_API_KEYenvironment variable, a--appels-reelsflag at startup, and the wordAPPELERtyped at a prompt — asked again at every launch, never remembered. Miss one and the server quietly stays in simulation. - A politeness guardrail: outside the configured calling window, every launch is refused, with the reason.
- Do-not-call is honoured absolutely: a flagged number is never dialled — not from a campaign, not from a cascade, not even hand-pasted into a list.
- Every real call leaves one audit line (timestamp, masked number, status).
- No click in the planner or the client list ever places a call. They prepare a campaign; a human starts it.
How I built it
- Python 3.12, standard library only. No framework, no
pip install, nothing to set up.sqlite3for storage,http.serverfor the UI,urllibfor the API,unittestfor the tests. It runs on a plain laptop and the client data never leaves it. - 1202 unit tests, all green — including the real client driven against a faithful local fake of the CALL-E API, which proves that while the locks hold, zero network requests leave the machine.
- An end-to-end bench: 661 checks across 121 combinations of campaign kind × starting point × call outcome, verifying both what is written in the database and what becomes visible on screen. It runs on a throwaway database, refuses to open the real one, and produces a byte-identical report from one run to the next.
- Additive-only migrations: an existing database gains new tables, never a rewrite.
- Fully bilingual, French and English — one button in the banner. The product is
written in French, because its users are French solo professionals; the English is
applied to the finished page, at the two lines where HTML becomes bytes. In French
the translation layer returns the page untouched, so the original product cannot
regress — a test asserts that with
is, not==. Everything follows the switch, not just the screens: the agent's briefing sent to CALL-E, the spoken dates, the voice itself (region: "GB",locale: "en-GB"), the demo data set — 75 English names and 99 English reasons — and the error messages. Set it to English and the whole thing, phone call included, is in English. The demo video is narrated in English.
Challenges
The hard part was never the API call. It was everything around a machine that speaks to a human being on your behalf:
- Saying "I don't know" instead of guessing. The three-column table above is the whole design philosophy: a product that reports a state it cannot verify is worse than one that stops.
- Making an agent that converses without going off-script. Too rigid and it answers "I'd rather not say something wrong" and hangs up — which is exactly what happened before the briefing was split in three.
- Knowing when not to call. The spared people in a cascade, the do-not-call flags, the closed days, the politeness window: most of the work is about calls that must never happen.
What's next
- Connect the appointment book to Google Calendar and Outlook, both ways.
- Let the operator listen back to the recording next to the transcript.
- Give the booking campaign the same negotiation method the other four already have: today it offers dates without a funnel or a fallback.
Built with
Python 3.12 (standard library only: sqlite3, http.server, urllib, csv,
email, unittest) · CALL-E REST API (POST /v1/calls, GET /v1/calls/{id},
result_schema + recipient_result_schema) · SQLite · no external assets, no CDN,
no dependency.
Log in or sign up for Devpost to join the conversation.