> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trylath.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Every Lath operation is POST https://platform.trylath.com/<operation name with dots replaced by slashes>, with a JSON body and `Authorization: Bearer <key>`. `email.send` is POST /email/send.
> Branch on `error.code`, never on `error.message`. Every refusal also carries `error.fix`, which names the next step.
> Send an `Idempotency-Key` header on any operation that is not retry-safe, so a retry cannot run it twice.
> A `lath_test_` key emails only the account's own members and sends no SMS; a `lath_live_` key reaches real recipients and is billed.
> The OpenAPI document, generated from the same registry as the routes, is at https://platform.trylath.com/openapi.json.

# Consent and suppression

> One question is asked before any message leaves: may this address receive this kind of message on this channel? This is how it is answered, and how you prove the answer later.

## 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.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/audience/suppression/add \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"channel":"email","address":"someone@example.com","reason":"manual"}'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  const { result } = await lath.audience.suppression.add({
    channel: "email",
    address: "someone@example.com",
    reason: "manual",
  });
  ```

  ```bash CLI theme={null}
  lath audience suppression add \
    --channel email \
    --address someone@example.com \
    --reason manual
  ```
</CodeGroup>

## 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 say `granted` — 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.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/audience/consent/grant \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "contactId":"…",
      "channel":"email",
      "topic":"promotions",
      "source":"checkout form, 2026-09-13",
      "wording":"Email me about offers and new products."
    }'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  const { result } = await lath.audience.consent.grant({
    contactId: "…",
    channel: "email",
    topic: "promotions",
    source: "checkout form, 2026-09-13",
    wording: "Email me about offers and new products.",
  });
  ```

  ```bash CLI theme={null}
  lath audience consent grant \
    --contactId '…' \
    --channel email \
    --topic promotions \
    --source 'checkout form, 2026-09-13' \
    --wording 'Email me about offers and new products.'
  ```
</CodeGroup>

## 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.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/audience/optin/get \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"channel":"sms","address":"+14155550100"}'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  const { result } = await lath.audience.optin.get({ channel: "sms", address: "+14155550100" });
  ```

  ```bash CLI theme={null}
  lath audience optin get --channel sms --address +14155550100
  ```
</CodeGroup>

## 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.
