Every send passes through one gate
email.send, sms.send, every broadcast and every automation step asks the same function the same question before anything leaves. There is no path around it, which is the point — a rule enforced in four places is a rule with three ways to forget it.
A refusal names which rule stopped it: suppressed, no_consent, consent_revoked or topic_archived, with the address, the reason and the time in the detail. The message is recorded as blocked rather than delivered, so a send that was refused never looks like a send that succeeded.
What the gate asks depends on one field. A send that names a topic is marketing. A send that names no topic is transactional — a sign-in code, a receipt, a password reset.
Suppression, and why email and SMS answer differently
There are five reasons an address is suppressed:bounce, complaint, unsubscribe, stop, and manual. You can add the last two yourself with audience.suppression.add; the others arrive from what actually happened.
On email they do not all mean the same thing. A bounce says the address did not accept mail, a complaint says the recipient reported it as spam, and a manual suppression is an operator saying do not contact this person — all three stop everything, transactional included. An unsubscribe is different in kind: it is somebody declining marketing, not asking to be locked out of the account they are signing in to. So an unsubscribe stops marketing and still lets a sign-in code through.
On SMS every suppression stops everything, and that difference is legal rather than aesthetic. The US carriers require a sender who receives STOP to cease all messages to that number, transactional included. There is no “but this one is a sign-in code” exception, so Lath does not offer one.
audience.suppression.list shows what is active, newest first, filterable by channel or reason.
A marketing send also needs a current grant
Clearing suppression is not permission. A send that names a topic additionally needs the most recent consent record for that contact, channel and topic to saygranted — no record at all is no_consent, and a revoked one is consent_revoked.
audience.consent.grant records the grant with the channel, the topic, what the person was shown and where it happened. That last part is the difference between having consent and being able to prove it a year later to somebody who is not inclined to believe you.
audience.consent.revoke appends a revocation rather than deleting the grant. The history is the record; erasing the earlier half of it would destroy the thing that makes the later half credible.
Archiving a topic with audience.topic.archive stops sending under it immediately — refused as topic_archived. It is deliberately not a suppression: nobody’s consent changed, so restoring the topic makes every existing grant count again exactly as it did.
Proving it: the opt-in record
audience.optin.get returns the complete record for one address: every grant and revocation with the wording shown, the source and the time; the suppression history including STOP, START, bounces and complaints; and whether a message may be sent right now, for transactional and for each marketing topic.
It is keyed by the address, not by a contact id, because that is what a carrier audit or a complaint names. An address with no contact record still has a history worth showing, and answering “we have no contact for that” to a regulator is not an answer.
It reports two verdicts rather than one, for the reason in the section above: on email a marketing opt-out does not stop a sign-in code, and on SMS a STOP stops everything. One combined answer would be wrong on one of the two channels.
audience.consent.list is the narrower version: the full consent history for a contact, newest first.
A bounce or a complaint lifts for the person, and for nobody else
audience.suppression.lift clears a suppression you added by hand or recorded from an unsubscribe. It refuses a bounce or a complaint, with suppression_protected.
Recording a fresh grant lifts more than that, but still not those two: a grant made through the API lifts unsubscribe and stop, because an opt-out is about permission and a customer who genuinely re-collected it may clear it. A bounce is not about permission — no assertion makes a dead address live — and a complaint puts every Lath customer’s sending reputation behind the next attempt.
The one thing that does lift them is the person, acting for themselves: subscribing through the hosted preferences page reached from a link in their own inbox. audience.preferences.set accepts the token from that link, which proves the person holds the address in a way a customer’s assertion cannot.
That page is public and needs no key — audience.preferences.get renders it, with the contact’s email and phone masked, because a preference link gets forwarded and sits in mail archives. It speaks for both channels symmetrically: every topic on email and on SMS, and a channel the contact has no address for is described rather than hidden, so the page can say so instead of implying the channel does not exist.
