Inspiration
When a flood hits, the people who need help most are often the ones least able to fill out a form. Water is rising, the power is out, the phone battery is low, and mobile data barely works. Meanwhile, rescue coordinators receive reports from everywhere at once (phone calls, messages, drone footage) with no single place to see them.
We wanted to build something that meets victims where they are. Anyone who can make a phone call should be able to ask for help, and every report, no matter how it arrives, should land as a pin on one dispatcher map.
What it does
FloodOps is a flood-rescue coordination prototype with three ways to report an emergency and one place to act on them:
- Call an AI rescue line. The victim dials a real phone number. An AI dispatcher answers, calmly asks for the exact location, the number of people, injuries and the water situation, and repeats the address back to confirm it. When the call ends, the case appears on the map as a critical (red) marker with the caller's number ready for a call-back.
- Web report form. Citizens or bystanders can submit a report with GPS location (or a typed address), the number of people, a call-back number, a description and photos.
- Drone observations. The backend accepts aerial images with optional GPS coordinates and queues them for human review.
- Dispatcher map. Responders see every case colour-coded by source, search by address, phone or case ID, call victims back with one tap, and mark cases as rescued. The map refreshes on its own every 10 seconds.
A human dispatcher always makes the final call. The AI only collects and structures information.
How we built it
The flow has four steps:
- A caller dials our Photon number. Photon forwards the call over SIP to a Retell voice agent.
- When the call ends, Retell sends a signed
call_analyzedwebhook to our FastAPI backend. - The web form and drone uploads reach the same backend, which stores everything in SQLite (
Incident,IncidentEvent,Evidence). - The React dispatcher map reads from the backend and shows every case in one place.
- Frontend: React + Vite. A small
api.jslayer talks to the backend, and Vite proxies/apiso the browser only ever sees one origin. - Backend: FastAPI + SQLModel on SQLite. Every status change is written to an
IncidentEventhistory table. - Voice: Photon gives us the phone number and forwards inbound calls over SIP to a Retell voice agent. We designed the agent's post-call extraction schema to match our database columns exactly (
address_text,people_count,description,phone), so the webhook can store the result without any translation layer. - Webhook security: Retell signs each event. We verify it before trusting the payload:
$$ d = \mathrm{HMAC}{\text{SHA-256}}\big(k,\; b \,\Vert\, t\big), \qquad |t{\text{now}} - t| \le 5\ \text{min} $$
where \(k\) is our API key, \(b\) the raw request body and \(t\) the timestamp. Anyone can find a public URL, but only Retell can produce a valid \(d\), and old requests can't be replayed.
- Idempotency: Retell retries a webhook up to 3 times if it doesn't get a
2xxwithin 10 seconds. To make sure one call never becomes two markers, the incident ID is derived from the call itself rather than generated randomly:
$$ \texttt{id} = \texttt{"CALL-"} \,\Vert\, \mathrm{SHA1}(\texttt{call_id})_{[0:8]} $$
- Geocoding: callers speak addresses, but maps need coordinates. We acknowledge the webhook immediately and geocode the spoken address with OpenStreetMap Nominatim in a background task, so we stay well inside Retell's timeout.
Challenges we ran into
- Polling vs. webhooks. Our first version asked Retell "any new calls?" every few seconds. It worked, but it wasted thousands of API calls a day for calls that never happened. We switched to webhooks so that Retell tells us when something happens.
- Getting the phone line connected. Retell's "Test SIP connection" returned
405 Method Not Allowed, which looked like a failure but only meant the outbound check wasn't supported. Inbound calls worked fine. At one point a test call reached a real person instead of our AI. Photon's call log showed zero inbound calls, which told us the call had never reached our line at all. - Debugging the webhook end to end. We went through
404(the webhook URL was missing its path), then503(the API key file had been typed but never saved), before finally getting200 OK. ngrok's request inspector atlocalhost:4040became our best debugging tool. - Honest UI. Our first demo screen said "AI is listening and analyzing in real time", but Retell analyzes calls after they end. We rewrote the demo page so it only shows what the system actually does.
- Messy spoken addresses. Callers often skip the city ("4622 South Austin Street"), which makes geocoding ambiguous. A case without coordinates is still created, because a call for help must never be dropped, and it remains searchable until a dispatcher calls back.
What we learned
- How webhooks really work. The provider triggers the event, the webhook is just the delivery mechanism, and our endpoint is a mailbox anyone can reach, so signatures matter.
- How a phone call travels from the phone carrier to Photon, over SIP to the Retell agent, and finally to our backend, and which side has to be configured for each hop.
- Designing for retries and timeouts (idempotent IDs, background work) instead of hoping every request arrives exactly once.
- Small integration details (matching extraction fields to database columns, an
.env.examplefor teammates, keeping secrets out of git) save more time than clever code.
What's next
- SMS confirmations through Photon: after a call, text the victim their case ID, and text them again when a rescue team is on the way.
- A real map (Leaflet) instead of our placeholder, plus photo previews for dispatchers.
- Show an "incoming call" card the moment a call starts, before the analysis is ready.
- AI-assisted triage of drone images, always with a human confirming before a team is sent.
FloodOps is a hackathon prototype, not an operational emergency service. In a real emergency, call your local emergency number.
Built With
- fastapi
- javascript
- react
- react-routerpython
- retell-ai
- sip
- sqlite
- sqlmodel
- uvicornphoton
- vite
Log in or sign up for Devpost to join the conversation.