Automates FedEx AVR call workflows through Telegram using C#/.NET 10 and Twilio Programmable Voice.
The bot runs predefined FedEx AVR DTMF flows so you spend less time in phone menus.
Telegram commands
| Command | Purpose |
|---|---|
/start |
Welcome message and reply keyboard |
/departing |
Terminal departure flow |
/spot |
Customer/spot departure flow (needs pickup trailer saved from a prior /departing) |
/arriving |
Terminal arrival flow |
/transcript |
Last saved transcript (and call type / time) for this user |
/recording |
Toggle capture + Gladia + Telegram WAV attachment for new calls (on, off, or status) |
/setfedexid <id> |
Save or update FedEx ID |
- Collects required values per flow in Telegram (
ConversationService+ prompts). - Persists per-user data to JSON (
Storage:UsersFilePath, defaultData/users.json). - Departing asks for: FedEx ID (if not already saved), truck number (or reuse), spot number, pickup trailer (stored for later
/spot), then empty trailer. The DTMF sequence uses truck, FedEx ID, spot, and empty trailer; pickup trailer is persisted for the spot flow, not dialed in this flow. - Spot reuses truck when within the TTL and uses the saved pickup trailer from profile. If no pickup trailer exists, the bot tells you to run
/departingfirst. - Truck reuse: if
TruckNumberValidUntilis in the future, truck is reused for/departing,/spot, and/arriving. The 11-hour window is refreshed when a/spotcall starts after you enter a truck number. - DTMF timing: Twilio
sendDigitspauses usew(0.5s each). After a menu key (single IVR digit), the next segment is preceded by**w** (0.5s). After a data entry segment (truck/FedEx ID/spot/trailer/100#, etc.), the next segment is preceded by**ww** (1s). Steps are explicit inBuildSendDigits/JoinDtmfStepsinCallService. No leading pause before the first keypress. Seeexamples/twiml-play-digits-example.xmlfor sample TwiML and digit strings. - Arriving flow end:
/arrivingnow appends a final menu key9so the call exits cleanly after the arriving path. - Twilio Media Streams (
wss→/twilio/stream) are only started in TwiML when/recordingis on; the WebSocket rejects streams for calls without capture. Inbound audio is captured as μ-law, transcribed via Gladia, and (when recording is on) sent to Telegram as a WAV after the outcome (skipped if the file would exceed Telegram’s size limit). - Telegram UX: While a call is active, one chat message is created and edited in place (
editMessageText) as Twilio status (and stream connect) lines arrive—same mechanism as any bot “live” refresh, not a separate AI API. When the call finishes, that same message is edited again into the final HTML outcome; the WAV (if any) is a follow-up document. If the call ends before any non-terminal status (e.g. busy), the bot sends only the outcome message. - Gladia: with capture on, the app prefers live transcription (
Gladia:UseLiveStreaming) via Gladia Live; if live setup fails, it falls back to buffering μ-law and uploading WAV for batch transcription. Stream auth usesTwilio:StreamAuthTokenif set, otherwiseTwilio:AuthToken. - Outcome is computed after the call ends (Twilio terminal status + stream stopped when capture is on): required phrase per flow (see
Outcome:*RequiredPhrase), then failure keywords, then success keywords in the tail of the transcript, then status/duration heuristics (CallOutcomeEvaluator). - Recording defaults to on at process start;
/recordingonly affects new calls. **GET /health** returns JSON: Twilio config/callback URL, recording toggle, Gladia readiness (API key + base URL via batch transcriber), Telegram connection state.
- Telegram long polling (
TelegramPollingService) handles commands and multi-step prompts. - UserStore reads/writes JSON profiles.
- CallService creates outbound calls, serves TwiML, handles
/twilio/statusand the media WebSocket, runs transcription and outcome evaluation. - GladiaLiveTranscriber / GladiaTranscriber implement live vs upload transcription paths.
See PLAN.md for DTMF step tables and persistence detail.
- .NET 10 (ASP.NET Core minimal API)
Telegram.Bot(long polling)- Twilio Programmable Voice (REST + TwiML + Media Streams)
- Serilog
- JSON file persistence
FedExAvrBot/Program.cs— bootstrap, DI,MapAppEndpointsFedExAvrBot/Endpoints/AppEndpoints.cs—/,/health, Twilio routesFedExAvrBot/HostedServices/TelegramPollingService.cs— Telegram botFedExAvrBot/Services/CallService.cs— calls, TwiML, streams, DTMFBuildSendDigits/JoinDtmfStepsexamples/twiml-play-digits-example.xml— sample<Play digits="…"/>TwiML and pause rulesFedExAvrBot/Services/ConversationService.cs— in-memory prompt stateFedExAvrBot/Services/CallOutcomeEvaluator.cs— outcome rulesFedExAvrBot/Services/GladiaTranscriber.cs/GladiaLiveTranscriber.csFedExAvrBot/appsettings.json— defaults (override with user-secrets or environment variables)
Use user-secrets or environment variables for secrets.
Twilio:AccountSidTwilio:AuthTokenTwilio:FromNumberTwilio:FedExNumber— required; destination number for the FedEx AVR IVR. Treat as a secret (do not commit); set via user-secrets or environment (e.g. Linux:Twilio__FedExNumber).Twilio:PublicBaseUrl— public HTTPS base URL for webhooks (e.g.https://example.com)Twilio:StreamAuthToken— optional; if set, must match thetokenparameter on the stream (otherwiseAuthTokenis used)
Gladia:ApiKeyGladia:ApiBaseUrl(defaulthttps://api.gladia.io)Gladia:TimeoutSecondsGladia:PollIntervalMsGladia:UseLiveStreaming— use Gladia Live WebSocket when capture is enabled (defaultfalseinappsettings.json)Gladia:EnableLiveAudioEnhancer— live session pre-processingGladia:LiveSpeechThreshold— 0–1, live pre-processing
Outcome:MinCompletedDurationSecondsOutcome:DepartingRequiredPhrase/SpotRequiredPhrase/ArrivingRequiredPhrase— if the normalized transcript contains this phrase, outcome is CERTAIN_SUCCESSOutcome:SuccessKeywords— matched near the end of the transcript if no failure matchOutcome:FailureKeywords— any match → LIKELY_FAILED
Bot:TelegramToken
Storage:UsersFilePath
Set secrets in the service environment (not in files committed to git). ASP.NET Core uses double underscores for nested keys, e.g. Twilio__FedExNumber, Twilio__AccountSid, Bot__TelegramToken. Example: EnvironmentFile=/etc/fedexavr.env with Twilio__FedExNumber=+1… in that file (mode 600, root-owned).
- Install .NET 10 SDK.
- Configure Twilio, Telegram, and (if using capture) Gladia.
- Run:
dotnet run --project FedExAvrBot- In Telegram, use
/startor a flow command.
GET /health — JSON with twilio, recording, transcription (Gladia), and telegram sections.
- Menu logic is tuned for the current FedEx AVR IVR (Mar 2026); if prompts change, update
BuildSendDigits/JoinDtmfStepsinCallService.cs. - Twilio Media Streams error
31920usually means the WebSocket handshake to/twilio/streamdid not complete (101); check stream auth (401/ policy violation) and reverse-proxy WebSocket settings. ops/fedexavr-site.confandscripts/deploy-server.ps1use placeholder hosts where appropriate; adjust for your domain.