LiteBot

Auto-dialer

This chapter explains outbound calling campaigns: you upload a CSV of numbers, choose a bot, and LiteBot asks the VEXYL Voice Gateway to call each number, tracks every call and retries the ones nobody answered. Read it before you run reminder, follow-up or survey calls. The gateway itself places the calls; its side is in the VEXYL Voice Gateway manual, chapter Outbound SIP and per-call settings.

How it works

1. You create a campaign      -> bot, gateway endpoint, call settings, retry policy
2. You upload a CSV           -> one contact per row, columns mapped to field names
3. You press Start            -> contacts go into the dial queue
4. LiteBot calls the gateway  -> POST <endpoint> (the gateway's /outbound/initiate)
5. The gateway rings, answers -> the chosen bot talks to the person
6. The gateway calls back     -> answered, then completed or failed, with the transcript
7. LiteBot records the result -> retries no-answers, forwards results to your webhook

The number of simultaneous calls is the campaign's concurrency. With call tracking on, a slot stays busy until the call really ends.

Enable the module

Setting Where Value
ENABLE_AUTO_DIALER LiteBot .env true. Off, the /api/campaigns routes do not exist
ENABLE_MESSAGE_QUEUE LiteBot .env true. Campaigns cannot start without it, and running campaigns are not recovered after a restart
VITE_ENABLE_AUTO_DIALER Dashboard build true shows the Auto Dialer page. It is fixed when the dashboard bundle is built into the binary; you cannot change it in .env
AUTO_DIALER_PUBLIC_URL LiteBot .env Public base URL the gateway can reach, for call tracking (see Call tracking)

Restart LiteBot after changing .env. If your binary's dashboard was built without the auto-dialer page, the API still works when ENABLE_AUTO_DIALER=true.

The other variables:

Variable Default Meaning
AUTO_DIALER_OUTBOUND_ENDPOINT unset Last-resort gateway endpoint (see Gateway endpoint)
AUTO_DIALER_WEBHOOK_SECRET JWT_SECRET Key that signs callback URLs and checks the legacy webhook
AUTO_DIALER_INITIATE_TIMEOUT_MS 90000 How long to wait for the gateway to answer a call request. Must exceed the carrier's ring time
AUTO_DIALER_MAX_CALL_SECONDS 900 How long a slot waits for the final callback before it is freed
AUTO_DIALER_FAILURE_TTL 86400 Seconds before the consecutive-error counter expires

Who can use it

A campaign belongs to its bot's tenant. Routes need the campaigns permission: read to view, create to add, update to edit, upload, map, test-call, start, pause, resume and stop, delete to remove. The admin and tenant-admin roles have it; the user role does not. Users outside the global admin role see only their tenant's campaigns, and another tenant's campaign answers 404. A global admin sees all, or one tenant's with the X-Tenant-Id header. Changing the default endpoint and the voice catalog needs the admin role. See Users, roles and tenants.

Campaigns created before multi-tenancy are scoped through their bot until you stamp them. The migration is a Node.js script that ships with the LiteBot sources, not inside the binary; run it from a source checkout with its node_modules installed and a .env next to scripts/ that has MONGODB_URI and MONGODB_DB_NAME, dry run first:

node scripts/migrate-campaign-tenants.js
node scripts/migrate-campaign-tenants.js --apply

Gateway endpoint

The gateway URL, for example https://gateway.example.com/outbound/initiate, is looked up each time a call is placed. The first one set wins:

  1. The campaign's own Gateway endpoint (optional, in the campaign form).
  2. Settings > Outbound Dialer in the dashboard (stored in MongoDB auto_dialer_settings).
  3. AUTO_DIALER_OUTBOUND_ENDPOINT.

A campaign with none of the three cannot start, resume or place a test call: No outbound endpoint: set one on the campaign, in Settings > Outbound Dialer, or AUTO_DIALER_OUTBOUND_ENDPOINT. A settings change applies to running campaigns from their next call.

curl -s -X PUT http://127.0.0.1:3345/api/campaigns/settings \
  -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
  -d '{"outboundEndpoint":"https://gateway.example.com/outbound/initiate"}'

An empty string clears the saved value. GET /api/campaigns/settings returns outboundEndpoint, envOutboundEndpoint, effectiveOutboundEndpoint and effectiveSource (settings or env).

Create a campaign

The dashboard form has four steps. Only a campaign in draft can be edited, take a CSV or be started.

Step What you set
1 Campaign Name, description, bot, gateway endpoint, simultaneous calls (1 to 50; keep within your SIP trunk's channels)
2 Calls Engine, language, speech recognition, voice provider, model and voice, opening, retry policy, results webhook
3 Contacts Upload the CSV, choose the phone column, map the other columns to field names
4 Review Check everything. Calls start only when you press Start

The call settings live in callConfig:

Field Values Default Meaning
realtimeProvider "", pipeline, litebot, gemini-live "" Engine: "" uses the gateway's own setting; pipeline forces speech-to-text, bot, text-to-speech; litebot runs the call on LiteBot's /voice (the gateway needs LITEBOT_LIVE_URL); gemini-live is run by the gateway. The form offers the first three; gemini-live is API only
language Locale such as ml-IN, en-IN ml-IN Call language
stt_provider "", sarvam, sarvam-r, groq, gemini, deepgram, openai, google, vexyl-stt "" Speech recognition. "" is the gateway default
tts_provider sarvam, google, gemini, elevenlabs, deepgram, azure, murf, vexyl, smallest, openrouter sarvam Voice provider
tts_model, tts_voice Provider ids "" Model and voice for that provider. "" is the gateway's default
greetingMode static, dynamic, hybrid, wait static How the call opens (see below)
initialGreeting Text "" Spoken greeting for dynamic
audioFile URL or path of a WAV "" Recording for static and hybrid. Empty plays the gateway's default for the language
webhookUrl URL "" Your receiver for call results
Greeting mode Form label What the person hears
dynamic Spoken greeting Your greeting text, then the bot
static Recording only A recording. Fastest to start
hybrid Recording, then bot A recording, then the bot
wait Wait for caller Silence until the person speaks

With litebot or gemini-live, LiteBot leaves out the speech recognition, voice, greeting and recording fields, because the realtime engine handles speech itself.

tts_model and tts_voice must match ^[A-Za-z0-9_.:-]{1,64}(/[A-Za-z0-9_.:-]{1,64})?$ and ^[A-Za-z0-9_.:-]{1,160}$. The gateway drops values that fail these patterns without an error, so LiteBot rejects them with 400 first. The gateway reads them only for the call's own tts_provider.

retryPolicy sets what happens to numbers that are busy, decline or do not pick up: maxAttempts (1 to 5, default 2) total calls per number, and retryDelayMinutes (1 to 1440, default 30) between them.

Voices and models

The form suggests models and voices per provider from a catalog: a built-in list plus your own additions. Voices tied to a model are offered only with that model, voices for the call language come first, and you can still type any id. Changing the provider clears the model and voice.

Add cloned or designed voices and new models in Settings > Outbound Dialer > Voices and models (admin), or by API:

curl -s -X PUT http://127.0.0.1:3345/api/campaigns/tts-catalog \
  -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
  -d '{"custom":{"gemini":{"models":[{"id":"gemini-2.5-flash-tts","label":"Flash TTS"}],"voices":[{"id":"Kore","label":"Kore, calm","model":""}]}}}'

The body replaces all additions; a provider you leave out loses its additions. An addition with a built-in id replaces that entry's label. GET /api/campaigns/tts-catalog (campaign read) returns providers (per provider: label, supportsModel, models, voices, voiceHint; your entries come first with custom: true), custom and updatedAt.

Contacts and CSV mapping

Upload a CSV of up to 10 MB and 50,000 rows. The first row is the header. UTF-8 (with or without BOM), UTF-16 and Windows-1252 are detected. Every row must have the same number of columns.

The form picks the phone column from a header that contains phone, mobile, cell, number or tel, and turns every other column into a field: Patient Name becomes patient_name. Rename or remove fields before you save. Rows with an empty phone number are skipped; a number that appears twice is called twice. LiteBot sends the phone value as written, so put it in the form your gateway dials.

Each mapped field is sent in the call's metadata, next to botId, campaignId and contactId. The gateway hands the call metadata to the bot as session context: tools read it, and the model sees it when the bot has injectContext: true. Two field names have a gateway meaning: caller_name is the person's name, and llm_context is background text for the bot. The form says the bot reads fields as {{field_name}}, but LiteBot fills {{...}} in the system prompt from the bot's own variables, not per call; refer to the facts in the prompt as context instead. See Chat API.

By API the mapping is two calls: POST /api/campaigns/:id/upload-csv (multipart field file) returns headers, totalRows and sampleRow; POST /api/campaigns/:id/map-columns creates the contacts.

curl -s -X POST http://127.0.0.1:3345/api/campaigns/<id>/map-columns \
  -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
  -d '{"phoneNumberColumn":"Mobile","metadataMapping":{"caller_name":"Patient Name","appointment_date":"Date"}}'

Mapping again replaces the contacts of the earlier import.

Test, start and control

Test call. POST /api/campaigns/:id/test-call calls one contact, by default the first; phoneNumberOverride sends it to your own phone with that contact's fields, and contactIndex picks another row. It answers once the gateway accepts the call, or 400 with the failure. A test call is not tracked by callbacks and does not change the contact's status.

Start, pause, resume, stop. Start needs draft, a mapping and contacts. Pause needs running; resume needs paused; stop works on either and marks every contact not yet dialled as skipped. When the last contact is done the campaign becomes completed. Campaign states: draft, running, paused, completed, stopped, failed.

Call tracking

With AUTO_DIALER_PUBLIC_URL set, every call gives the gateway its own signed callback URL:

<AUTO_DIALER_PUBLIC_URL>/api/campaigns/callback/<campaignId>/<contactId>/<hmac>

The gateway posts {"sessionUuid":"...","status":"answered"} when the call is answered, and {"status":"completed"|"failed","duration":...,"conversation":{...}} when it ends. The HMAC in the path stands in for a signature, because the gateway's status webhook is unsigned. A bad token gets 401. The address must be public: the gateway refuses private and localhost callback addresses unless its own WEBHOOK_SSRF_CHECK=false.

A slot stays busy until the final callback, so concurrency is the number of live calls. If no final status arrives within AUTO_DIALER_MAX_CALL_SECONDS, the slot is freed and the contact closed as completed with endReason: no_final_callback; a late callback still corrects it. Your campaign webhookUrl receives a copy of each callback with campaignId, contactId, phoneNumber and the contact's metadata added.

Without AUTO_DIALER_PUBLIC_URL, the gateway gets your campaign webhook directly, a contact is completed as soon as the gateway accepts the call, and concurrency limits only the call requests.

Outcomes

Contact state Meaning
pending Waiting to be queued, or a retry is due while the campaign is paused
queued In the dial queue, possibly waiting for a retry time
calling Call request in flight. Over SIP the phone is ringing
in_call The gateway accepted the call; waiting for the final status
completed Answered. callResult: ended_early means the person hung up during the greeting
no_answer Busy, unanswered or declined on every allowed attempt
failed Technical error, or a call request that timed out
skipped The campaign was stopped first

How LiteBot reads the gateway's answer to a call request:

Gateway answer Result
200 with status: initiated and a sessionUuid in_call
500 failed with a SIP error (busy, timeout, declined) Retry per retryPolicy, else no_answer
Concurrent-call limit, 429 or 503 Requeued after 15 s; the attempt does not count
Set-up error (not registered, cannot route, originate webhook, licence) Up to 3 tries with backoff, then failed
Any other 4xx failed, no retry
No answer within AUTO_DIALER_INITIATE_TIMEOUT_MS failed, no retry: the call may have connected

Ten technical errors in a row pause the campaign; no-answers do not count. Over SIP the gateway's call request returns only after the person answers, which is why the timeout must cover ring time.

Gateway quirks. The gateway can report a fully answered call as failed. LiteBot counts any call answered at request time or by callback as completed, and sets callResult to ended_early only when the transcript has no turn from the person. Realtime calls never send answered, so an answered realtime call that ends failed is recorded as completed with endReason: gateway_realtime_ended.

Restarts. Queued contacts are queued again with their remaining retry delay. Contacts that were calling or in_call are not redialled; a sweep every minute closes them after the maximum call time if their callback never arrives.

API routes

All under /api/campaigns, login or API key required, except the two callback routes.

Method Path Permission Purpose
GET / read List. page, limit (max 100, default 20), status, sortBy, sortOrder
POST / create Create a campaign (201)
GET, PUT, DELETE /:id read, update, delete Read, edit (draft only), delete (draft, stopped, completed or failed)
POST /:id/upload-csv update Stage a CSV
POST /:id/map-columns update Map columns and create contacts
GET /:id/preview read First mapped contacts, limit up to 500
GET /:id/contacts read Contacts. page, limit (max 100, default 50), status
POST /:id/test-call update One test call
POST /:id/start, /pause, /resume, /stop update Control
GET /:id/stats read Counts per state and progress
GET, PUT /settings read; admin to change Default gateway endpoint
GET, PUT /tts-catalog read; admin to change Voice catalog
POST /callback/:campaignId/:contactId/:token signed URL Gateway call status
POST /webhook/:campaignId X-Webhook-Signature Legacy result webhook

The legacy webhook checks X-Webhook-Signature, a hex HMAC-SHA256 of the JSON body, only when AUTO_DIALER_WEBHOOK_SECRET is set. Without that variable it accepts unsigned posts, so set the secret or block the path at your proxy.

Where to go next