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

# Messages and consent

> Every message this platform sends your users declares two things: what it says, and what it is worth. One of them is the recipient's decision and the other is your plan's.

Everything sent to one of your users — a verification link, a receipt, a win-back,
a release note — goes through one funnel, and every message declares two things
there. They look like one field and they are not.

## Two questions, never one

|                  | **Category**                                    | **Class**                                                                   |
| ---------------- | ----------------------------------------------- | --------------------------------------------------------------------------- |
| Asks             | May we write to this person about this at all?  | When the environment has spent its monthly email allowance, what gives way? |
| Answered by      | The recipient                                   | Your plan                                                                   |
| Lasts            | Until they change it                            | A month                                                                     |
| Where you set it | On the [message template](/en/guides/campaigns) | Nowhere — it is decided by what the message is                              |

Neither is derivable from the other, which is why both are declared. A receipt is
a **courtesy** and is not marketing: nobody unsubscribes from the answer to
something they did with their own address. A campaign is **marketing** and is not
free: it costs a send like everything else.

## Three categories, and transactional is not one of them

| Category       | What it covers                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------------- |
| `marketing`    | Outbound that exists to sell: a win-back, an upgrade nudge, an announcement of something for sale |
| `product_news` | What changed in the product                                                                       |
| `surveys`      | Being asked something                                                                             |

`product_news` is separate from `marketing` on purpose. Somebody who wants no
offers may still want to know their tools changed, and one switch for both loses
that person entirely. `surveys` is separate because the cost to the recipient is
different in kind — an announcement is read, an NPS prompt wants work.

**There is no `transactional` category, and there never will be.** A verification
link, a password reset, a receipt, a support reply and a dunning notice keep
arriving after somebody switches every one of the three off. That is not a rule
applied at send time: the value does not exist, so there is no row a settings
screen, a bulk update or a mistake could write to stop one.

## The class ranks messages, never moments

Free authentication is only free if the mail behind it is, so an environment has a
monthly email allowance. Exceeding it may never block a sign-in — and a message that
does not go out *is* a sign-up that does not complete, so "never block" cannot mean
"never withhold" either. It is resolved by ranking the messages:

| Class        | Examples                                                         | Past the allowance |
| ------------ | ---------------------------------------------------------------- | ------------------ |
| `security`   | A sign-in from a new address, a session revoked by support       | Never withheld     |
| `credential` | The magic link, the six-digit code, a feature waitlist admission | Never withheld     |
| `reply`      | A support agent answering a conversation the person started      | Never withheld     |
| `courtesy`   | The welcome note, a feature waitlist confirmation, a campaign    | Withheld           |

You do not set this. It is what the message *is*, and campaigns are `courtesy` —
a campaign withheld for budget is a campaign, while a magic link withheld for
budget is a sign-in refused with an extra step in front of it.

## Consent is honoured at the send, in one statement

The delivery record and the consent check are the same write. A message somebody
switched off is never composed rather than composed and filtered, and two sends
racing on the same contact cannot both read "has not opted out" and both go out.

For a campaign, the refusal is visible: the send is recorded as `suppressed` with
the reason, which is what lets support answer *why didn't they get it* with a fact.
An opt-out that lands between a send being claimed and the message being built
still stops it — the funnel checks again.

## The unsubscribe link

Every non-transactional message carries the link in its footer and the
`List-Unsubscribe` / `List-Unsubscribe-Post` header pair on its envelope. No
transactional message carries either: a receipt offering to stop sending receipts
is an offer this product does not make.

The link lands on a hosted page that names **your** product, because the tenant
travels in the link beside the token. Behind it:

```http theme={null}
GET  https://api.userkit.dev/v1/unsubscribe/{token}
POST https://api.userkit.dev/v1/unsubscribe/{token}
```

**The `GET` is a read and the `POST` is the act.** Mail gateways and security
scanners fetch every link in a message before a person sees it, so a `GET` that
unsubscribed would unsubscribe people who never clicked.

Neither takes a credential of any kind — the `uk_ns_…` token in the path is the
whole of it. That is the requirement rather than a gap: the reader clicked from an
inbox on a device they are not signed in on, and RFC 8058's one-click unsubscribe
is performed by the **mail client** with no browser at all. A descadastro that
first asks somebody to remember a password is a descadastro that fails, and its
next step is the spam button.

For the same reason the token **never expires and is never spent**. The mail
client posting on the reader's behalf and then the reader clicking the same link
are the two callers that actually happen, and both have to end in the same place.

One link switches off **one category for one contact**, and it is reversible from
the same page — `{"opted_out": false}` turns it back on. It never reveals the
address it was sent to: a token out of a forwarded email must not become a lookup
of whose inbox it came from.

## The person's own screen

```ts theme={null}
const preferences = await userkit.getNotificationPreferences();

await userkit.updateNotificationPreferences([{ category: "marketing", opted_out: true }]);
```

```json theme={null}
{
  "preferences": [
    { "category": "marketing", "label": "Novidades e ofertas", "opted_out": true },
    { "category": "product_news", "label": "Novidades do produto", "opted_out": false },
    { "category": "surveys", "label": "Pesquisas de satisfação", "opted_out": false }
  ]
}
```

`label` is the category as a person reads it, and the customer-facing copy this
platform ships is Portuguese today — render your own wording if your users read
something else.

The answer is always the **whole catalogue**, not the decisions that happen to be
stored: the absence of a stored decision *is* consent, so a screen rendering only
what exists would be an empty settings page for everybody who never opened it.
Transactional mail is not listed, because there is nothing here that could stop it.

A category this API does not know is refused **by name**, `transactional`
included. A consent decision dropped in silence is a screen that says *saved* over
a preference that was never stored, and the person finds out from the next email.

This is the one part of the contact's self-administration surface that does **not**
demand a verified session. The worst an unproven session can do here is switch a
category off for the contact it named — exactly what the token in that contact's
own inbox already does with no session at all. Demanding more proof for the in-app
control than for the emailed one would be the wrong way round.

An opt-out publishes a fact internally and is deliberately **not** delivered as a
[webhook](/en/guides/webhooks). It is the one withheld type whose reason is about
direction rather than permissions: what anybody would build on "this person
unsubscribed" is a message from their own system, which is the one message the
fact says must not be sent.

## What this platform sent one person

```bash theme={null}
curl "https://api.userkit.dev/v1/organization/contacts/{id}/messages?environment=live" \
  -H "Authorization: Bearer uk_st_…" \
  -H "X-Organization-Id: org_4b1e…"
```

One ordered read over the three things that send — campaigns, in-app
notifications, and email including the transactional kind — so a password reset
sits beside the marketing. It is gated with `customers:read` rather than
`messaging:manage`: composing a campaign and answering *did the reset arrive?* are
different jobs, and the second belongs to whoever may already see the person.

**Read `tracked` before you read the four timestamps.** When it is `false`,
nothing was ever going to report on that message — no delivery webhook is
configured for the sending domain — so `delivered_at`, `opened_at`, `clicked_at`
and `bounced_at` are `null` because nobody measured them, not because the message
failed. Render that as *not tracked*. When it is `true`, `null` means *not yet*.

## Permissions

`messaging:manage` — owner and admin — covers templates and campaigns, reads
included: an unsent price change is not something a wider permission should be
able to read. The delivery log above is `customers:read`, and nothing here is
gated by a plan except **arming** a campaign.
