Core concepts
Voxa’s object model is small. Once these seven concepts click, the rest of the product is mostly UI over them.
Tenant
Section titled “Tenant”A tenant is one isolated workspace. Every tenant has its own users, its own WhatsApp number, its own contacts, its own campaigns, its own billing record, and its own audit trail. A single user belongs to exactly one tenant. Tenant scoping is enforced in API middleware — nothing crosses tenant boundaries.
When you signed up, you created your first tenant. Subsequent team members join that tenant via invitation, not by creating a new one.
User and role
Section titled “User and role”Voxa has five roles with numeric levels you occasionally see in error messages:
| Role | Level | What it can do |
|---|---|---|
| super_admin | 200 | Platform staff. Access every tenant, impersonate users for support. |
| admin | 100 | Tenant admin. Manage team, Meta credentials, campaigns, billing. |
| owner | 80 | Full tenant access including billing and delete-tenant. |
| editor | 50 | Create and edit campaigns, flows, contacts, templates. |
| viewer | 10 | Read-only dashboards and entries. |
The super_admin role is held only by Voxa staff — tenants never see it.
Channel
Section titled “Channel”A channel is a connected messaging surface. Voxa supports
WhatsApp today via Meta’s WhatsApp Business Cloud API. Each tenant
connects exactly one WhatsApp phone number; the credentials live in
meta_credentials and are tenant-unique.
Telephony (SMS / voice) and other channels are on the roadmap; see Integrations overview.
A flow is a directed acyclic graph of nodes that defines how a conversation progresses. Each node is one of:
| Node type | Behaviour |
|---|---|
| start | Auto-created entry point. Every flow has exactly one. |
| message | Sends a free-form text message, then advances. |
| question | Sends a prompt and waits for the contact’s reply. Stores the answer in a named variable. |
| media_capture | Prompts for an image / video / audio / document upload. |
| validation | Runs a validator against a stored variable (e.g. South African ID checksum). |
| condition | Branches based on variable comparison (equals / contains / not_equals). |
| delay | Pauses for a configurable number of seconds. |
| submission | Writes the accumulated variables to an entries record. |
| end | Terminates the flow and closes the conversation. |
Every flow has a draft/published versioning model. Publishing a flow snapshots the current draft as an immutable version and creates a fresh draft for future edits. Live conversations continue running on whichever version they started on.
Campaign
Section titled “Campaign”A campaign wraps a flow with entry rules, scheduling, and the customer-facing surface (link / QR code / keyword). It carries:
- entry_mode — one of
link,qr,keyword, orbroadcast. - status —
draft → scheduled → live → paused → completed → archived. - starts_at / ends_at — optional scheduled window.
- entry_limit_per_contact — usually
1for competitions; higher for polls or repeat surveys. - requires_optin — if true, only contacts with
opted_in=truecan join. - keyword — for
keyword-mode campaigns, the word that triggers entry (e.g.ENTER). - Customisable success / duplicate / closed messages.
A campaign must be manually set to live — Voxa does not auto-transition
scheduled campaigns today. ends_at is informational.
Contact
Section titled “Contact”A contact is a person reachable on WhatsApp with a unique wa_id
(their phone number in Meta’s format, e.g. 27821234567). Contacts land
in Voxa in three ways:
- They send a WhatsApp message to your number — Voxa creates the contact on first inbound.
- You add them manually via Contacts → Add.
- Future: CSV import and API ingest (not yet wired).
Contacts carry tags (JSON array of strings) and custom_fields (JSON
object) for tenant-specific segmentation, plus opted_in / opted_out_at
for consent tracking.
Conversation
Section titled “Conversation”A conversation is the live runtime state of a contact inside a flow.
It tracks which node they’re on, what variables they’ve collected so
far, and which input the flow is waiting for. Conversations end when a
flow reaches an end or submission node, or when they’re explicitly
abandoned.
Conversations also hold the WhatsApp 24-hour customer-care window: once a contact has sent a message, you can reply freely with free-form text for 24 hours. After that window closes, Meta requires an approved template for outbound contact — a feature Voxa has scaffolded but not yet wired end-to-end (see Messaging integration).
An entry is the result of a completed flow. When a contact reaches
a submission node, Voxa writes an entries record containing:
campaign_id— which campaign they completed.wa_id— the contact’s phone number.data— a JSON blob of every variable the flow collected.media_refs— signed Cloudflare R2 URLs for any media uploaded along the way.submitted_at— the completion timestamp.is_duplicate— true if this contact has already entered this campaign and the campaign’sentry_limit_per_contactallows repeats.
Entries are the primary output of a Voxa campaign. You export them
as XLSX or CSV from the dashboard, or via the /entries/export API.
How they fit together
Section titled “How they fit together”Tenant ─── owns ──→ Users, Contacts, Meta credentials, Templates, Flows, Campaigns, Entries, Analytics
↓ connects Channel (WhatsApp number)
↓ triggers Campaign ── binds ──→ Flow ── runs ──→ Conversation │ completes │ ↓ EntryA tenant owns everything. A campaign binds a flow to a channel. When a contact enters (link / QR / keyword), a conversation starts on the flow, progresses through nodes, and ends as an entry you can export.
Ready to build?
Section titled “Ready to build?”- Build your first real flow
- Glossary — every term defined