Disclosure: I run NxFlowAI, an automation agency serving US businesses remotely from Mumbai. Not legal advice; check consent rules with your own adviser.
In US small-business automations, texting consent tends to live in the texting tool and nowhere else. Then a second tool (a CRM sequence, a review-request app, a reminder service) texts someone who replied STOP last month. The fix is boring and effective: one consent ledger that every automation checks before sending. It is a pattern I use as an AI automation agency building US texting workflows.
The table
CREATE TABLE consent_events ( id BIGSERIAL PRIMARY KEY, phone_e164 TEXT NOT NULL, -- +1XXXXXXXXXX channel TEXT NOT NULL, -- sms, whatsapp, email purpose TEXT NOT NULL, -- transactional, marketing event TEXT NOT NULL, -- opt_in, opt_out source TEXT NOT NULL, -- web_form, keyword_reply, staff_entry evidence TEXT, -- form id, message id, staff note occurred_at TIMESTAMPTZ NOT NULL DEFAULT now() ); Append-only. Never update a row; add a new event.
The check
def may_send(phone, channel, purpose): last = db.latest_event(phone, channel, purpose) if last is None: return purpose == "transactional" and has_active_request(phone) return last.event == "opt_in" Every automation calls may_send right before sending, not when the job is queued.
Capturing opt-outs
- Inbound texts matching the standard opt-out words (the FCC's list is stop, quit, end, revoke, opt out, cancel and unsubscribe; carriers and providers may recognise more) write an
opt_outevent for all purposes on that channel. Plain-language requests such as "please stop texting me" should be caught too, by a person if not by code. - Staff can record an opt-out by hand ("customer asked on the phone").
- Opt-ins come from forms with clear wording, and the form id goes in
evidence.
Why append-only
When someone asks "why did we text this person?", you can answer with dates and sources. That is worth more than a boolean flag.
Test cases
- Opt-out on Monday, reminder scheduled Sunday: blocked.
- Opt-in for reminders only, marketing job runs: blocked.
- Number formatted three ways: same person (normalize to E.164).
We design ledgers like this during a 72-hour audit before building any texting flow. For the non-technical overview of that audit, see our guide to auditing a business workflow before AI.