Reader setup
Before you start
Run each step in order and move only when the outcome is confirmed.
- Read-only database access to the VICIdial schema — see Create a read-only database account for safe VICIdial queries if you do not have one yet — or an admin account for the Admin → Call Times and Lists screens
- One inactive test campaign and an isolated test list, so you are validating the mechanism, not reconfiguring a live campaign
- The named jurisdictions your leads fall under, since calling-hour and state telemarketing rules are set by law and differ by state, province and country
- What you will prove
- You will know exactly which column drives local-time eligibility, how to attach state-specific and holiday overrides to a campaign's call time, and how to validate postal_code and state at load time so a bad value cannot silently disable the gate.
- Safety boundary
- This is technical configuration guidance, not legal advice; getting the mechanism right does not by itself establish that calling any number, at any hour, in any state, is lawful.
Reader path
How to use this article
- Use it when: You need a fixed sequence to make a deployment or configuration change now.
- Expected result: Follow each step and verify the outcome before changing the next layer.
- Start here: Start at the first section and complete every checkpoint before moving to the next.
01 / 07
The gate runs on one column: gmt_offset_now
Fast answer: VICIdial does not know what time it is for a lead. It knows what time it is on the server, and it subtracts a per-lead offset stored in vicidial_list.gmt_offset_now, a DECIMAL(4,2) column defaulting to 0.00, to work out the lead's local clock. Every calling-hour and state-hour rule downstream, campaign-level local_call_time, per-state overrides, holidays, depends on that one number being correct. A lead loaded with a missing or zero gmt_offset_now is not gated at all; it is treated as if it sits in Greenwich Mean Time (GMT), the zero-offset reference zone, and can be dialed at whatever hour the server clock happens to show.
This is configuration guidance, not legal advice, and completing it does not establish that any call is lawful. Calling-hour restrictions and state-specific telemarketing rules are set by law in the United States and elsewhere, and they vary by state, province and country; confirm the actual rule text for every jurisdiction you call into with your own legal or compliance reviewer. Nothing in this article should be read as a citation to a specific statute or a specific permitted hour window.
See VICIdial terminology for complete beginners: users, phones, campaigns and leads for definitions of lead, list, campaign, agent, the hopper, disposition and carrier. For how leads get into vicidial_list in the first place, see the companion article “Load leads into VICIdial from a CSV file”; this guide picks up after that load, at the columns that decide whether a loaded lead may be dialed right now.
- Name every jurisdiction (state, province, country) this campaign calls into before you configure anything.
- Confirm gmt_offset_now is populated for every lead in the list, not just most of them.
- Treat a 0.00 offset as unverified, not as “lead is in GMT”, until you have checked why.
Visual walkthrough
Follow three real demo screens
Captured on an isolated VICIdial demo: Administration screens on September 24, 2026, and the idle Agent screen on August 11, 2026. Each caption states its own capture time, and every sanitized image helps you recognize a related screen; none proves that this article's call, command, or result occurred.Identify the owning campaign

Find the Lists workspace

Inspect one list without opening a lead

