Handling Opt-Outs and Consent
This guide shows you how to keep consent state consistent between Sent and your own systems: mirror keyword opt-outs into your database, opt contacts out or back in through the API, detect consent-blocked sends, and apply a send window stricter than the platform's quiet hours.
Prerequisites:
- A Sent API key, passed in the
x-api-keyheader - A webhook endpoint subscribed to
messageevents (see Getting Started with Webhooks)
Sent already enforces the consent gate at send time, so you do not need a pre-send opt-out check of your own. Compliance & Regulations describes everything the platform enforces automatically.
Mirror keyword opt-outs into your database
When a contact texts an opt-out keyword, Sent records the opt-out on the contact and fires a message.received webhook carrying the raw text. The webhook fires for every inbound message, keywords included. Match the keyword in your handler and update your own subscriber store:
const OPT_OUT_KEYWORDS = new Set(['STOP', 'CANCEL', 'UNSUBSCRIBE', 'QUIT', 'END']);
const OPT_IN_KEYWORDS = new Set(['START', 'UNSTOP', 'SUBSCRIBE']);
app.post('/webhooks/sent', async (req, res) => {
// Acknowledge first; process after responding.
res.sendStatus(200);
const { field, event, payload } = req.body;
if (field !== 'message' || event !== 'message.received') return;
// payload.inbound_number is the contact's phone number.
const keyword = (payload.text ?? '').trim().toUpperCase();
if (OPT_OUT_KEYWORDS.has(keyword)) {
// Update your own subscriber store.
await db.subscribers.update(payload.inbound_number, {
messagingConsent: false,
optedOutAt: new Date(),
});
} else if (OPT_IN_KEYWORDS.has(keyword)) {
await db.subscribers.update(payload.inbound_number, {
messagingConsent: true,
optedOutAt: null,
});
}
});If you configured custom keywords in the dashboard (Compliance → Opt-Out Keywords), add them to the OPT_OUT_KEYWORDS and OPT_IN_KEYWORDS sets. Sent matches keywords exactly and case-insensitively: the entire trimmed message body must equal the keyword. Mirror that rule rather than substring-matching. The contact record's opt_out field is what Sent checks on every send; when in doubt, read it back with GET /v3/contacts?phone=....
Opt a contact out or in through the API
If you collect opt-outs on other surfaces (a web preference center, support tickets, email unsubscribes), write them to Sent so the send-time gate enforces them everywhere.
Find the contact
Look up the contact by phone number (URL-encode the + as %2B):
curl "https://api.sent.dm/v3/contacts?phone=%2B15551234567" \
-H "x-api-key: YOUR_API_KEY"The contact ID is in data.contacts[0].id.
Set the opt-out flag
curl -X PATCH "https://api.sent.dm/v3/contacts/8ba7b830-9dad-11d1-80b4-00c04fd430c8" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"opt_out": true}'To restore consent, send {"opt_out": false}. Opt a contact back in only when you hold their documented consent; the API does not check how the opt-out was created. Opt-out is per contact, not per channel: setting the flag suppresses SMS, WhatsApp, and RCS alike.
Detect consent-blocked sends
A send to an opted-out contact is still accepted with 202; the message is then finalized as FILTERED with reason code ERR_CONSENT_BLOCKED, and a message.filtered webhook fires. Treat it as an expected policy outcome: do not retry, and reconcile your local consent state if it disagrees. Extend the same webhook handler:
// Inside the same webhook handler:
if (field === 'message' && event === 'message.filtered') {
// payload.outbound_number is the recipient's phone number. FILTERED also
// covers routing DENY rules, so confirm the contact's opt-out state
// before mirroring it.
const resp = await fetch(
`https://api.sent.dm/v3/contacts?phone=${encodeURIComponent(payload.outbound_number)}`,
{ headers: { 'x-api-key': process.env.SENT_API_KEY } },
);
const { data } = await resp.json();
const contact = data?.contacts?.[0];
if (contact?.opt_out) {
await db.subscribers.update(contact.format_e164, {
messagingConsent: false,
optedOutAt: new Date(),
});
}
}For per-message detail, GET /v3/messages/{id} shows the FILTERED status; refer to the Error Catalog for ERR_CONSENT_BLOCKED remediation.
Apply a stricter send window
Sent's quiet-hours gate already holds sends that land inside a protected window for the destination country: the message is parked as SCHEDULED and released automatically, with a message.scheduled webhook on the hold. If your legal review requires a stricter window (for example, the US TCPA limits telephone solicitations to 8 AM–9 PM recipient local time, and several states go further), gate the send in your own scheduler:
// TCPA window: 8 AM-9 PM recipient local time (47 CFR § 64.1200(c)(1)).
// This check deliberately keeps a one-hour buffer inside the federal window
// (sending only 9 AM-8 PM) to absorb clock skew and stricter state rules.
function isInsideSendWindow(timeZone: string, now = new Date()): boolean {
const hour = Number(
new Intl.DateTimeFormat('en-US', {
timeZone,
hour: 'numeric',
hourCycle: 'h23',
}).format(now),
);
return hour >= 9 && hour < 20;
}
if (isInsideSendWindow(subscriber.timeZone)) {
await sendMessage(subscriber);
} else {
await queueForNextWindow(subscriber);
}If the platform's quiet-hours rules are sufficient for you, skip this section and subscribe to message.scheduled webhooks to observe holds.
Verify your integration
- Set
opt_out: trueon a test contact and send it a message: the API returns202, andGET /v3/messages/{id}showsFILTEREDshortly after. - Text
STOPto your number from a test phone: amessage.receivedwebhook arrives with the text, and the contact'sopt_outflag readstrueon the nextGET /v3/contacts/{id}. - Text
STARTfrom the same phone: the flag returns tofalse.
Related pages
- Two-Way Conversations: keyword matching rules, auto-replies, and cross-channel opt-out
- Compliance & Regulations: what Sent enforces automatically, plus per-region regulation summaries
- Events Reference: full webhook payloads for
message.receivedandmessage.filtered - Managing Contacts: the full contact API surface
Two-Way Conversations
How Sent handles two-way conversations, from inbound matching and storage to keyword detection, opt-out handling, auto-replies, and the WhatsApp 24-hour window.
Managing Contacts
Import your user base into Sent, honor opt-outs, and control channel routing per contact. Covers automatic contact creation, phone validation, and search.