SMS & Campaigns Full Schema
This page documents the complete table schemas for the SMS and campaigns system. The overview and gotchas pages cover behavior — this page covers structure.
campaigns
SELECT id, id_company, name, status, id_template,
schedule_type, scheduled_at, created_at, deleted_at
FROM campaigns
WHERE id_company = :id_company AND deleted_at IS NULL;
| Column | Type | Notes |
|---|---|---|
id |
int | Primary key |
id_company |
int | Tenant scope — always required |
name |
varchar | Campaign display name |
status |
varchar | See status enum below |
id_template |
int | FK → email_templates.id |
schedule_type |
varchar | immediate, scheduled |
scheduled_at |
timestamptz | Null if immediate |
deleted_at |
timestamptz | Soft delete |
Status enum
| Value | Meaning |
|---|---|
draft |
Not yet scheduled |
scheduled |
Queued for future send |
active |
Currently sending |
paused |
Manually paused |
completed |
All sends finished |
Creating a campaign
INSERT INTO campaigns (id_company, name, status, id_template,
schedule_type, created_at)
VALUES (:id_company, :name, 'draft', :id_template,
'immediate', NOW())
RETURNING id;
campaigns_items
One row per contact per campaign send.
SELECT id, id_campaign, customer_id, id_company,
status, sent_at, opened_at, replied_at, deleted_at
FROM campaigns_items
WHERE id_company = :id_company
AND id_campaign = :id_campaign
AND deleted_at IS NULL;
| Column | Type | Notes |
|---|---|---|
id |
int | Primary key |
id_campaign |
int | FK → campaigns.id |
customer_id |
int | FK → contacts table — NOT id_customer |
id_company |
int | Tenant scope |
status |
varchar | pending, sent, failed, unsubscribed |
sent_at |
timestamptz | Null until sent |
opened_at |
timestamptz | Null until opened |
replied_at |
timestamptz | Null until reply received |
deleted_at |
timestamptz | Soft delete |
Adding contacts to a campaign
INSERT INTO campaigns_items (id_campaign, customer_id,
id_company, status)
VALUES (:id_campaign, :customer_id, :id_company, 'pending');
Reply rate calculation
SELECT
COUNT(*) AS total_sent,
COUNT(replied_at) AS total_replied,
ROUND(
COUNT(replied_at)::numeric / NULLIF(COUNT(*), 0) * 100,
1
) AS reply_rate_pct
FROM campaigns_items
WHERE id_campaign = :id_campaign
AND id_company = :id_company
AND deleted_at IS NULL
AND status = 'sent';
sms_templates
SELECT id, id_company, name, body, created_at
FROM sms_templates
WHERE id_company = :id_company;
| Column | Type | Notes |
|---|---|---|
body |
text | Template content with {{variable}} syntax |
Available variables: {{first_name}}, {{last_name}},
{{phone}}, {{email}}, {{company}}
Opt-out tracking
Opt-outs are stored as a tag on the contact record. When a contact
replies with STOP, the inbound SMS router adds the tag unsubscribed.
Before sending any campaign SMS, check:
SELECT id FROM taxonomy
WHERE id_customer = :customer_id
AND tag = 'unsubscribed'
AND id_company = :id_company;
-- If rows returned, do not send
Available phone numbers
Outbound SMS sends from the number configured in pb_settings
under the key telnyx_phone (or twilio_phone depending on
provider). Do not hardcode phone numbers — always read from
pb_settings.
SELECT value FROM pb_settings WHERE key = 'telnyx_phone';
-- Note: pb_settings has NO id_company column