02 / 07
Step 1 — Understand how a lead's local time gets set
vicidial_list carries city, state VARCHAR(2), postal_code VARCHAR(10) and country_code VARCHAR(3) alongside gmt_offset_now, and the list itself carries a time_zone_setting ENUM with four values: COUNTRY_AND_AREA_CODE, POSTAL_CODE, NANPA_PREFIX and OWNER_TIME_ZONE_CODE, labeled Time Zone Setting on the Lists screen, defaulting to COUNTRY_AND_AREA_CODE. That setting decides which reference table a population job reads to fill in gmt_offset_now for every lead in that list: the area-code method reads vicidial_phone_codes (country_code, areacode, state, GMT_offset, DST, DST_range), the postal-code method reads vicidial_postal_codes (postal_code, state, GMT_offset, DST, DST_range, country, country_code), and the other two methods derive the offset from the phone number's NANPA prefix or from a fixed owner-assigned zone code instead of a lookup table.
This is the mechanism worth internalizing: local-time gating is not computed at dial time from a live timezone API. It is computed once by a population job against whichever reference table time_zone_setting points at, written into gmt_offset_now, and read from there on every subsequent dial attempt. That is also why gmt_offset_now carries its own index in vicidial_list: the dialer needs to filter large lead pools by local time cheaply, over and over, without re-deriving the timezone on every pass.
The practical consequence: if area_code and postal_code disagree, because a lead moved, ported a number, or was entered with a typo, the method you chose decides which one wins, and the other is simply not consulted. Pick POSTAL_CODE when your source data trusts the ZIP (postal code) more than the phone number's area code, and confirm postal_code is actually populated before you do, since an empty postal_code under the POSTAL_CODE method leaves gmt_offset_now unset just as surely as a bad area code does under the default method.
SHOW CREATE TABLE vicidial_list;SELECT list_id, list_name, time_zone_setting, local_call_timeFROM vicidial_listsWHERE list_id = '<LIST_ID>';SELECT COUNT(*) AS zero_or_null_offsetFROM vicidial_listWHERE list_id = '<LIST_ID>' AND (gmt_offset_now = 0.00 OR gmt_offset_now IS NULL);Captured demo response · 2026-09-24 22:25 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.
- Before you run it
- Run through an authorized read-only database session against your own test list_id. All three statements only read.
- Success looks like
- vicidial_list matches the documented gmt_offset_now DECIMAL(4,2) default 0.00 with an index, the list's time_zone_setting is the method you intended, and zero_or_null_offset is 0 or explainable (for example, leads you know are genuinely in the zero-offset zone).
- Stop if
- Stop and investigate before dialing if zero_or_null_offset is any meaningful share of the list; those leads have no verified local time and can be called at the wrong local hour.
03 / 07
Step 2 — Scope calling hours to a campaign, a state and a holiday
vicidial_campaigns.local_call_time is a VARCHAR(10) defaulting to '9am-9pm', labeled Local Call Time on the campaign screen. That default value is not a literal window; it is the call_time_id of the stock record VICIdial ships in vicidial_call_times, which stores ct_default_start and ct_default_stop as SMALLINT values in 24-hour HHMM form (900 and 2100 for the stock record), plus a start and stop pair for every day of the week that default to 0, meaning fall back to the default pair unless a specific day is overridden. A list can override its campaign's call time individually through vicidial_lists.local_call_time, which itself defaults to the literal string 'campaign', meaning inherit whatever the campaign says.
State-specific hours live in a separate table, vicidial_state_call_times, one row per named state rule (state_call_time_state VARCHAR(2)) with its own default and per-day start/stop pairs. A state rule only takes effect once it is attached to a call time record through vicidial_call_times.ct_state_call_times, a pipe-delimited TEXT column listing every state_call_time_id linked to that record, added and removed from the Admin → Call Times screen labeled ADD NEW STATE CALL TIME. Attach a state rule and the campaign's dial-eligibility check for a lead in that state uses the state's window instead of the record's own default; leave a state unlisted and it falls back to the parent call time's default hours.
Two things get confused with state-hour gating and are not the same mechanism. vicidial_campaigns.state_descriptions, a VARCHAR(40) defaulting to '---DISABLED---', is labeled State Descriptions Banner on the campaign screen; it points at a settings container of per-state text shown to the agent, useful for a state-mandated disclosure or script note, but it does not block or permit a call by itself. The actual per-state hour block is vicidial_state_call_times attached through ct_state_call_times, described above. Do not assume enabling one gives you the other.
- Confirm local_call_time on the campaign resolves to a real call_time_id, not an ad hoc string that happens to look like one.
- List every state you call into, and confirm whether each one needs its own row in vicidial_state_call_times or can safely use the parent default.
- Do not mistake state_descriptions (an agent-facing banner) for a calling-hour or calling-permission control.
SELECT campaign_id, local_call_time, state_descriptions FROM vicidial_campaigns WHERE campaign_id = '<CAMPAIGN_ID>';SELECT list_id, local_call_time FROM vicidial_lists WHERE campaign_id = '<CAMPAIGN_ID>';SELECT call_time_id, call_time_name, ct_default_start, ct_default_stop, ct_state_call_times, ct_holidaysFROM vicidial_call_timesWHERE call_time_id = (SELECT local_call_time FROM vicidial_campaigns WHERE campaign_id = '<CAMPAIGN_ID>');Captured demo response · 2026-09-24 22:25 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.
- Before you run it
- Run through the same read-only session, substituting your own campaign_id; the third statement looks up that campaign's own call_time_id automatically through a subquery instead of asking you to copy a value over from the first query by hand.
- Success looks like
- You can state, for that exact campaign, which call time record governs it, which lists override it, and which state rules and holidays are actually linked through ct_state_call_times and ct_holidays.
- Stop if
- Stop if ct_state_call_times is empty for a campaign you know calls into a state with its own required hours; that state is silently using the parent default instead of its own rule.
04 / 07
Step 3 — Validate postal code and state before the load, not after
Gating only works if the value it depends on is real. A blank state, a malformed postal_code, or a postal_code that does not exist in vicidial_postal_codes all produce the same failure mode: the population job has nothing to look up, gmt_offset_now stays at its 0.00 default, and the lead dials as if no local-time rule applies. Validate before the load lands in vicidial_list, using the reference tables VICIdial already ships (vicidial_postal_codes for postal_code, vicidial_phone_codes for area code and state) as the source of truth for what a real value looks like, rather than trusting the source file's own state and ZIP columns.
state on vicidial_list is VARCHAR(2); anything longer, lowercase, or a non-postal abbreviation will not match a state rule that expects an exact two-letter code, and will not match ct_state_call_times or vicidial_state_call_times either. postal_code is VARCHAR(10), wide enough for a Canadian postal code or a ZIP+4, but a value with stray whitespace or a country's format mismatched against country_code will fail the same lookup for the same reason: it never matches a row in the reference table, silently, with no error surfaced to the person who ran the load.
Run the check as a pre-load gate against the file you are about to import, and again as a post-load audit against what actually landed, because a value can pass the first check and still fail to resolve if the list's time_zone_setting points at a different reference table than the one you validated against.
#!/usr/bin/env bashset -euo pipefailSRC_CSV="$1"STATE_COL=6ZIP_COL=7 bad_state=$(awk -F',' -v c="$STATE_COL" 'NR>1 && $c !~ /^[A-Z]{2}$/ {n++} END{print n+0}' "$SRC_CSV")bad_zip=$(awk -F',' -v c="$ZIP_COL" 'NR>1 && $c !~ /^[0-9]{5}(-[0-9]{4})?$/ {n++} END{print n+0}' "$SRC_CSV") echo "Rows with a bad state column: $bad_state"echo "Rows with a bad ZIP column: $bad_zip" if [ "$bad_state" -gt 0 ] || [ "$bad_zip" -gt 0 ]; then echo "Fix or drop the flagged rows before loading; do not load and clean up after." exit 1fiecho "State and ZIP columns pass basic shape validation."This sample changes a system, contacts an outside service, needs a live call, or would print real data from a shared server, so it was not run on the demo. Run it only where you are authorized, and compare the result with the success and stop guidance.
- Before you run it
- Run against the exact CSV you are about to load, with STATE_COL and ZIP_COL set to the real column positions in that file. This only reads the file; it changes nothing.
- Success looks like
- Both counts are zero, or every flagged row has been corrected or intentionally dropped and re-checked before you proceed to the actual load.
- Stop if
- Stop the load if either count is nonzero. This is a shape check only, catching malformed values; it does not confirm the ZIP or state is real, which the post-load audit below covers.
05 / 07
Step 4 — Audit gmt_offset_now, state and postal_code after the load lands
A shape check on the source file catches malformed values, but it cannot catch a well-formed value that VICIdial's reference tables do not recognize, or a population job that has not run yet. The definitive check is against vicidial_list itself, after the load, looking for exactly the failure mode described at the top of this article: a zero or missing gmt_offset_now, or a blank state or postal_code, on a lead that is about to enter the hopper.
Run this audit every time you load a new list, and again on a schedule against active lists, since a lead can arrive with good data and still lose it later through a manual edit, an API update, or a merge from a source that did not carry the same fields.
#!/usr/bin/env bashset -euo pipefailREADONLY_CNF=/etc/vicidial-readonly.cnfLIST_ID='<LIST_ID>'mysql --defaults-extra-file=$READONLY_CNF -e "SELECT lead_id, list_id, state, postal_code, country_code, gmt_offset_nowFROM vicidial_listWHERE list_id='$LIST_ID' AND (gmt_offset_now = 0.00 OR gmt_offset_now IS NULL OR state = '' OR state IS NULL OR postal_code = '' OR postal_code IS NULL)ORDER BY lead_idLIMIT 1000;"Captured demo response · 2026-09-24 22:25 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.
- Before you run it
- Run on a host with a working read-only credentials file at /etc/vicidial-readonly.cnf, substituting the list_id you just loaded. The statement only reads.
- Success looks like
- Zero rows come back, meaning every lead in the list has a nonzero gmt_offset_now and a populated state and postal_code.
- Stop if
- Any row returned here is a lead that can be dialed at the wrong local hour right now. Pull it from the hopper's eligible pool, or hold the whole list, until the underlying value is corrected and the population job has re-run.
06 / 07
Step 5 — Add a holiday without breaking the rest of the schedule
vicidial_call_time_holidays holds one row per holiday: holiday_date, a holiday_status ENUM of ACTIVE, INACTIVE or EXPIRED, its own ct_default_start and ct_default_stop override pair, and a holiday_method VARCHAR(40) defaulting to REPLACE, with REPLACE and ADDITION_REVERSE as the two values offered on the Admin → Call Times screen under the label Holiday Inbound Method. A holiday only affects a given call time or state call time once its holiday_id is added to that record's ct_holidays pipe-delimited column; defining a holiday in the table does nothing on its own.
REPLACE is the default and the one whose effect is directly evidenced in the schema: on the holiday's date, the record's normal per-day hours are swapped out for the holiday's own ct_default_start/ct_default_stop pair for that one day. ADDITION_REVERSE is the alternate mode; confirm its exact effect on a test call time in your own environment before relying on it for a live campaign, since the label alone does not fully specify the behavior and this article will not guess at it.
Set holiday_status to INACTIVE well before the date if you decide a holiday should not apply this year, rather than deleting the row; EXPIRED is the status VICIdial uses for a holiday whose date has already passed, and keeping the row around with a clear status is cheaper than reconstructing it next year.
07 / 07
Troubleshoot a gate that is not holding, and how to stop safely
Three symptoms cover most real incidents. Leads dialing outside the hours you expect almost always trace back to gmt_offset_now being 0.00 or unset on the specific leads involved, not to a wrong call_time_id; check the offset before you touch the call time record. A state you believed was gated but is not usually means the state's rule exists in vicidial_state_call_times but was never added to ct_state_call_times on the call time the campaign actually uses, so it is silently falling back to the parent default. A holiday that did not take effect usually means the holiday_id was never added to ct_holidays on that specific call time record, even though the holiday row itself looks correctly configured.
If you find leads dialing on a bad offset, stop that list from feeding the hopper first, then run the audit query from the previous section to find every affected lead_id before deciding whether to fix the value in place or pull the batch and reload it with corrected postal_code and state values.
Do not fix an urgent gating failure by hand-editing gmt_offset_now on individual rows under pressure. Correct the underlying state or postal_code value instead, through the admin lead screen or the update_lead Non-Agent API function, then let the same population job that runs for a normal load recompute the offset, so the rollback stays reproducible and the reference table remains the single source of truth. Re-run the audit query on the affected list before you consider the incident closed; a fix you have not re-verified is not a fix yet.
- Check gmt_offset_now on the specific leads before suspecting the campaign's call_time_id.
- Confirm ct_state_call_times and ct_holidays actually list the state or holiday you expect, not just that the state or holiday row exists somewhere.
- Stop a list from feeding the hopper before investigating, not after.
- Re-run the post-load audit on the affected list before closing the incident.
Evidence ledger
Verification basis
- vicidial_list carries gmt_offset_now DECIMAL(4,2) default '0.00' with its own index, alongside city, state VARCHAR(2), postal_code VARCHAR(10) and country_code VARCHAR(3), also indexed on postal_code; vicidial_campaigns.local_call_time is VARCHAR(10) default '9am-9pm' and state_descriptions is VARCHAR(40) default '---DISABLED---', confirmed by this guide's own SHOW CREATE TABLE vicidial_list query in Step 1.
- vicidial_lists.time_zone_setting is an ENUM of COUNTRY_AND_AREA_CODE, POSTAL_CODE, NANPA_PREFIX and OWNER_TIME_ZONE_CODE, labeled Time Zone Setting on the Lists screen, default COUNTRY_AND_AREA_CODE; vicidial_lists.local_call_time defaults to the literal string 'campaign'.
- vicidial_call_times stores ct_default_start/ct_default_stop plus per-weekday overrides, a ct_state_call_times pipe-delimited column linking vicidial_state_call_times rows, and a ct_holidays pipe-delimited column linking vicidial_call_time_holidays rows; vicidial_call_time_holidays.holiday_status is an ENUM of ACTIVE, INACTIVE and EXPIRED, and holiday_method defaults to REPLACE with ADDITION_REVERSE as the other admin-screen option, labeled Holiday Inbound Method.
- The campaign screen labels local_call_time as “Local Call Time” and state_descriptions as “State Descriptions Banner” — these are the field labels shown on the campaign screen on this build; confirm the exact wording on yours, since UI text can change between versions. state_descriptions is an agent-facing per-state banner container, distinct from the state-hour gate in vicidial_state_call_times.
Primary references
Sources
- VICIdial Non-Agent APIVICIdial · accessed August 5, 2026
- Complying with the Telemarketing Sales RuleFederal Trade Commission · accessed August 5, 2026
- National Do Not Call Registry for BusinessesFederal Trade Commission · accessed August 5, 2026