Compliance & Regulations

Understand and implement compliance requirements for messaging regulations worldwide.

GDPR (Europe)

Data Processing

  • Obtain explicit consent before messaging
  • Provide opt-out mechanism
  • Honor data deletion requests
  • Maintain processing records

Implementation

// Check consent before sending
async function canMessageUser(userId: string) {
  const user = await db.users.findById(userId);

  return user.messagingConsent === true &&
         user.optedOutAt === null;
}

// Handle opt-out
app.post('/opt-out', async (req, res) => {
  await db.users.update(req.body.phoneNumber, {
    optedOutAt: new Date(),
    messagingConsent: false
  });

  res.send('You have been unsubscribed');
});

TCPA (United States)

Requirements

  • Written consent for marketing messages
  • Honor opt-outs within 24 hours
  • Maintain consent records
  • No messages before 8 AM or after 9 PM (recipient's timezone)

Best Practices

// Timezone-aware sending
async function sendInUserTimezone(userId: string, templateId: string) {
  const user = await db.users.findById(userId);
  const userTime = new Date().toLocaleString('en-US', {
    timeZone: user.timezone
  });
  const hour = new Date(userTime).getHours();

  // Only send between 9 AM and 8 PM
  if (hour >= 9 && hour < 20) {
    return client.messages.send({
      to: [user.phoneNumber],
      template: { id: templateId }
    });
  } else {
    // Queue for next available window
    await queueForTomorrow(userId, templateId);
  }
}

Country-Specific Requirements

CountryRequirement
AustraliaSPAM Act compliance, unsubscribe within 5 days
CanadaCASL - express consent required
UKPECR regulations, opt-in required
SingaporePDPA compliance
BrazilLGPD data protection

RCS Compliance

RCS has built-in compliance features that simplify opt-out management:

  • STOP chip auto-injection: Every RCS message automatically includes a STOP suggestion chip. You do not need to add opt-out footer text manually — the platform handles this for you.
  • Inbound replies: When a user taps the STOP chip or sends an opt-out keyword, the platform updates the contact's opt_out flag and fires a message.received webhook with the user's text. Treat any inbound opt-out keyword (STOP / UNSUBSCRIBE / END / CANCEL / QUIT) the same way across channels.
const OPT_OUT_KEYWORDS = new Set(['STOP', 'UNSUBSCRIBE', 'END', 'CANCEL', 'QUIT']);

app.post('/webhooks/sent', async (req, res) => {
  res.sendStatus(200);
  const { field, sub_type, payload } = req.body;

  if (field === 'message' && sub_type === 'message.received') {
    const keyword = (payload.text ?? '').trim().toUpperCase();
    if (OPT_OUT_KEYWORDS.has(keyword)) {
      // The platform has already flipped the contact's opt_out flag — mirror it locally.
      await db.users.update(payload.from, {
        optedOutAt: new Date(),
        messagingConsent: false
      });
    }
  }
});

Opt-Out Handling at Send-Time

Sent runs a pre-send consent gate on every message. If the recipient's contact has opt_out = true, or their phone is on your phone-channel suppression list, the message is finalized as FAILED with the ERR_CONSENT_BLOCKED reason — no provider call is made and you are not charged.

When every recipient on a POST /v3/messages batch is opted out, the API rejects the request synchronously with BUSINESS_004. For partial opt-outs the request succeeds and only the blocked messages fail asynchronously.

app.post('/webhooks/sent', async (req, res) => {
  // Acknowledge immediately
  res.sendStatus(200);

  const { field, payload } = req.body;

  if (field === 'message' && payload.message_status === 'FAILED') {
    // Fetch the message detail to read the failure reason (description includes the error code,
    // e.g. ERR_CONSENT_BLOCKED for an opt-out / suppression block).
    const message = await client.messages.retrieveStatus(payload.message_id);
    const description = message.data?.description ?? '';

    if (description.includes('ERR_CONSENT_BLOCKED')) {
      await db.users.update(payload.inbound_number, {
        optedOutAt: new Date(),
        messagingConsent: false
      });
    }
  }
});

On this page