Compliance Requirements
Zyphr gives you the machinery to comply with CAN-SPAM and TCPA. Some of it only works once you configure it.
This page lists what Zyphr does automatically and what needs an action from you. If you send marketing email or any SMS, read the whole thing — the gaps below are the ones that carry real legal exposure.
Under CAN-SPAM you are the sender. Under TCPA you are the party who obtained (or didn't obtain) consent. We can enforce rules and keep records, but the legal obligations are yours, and nothing here is legal advice.
Quick checklist
| Action | Needed if you send | Without it |
|---|---|---|
| Configure the inbound SMS webhook | Any SMS | STOP replies are never received |
| Record consent for phone numbers | Marketing SMS | No evidence if challenged |
| Supply recipient timezones | Any SMS | Quiet hours can't be enforced |
| Set your postal address | Marketing email | Missing a required element |
| Classify your messages | Anything | Rules default to the strictest |
1. Inbound SMS webhook (STOP handling)
This is the most important item on this page.
TCPA requires you to honor opt-out requests. The FCC treats these keywords as conclusively revoking consent:
STOP · QUIT · END · REVOKE · OPT OUT · CANCEL · UNSUBSCRIBE
Zyphr can process these automatically — suppressing the number and revoking consent the moment a reply arrives. But your SMS provider has to tell us the reply happened.
Because you use your own provider account, we cannot configure this for you.
Setup (Twilio)
- Get your webhook URL from Dashboard → Project Settings → SMS. It looks like:
https://api.zyphr.dev/v1/webhooks/sms/twilio/YOUR_PROJECT_TOKEN - In the Twilio Console, go to Phone Numbers → Manage → Active Numbers and select your number.
- Under Messaging → A MESSAGE COMES IN, set the webhook to that URL with method HTTP POST.
- Save.
Verifying it works
Text STOP to your number from a phone you control, then check Dashboard → SMS → Inbound. You should see the message with a matched keyword. Sending to that number afterwards should be blocked.
Replies go to your provider and stop there. From Zyphr's side the recipient never opted out, and we will keep delivering — which is exactly the scenario TCPA penalties exist for.
Other providers
Only Twilio is supported today. Vonage, MessageBird, Plivo, Telnyx and Sinch return 501 Not Implemented — each uses a different signature scheme, and we would rather return an honest error than accept unverified webhooks that could be forged to revoke consent for arbitrary numbers.
If you use one of these, handle STOP replies in your own application and call the suppression API.
2. Phone consent records
TCPA recognizes two tiers of consent, and they are not interchangeable:
| Tier | What it covers |
|---|---|
express | Transactional/informational only — OTPs, alerts, receipts |
written | Required for marketing. Prior express written consent |
Zyphr stores consent per phone number, per channel, with the evidence you would need if challenged: timestamp, source, IP, user agent, and the exact disclosure text the person agreed to.
Recording consent
await zyphr.consent.record({
phone: '+14155551234',
tier: 'written',
source: 'web_form',
disclosure_text: 'I agree to receive marketing texts from Acme. ' +
'Msg & data rates may apply. Reply STOP to opt out.',
source_ip: request.ip,
});
disclosure_text is the single most valuable field. Store the words you actually showed, not a summary.
What happens without a record
| Consent on file | Marketing send | Transactional send |
|---|---|---|
written | ✅ delivered | ✅ delivered |
express | ❌ blocked | ✅ delivered |
| Revoked | ❌ blocked | ❌ blocked |
| Nothing recorded | ⚠️ delivered, flagged | ⚠️ delivered, flagged |
Nothing recorded means we let it through. Blocking every un-recorded number would break existing traffic overnight — so we allow and flag instead. That is a coverage gap on your side, not permission: you still need the consent, we just cannot prove you have it.
3. Recipient timezones (quiet hours)
TCPA prohibits texts before 8:00 AM or after 9:00 PM in the recipient's local time.
Zyphr enforces this — but only when it knows where the recipient is. Pass a timezone with the send:
await zyphr.sms.send({
to: '+14155551234',
body: 'Your order has shipped',
recipient_timezone: 'America/New_York', // IANA name
classification: 'transactional',
});
Or set a project-wide default in Dashboard → Project Settings → SMS → Quiet Hours.
Why we don't guess
We deliberately do not infer a timezone from the area code. People keep their numbers when they move, so a 212 number in Los Angeles would get New York quiet hours. A control that silently applies the wrong window is worse than none: it produces a compliance record showing enforcement that never happened.
Without a timezone we send the message and record that quiet hours could not be evaluated.
America/New_York, not EST. Abbreviations like EST and MST are fixed-offset and ignore daylight saving — they are an hour wrong for half the year, so we reject them.
Messages caught by quiet hours are deferred, never dropped — they send at the next permitted time.
4. Postal address (CAN-SPAM)
Every commercial email must include your valid physical postal address: a street address, USPS PO Box, or registered private mailbox.
This is the most commonly missed CAN-SPAM element.
Set it in Dashboard → Project Settings, then include it in your templates.
Putting it in a template
Zyphr injects your project's address as a reserved template variable, so you don't have to hard-code it into every layout — update it in one place and every template follows:
If no address is set, it renders as empty rather than failing the send. That keeps a missing address from taking your mail offline, but it also means an empty footer is your signal that the field is unset — check Project Settings.
project is reservedIf you pass your own variable named project, yours wins and the address will not render. Name your own variables something else to avoid the collision.
Transactional email is exempt — password resets and receipts do not need it.
5. Message classification
Several rules above depend on whether a message is transactional or marketing. Tell us which:
await zyphr.emails.send({
from: 'hello@yourdomain.com',
to: 'customer@example.com',
subject: 'Summer sale',
html: '<p>...</p>',
classification: 'marketing', // or 'transactional'
});
You can also set a default per template, which a send may override.
What classification changes
transactional | marketing | |
|---|---|---|
| Unsubscribe link required | No | Yes |
| Postal address required | No | Yes |
| SMS consent tier | express | written |
| Quiet hours | Exempt | Enforced |
Unclassified messages are treated as marketing — the stricter reading. If you send transactional traffic without classifying it, you may hit rules that don't apply to you.
Password resets, magic links, OTP and verification sent through Zyphr Auth are classified transactional automatically, and never carry unsubscribe links — a user cannot accidentally opt out of their own account recovery.
What Zyphr does automatically
- Suppression enforcement on every send — unsubscribes, bounces and complaints
- One-click unsubscribe (RFC 8058) on platform email, as Gmail and Yahoo require
- STOP keyword processing once the inbound webhook is configured
- Quiet-hours deferral when a timezone is known
- Consent tier checks on SMS when a record exists
- Auth email exemptions so account recovery always reaches the user
What remains yours
- Obtaining lawful consent before sending
- The accuracy of your disclosure text
- Your postal address and sender identity
- Classifying your own messages
- Configuring the inbound webhook