How to handle STOP and HELP when an AI agent sends texts
Handle opt-out keywords before the model ever sees a reply: record STOP, send one confirmation and nothing after it, and drop the message from the agent's queue. Answer HELP with who you are, how to reach a person and how to opt out. Send plain-language requests to stop to a person or a fixed rule, log every consent change, treat START as a return to service texts only, and check opt-out status on every send.
Unitpost ·
An agent that reads replies will eventually read "STOP". The worst thing it can do is answer: an apology, a retention offer, a question about why. Each of those is a message sent after an opt-out.
Opt-out handling has to sit in front of the model, in code that matches words exactly and doesn't negotiate.
Why must STOP be handled before the model sees it?
The CTIA Messaging Principles (section 5.1.3) ask senders to acknowledge an opt-out with one final confirmation and send nothing after it. A model asked to reply helpfully doesn't know that the right reply to STOP is no reply. A prompt makes a behavior likely. An opt-out needs it to be certain.
On Unitpost numbers, keyword handling happens before your webhook fires. An incoming text is matched, as the whole message, against the opt-out keywords: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE, OPT OUT, OPTOUT, OPT-OUT, REMOVE, TD and ARRET. Case, accents and punctuation around the word don't matter, so "Stop." and "(STOP)" both count. Unitpost records the opt-out and sends one confirmation from your registered STOP text. On a US toll-free number the carrier also catches STOP and sends its own confirmation, so for that word Unitpost records the opt-out and sends none.
Your sms.received webhook still gets the text, with keyword: "OPT_OUT" and auto_replied: true. That is the signal to drop it before it reaches the agent. This is the handler from Unitpost's SMS docs:
import { Unitpost, verifyWebhook } from "unitpost";
const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);
// Your webhook endpoint, subscribed to sms.received. Pass the RAW body.
export async function onWebhook(rawBody: string, headers: Record<string, string>) {
const event = verifyWebhook<{ type: string; data: SmsReceived }>({
payload: rawBody,
secret: process.env.UNITPOST_WEBHOOK_SECRET!, // whsec_…
headers,
});
if (event.type !== "sms.received") return;
const reply = event.data;
if (reply.keyword === "OPT_OUT" || reply.auto_replied) return; // STOP, or already answered
if (reply.looks_like_opt_out) return flagForReview(reply); // "please stop texting me": a person decides
const { data: history } = await unitpost.sms.list({ participant: reply.from, limit: 20 });
const answer = await yourAgent(history?.data ?? [], reply.body);
// No `from`: the answer goes out from the number they texted.
await unitpost.sms.send({ to: reply.from, body: answer });
}
type SmsReceived = {
sms_id: string;
from: string;
to: string;
body: string | null;
keyword: "OPT_OUT" | "OPT_IN" | "HELP" | null;
auto_replied: boolean;
looks_like_opt_out: boolean;
in_reply_to: string | null;
received_at: string | null;
};The early returns are the point. An opt-out, and a text that already got its compliance answer, never becomes a prompt.
What should the opt-out confirmation say?
One message that confirms and ends. Unitpost's registration recipes write it as "Acme: You are unsubscribed and will receive no further messages." No offer, no survey, no "before you go".
The wording is the stop_message of the messaging program your registration was filed from, which is also where carriers read it. Because Unitpost sends it, the agent never writes it and can't be tempted to improve it.
What does HELP return?
HELP (or INFO) gets your registered help text: who you are, how to reach a person, and how to stop. The recipe version reads:
Acme: For help, email support@acme.com. Message and data rates may apply. Reply STOP to opt out.Unitpost sends it automatically, and the webhook arrives with keyword: "HELP" and auto_replied: true, so the agent shouldn't answer a second time. HELP isn't an opt-out: the person stays subscribed. A question in their own words, such as "who is this?", is an ordinary message. The agent can answer it, starting with your business name.
What about "please stop texting me"?
Only an exact keyword records an opt-out on Unitpost. "Please stop texting me", "unsubscribe me" or "take me off this list" arrive with keyword: null and looks_like_opt_out: true. Unitpost doesn't guess, because a loose match would opt people out of texts they asked for.
That flag is yours to act on, and you should act on it. CTIA asks senders to read and act on opt-out requests made in normal language, not only the standard keyword. Unitpost's SMS Messaging Policy requires you to honor an opt-out however it reaches you (a reply in plain words, an email, a call, a support ticket) and to record it in Unitpost promptly, within ten days at most.
US federal rules point the same way. The FCC's revocation rule, 47 CFR 64.1200(a)(10), names stop, quit, end, revoke, opt out, cancel and unsubscribe as replies that revoke consent and gives senders ten business days to honor a revocation. An FCC order published on October 26, 2026 amends these rules from November 25, 2026: a sender may designate clearly disclosed opt-out methods, and a texting sender must still honor all seven words. The practice doesn't change. Treat a clear request in plain words as an opt-out.
Don't give that call to the agent. When looks_like_opt_out is true the agent stays silent, and a person, or a narrow rule you have written and tested, decides. If it is an opt-out, record it with POST /contacts/{id}/sms-consent and action: "opt_out". From then on the number is refused like any STOP.
How do you keep a consent event log?
When a carrier or regulator asks whether someone agreed, "the agent remembered" isn't an answer. CTIA lists what to keep (section 5.1.2): when consent was given, the medium, what the person saw, the campaign, the IP address, the number, and who consented.
Unitpost keeps that as an append-only history per number. Each POST /contacts/{id}/sms-consent takes:
action:opt_inoropt_outsource:web_form,hosted_form,keyword,qr,verbal,paper,api_importorapipurpose:marketingortransactionaloccurred_at, backdated to when it really happened (a future time is refused)evidence_urlanddisclosure_version
STOP and START replies land in the same history on their own. GET /contacts/{id}/sms-consent returns the current state, marketing_eligible, and the history, newest first. Let the agent read it. Keep consent writes in your own code, where a person or a tested rule decides.
How does someone opt back in?
They text START or UNSTOP to your number. Unitpost records it, and the number can receive service texts again: reminders, receipts, codes.
START does not create marketing consent. Someone who texted START to get their appointment reminders back hasn't asked for promotions, so marketing needs a new express written consent with its own record. An agent should never read a friendly reply ("sure, keep me posted") as a re-opt-in. Only START, or a new opt-in recorded with its source, changes the state.
Why check opt-out status on every send?
Lists go stale between the moment they are built and the moment they are used. An agent that pulled this morning's patients and texts them this afternoon is working from a list some of them may have left at lunch. Agents also carry state through a conversation: "this person is fine to text" was true an hour ago.
Unitpost checks the suppression list and the number's opt-out state on every send, transactional included. A STOP has no transactional bypass. A refused send comes back 403 with message_suppressed or recipient_not_opted_in, and error.details[0].code is opted_out. Nothing is sent and nothing is billed. A text scheduled earlier is checked again when it is due, ends suppressed, and fires sms.suppressed.
Teach the agent what that refusal means: stop, tell the user why, don't retry, and don't look for another route to the person, such as another sender or another workspace. Working around an opt-out breaks Unitpost's Acceptable Use Policy. An SMS opt-out leaves the person's email subscription as it was, and it isn't a reason to deliver the same message by email instead.
Not legal advice
This describes industry guidance and how Unitpost behaves. Consent and revocation rules are changing; confirm the ones that apply to you with counsel.