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:
- The campaign's own Gateway endpoint (optional, in the campaign form).
- Settings > Outbound Dialer in the dashboard (stored in MongoDB
auto_dialer_settings). 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
- How the gateway uses the bot during a call: Voice
- See the calls' transcripts and costs: Conversations and analytics
- Permissions and tenants for campaign users: Users, roles and tenants
- All other endpoints: HTTP API reference