POST /calls takes a
number, POST /batches takes rows. That is deliberate, and it stays true. The
contacts layer sits beside it for the case where you would rather Vodex kept the
people — so a campaign can say “everyone in Ohio with a balance over $500”
instead of a pasted CSV, and so the report that comes back joins to a person
rather than to a phone number.
A campaign is not a second dialer. Launching one creates an ordinary
batch — same runner, same number
rotation, same calling-window logic. What a campaign adds is who.
The shape of it
1
Contacts
People, with any number of phone and email channels. Anything
industry-specific is a custom attribute, never a column.
2
Audiences
A saved filter, not a saved list — so it means the same thing next
month, when the people who match it are different.
3
Campaigns
An audience plus an assistant. Creating one dials nobody; launching turns it
into a batch.
4
Do not contact
Two separate things: a decision about a person, and a suppression list of
addresses.
Getting people in
Create them one at a time:externalRef, else accountNumber, so re-running
the same export reports updates instead of duplicates, and channels merge
rather than replace: a second file listing only a mobile will not delete the
work number the first one brought. Rejections carry the file line number —
the one your spreadsheet shows — up to 200 of them, and the ceiling is 50,000
rows.
GET /contacts/imports/template
returns the headers the importer maps without configuration.
Two things are rejected rather than guessed:
Audiences are filters
attr:<key> reaches a custom attribute — and
the whitelist is published
with the comparators each type admits, because a client that guesses field names
gets a 400. The filter is validated when you save it, not only when a campaign
runs, so a typo fails now rather than on the morning of the call.
Preview before
committing:
Campaigns
variables maps each assistant {{var}} to a contact field or attr:<key>;
the sources are listed
rather than guessed. A variable with no value for a contact makes that person a
rejected enrollment at launch — not a call with a blank in the prompt.
Then launch, which is
the only call that dials anything:
- Every refusal is recorded as an enrollment with a reason —
dnc_account,dnc_list,opted_out,no_channel,bad_number,rejected. “Why was this person not called” is what a compliance review asks, and a batch row that was never created cannot answer it. tzis injected on every row from the contact’s timezone. Calling people only during their own local hours is the entire point of having contacts.- Relaunch is idempotent on
campaign:<id>:run:<n>, so a double-clicked launch replays instead of dialling the audience twice.
pause, resume, stop — rather than opening its
batch. Those delegate to the same transition the batch routes use, and campaign
status is derived from the batch on every read, so the two can never disagree.
Set blockSize to dial the audience in blocks; nothing beyond the current block
runs until you
ask for the next one.
Do not contact
There are two concepts here, and neither is derived from the other:
Upload a list as CSV,
and ask about addresses
before you dial:
check is a read, so a viewer may call it; POST only because it takes a
list in the body.
Joining back to the call
Launch sets each row’scontactRef to the contact’s id, which is the same field
you would set by hand on
POST /calls — so it is echoed
on the interaction.completed webhook exactly as it always was.
Nothing about the report changes because a call came from a campaign.
That one field is also the whole of the join:
the timeline
matches calls on it, with no projection to maintain and no events to replay —
and it works retroactively, for calls placed before the contact record existed.
Enrollments appear there too, including the ones that were never called, each
with its reason in words.
The compliance view
gathers what a reviewer wants on one screen: consent basis and when it was
captured, the contact’s local time right now, the tenant’s calling window, and
attempt counts for the last seven days.
Those counts gate nothing — the response says
enforced: false, because a
number that looks like a limit but is not one is worse than no number at all.
Attempt limits are the retry policy on the batch.Next
Batch dialing
What a launched campaign becomes. Retry policy, number rotation, calling
windows.
Webhooks
The report that comes back, with your
contactRef on it.Import contacts
Headers, upsert keys, and what a rejected row tells you.
Launch a campaign
The one call that dials, and everything it refuses to dial.