Skip to content

Core concepts

Voxa’s object model is small. Once these seven concepts click, the rest of the product is mostly UI over them.

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.

Voxa has five roles with numeric levels you occasionally see in error messages:

RoleLevelWhat it can do
super_admin200Platform staff. Access every tenant, impersonate users for support.
admin100Tenant admin. Manage team, Meta credentials, campaigns, billing.
owner80Full tenant access including billing and delete-tenant.
editor50Create and edit campaigns, flows, contacts, templates.
viewer10Read-only dashboards and entries.

The super_admin role is held only by Voxa staff — tenants never see it.

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 typeBehaviour
startAuto-created entry point. Every flow has exactly one.
messageSends a free-form text message, then advances.
questionSends a prompt and waits for the contact’s reply. Stores the answer in a named variable.
media_capturePrompts for an image / video / audio / document upload.
validationRuns a validator against a stored variable (e.g. South African ID checksum).
conditionBranches based on variable comparison (equals / contains / not_equals).
delayPauses for a configurable number of seconds.
submissionWrites the accumulated variables to an entries record.
endTerminates 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.

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, or broadcast.
  • statusdraft → scheduled → live → paused → completed → archived.
  • starts_at / ends_at — optional scheduled window.
  • entry_limit_per_contact — usually 1 for competitions; higher for polls or repeat surveys.
  • requires_optin — if true, only contacts with opted_in=true can 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.

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:

  1. They send a WhatsApp message to your number — Voxa creates the contact on first inbound.
  2. You add them manually via Contacts → Add.
  3. 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.

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’s entry_limit_per_contact allows 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.

Tenant ─── owns ──→ Users, Contacts, Meta credentials, Templates,
Flows, Campaigns, Entries, Analytics
↓ connects
Channel (WhatsApp number)
↓ triggers
Campaign ── binds ──→ Flow ── runs ──→ Conversation
completes │
Entry

A 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.