# Record consent from a checkbox (/docs/build/consent)



> [!NOTE] Experimental
> The consent API is experimental: `relay.consent` and `useConsent()` can change in a minor release.

Your app shows the checkbox; Domino records what the member chose, when, and the words beside the checkbox. See [marketing consent](/docs/concepts/consent) for what Domino keeps.

## 1. Show the checkbox with its wording [#1-show-the-checkbox-with-its-wording]

Put a checkbox where the member decides, such as their account settings or after sign-in. Write the label your members agree to, including a link to your privacy policy. Domino stores that label word for word as the consent wording, so keep it as one string.

Leave the box unchecked until the member checks it.

## 2. Send each change [#2-send-each-change]

When the member checks the box, record the opt-in with the label they saw. When they uncheck it, record the opt-out. Each takes one call.

> [!DEV]
> With React, `useConsent()` reads the signed-in member's consent and changes it:
>
> ```tsx
> import { useConsent } from "@domino-sdk/relay/react"
>
> const label =
>   "Email me news and offers. See our privacy policy: https://example.com/privacy"
>
> export function MarketingEmail() {
>   const consent = useConsent()
>   const email = consent.data?.find((entry) => entry.channel === "email")
>
>   return (
>     <label>
>       <input
>         type="checkbox"
>         checked={email?.state === "granted"}
>         disabled={consent.isPending}
>         onChange={(event) =>
>           event.target.checked
>             ? consent.grant("email", label)
>             : consent.withdraw("email", label)
>         }
>       />
>       {label}
>     </label>
>   )
> }
> ```
>
> Without React, call the client directly:
>
> ```ts
> import { createBrowserRelayClient } from "@domino-sdk/relay/browser"
>
> const relay = createBrowserRelayClient({ baseUrl: "/relay" })
> const label =
>   "Email me news and offers. See our privacy policy: https://example.com/privacy"
>
> const current = await relay.consent.get()
> await relay.consent.set({ channel: "email", granted: true, wording: label })
> await relay.consent.set({ channel: "email", granted: false })
> ```
>
> `get()` lists one entry per channel the member chose on, each with `member`, `channel`, `state` (`granted` or `withdrawn`), `at` (epoch milliseconds, Relay's clock), `wording` and `source` (`member`, or `imported` for consent a [member import](/docs/use/imports) carried over, whose `at` is the old platform's time). A channel missing from the list has no record. `set()` answers the channel's current entry. `grant` and `withdraw` resolve true once saved and put a failure in `actionError`.

## 3. Check what was recorded [#3-check-what-was-recorded]

Read the member's consent back and show the checkbox from it, so the box matches what Domino has. Sending the same choice and wording again records nothing new.

> [!DEV]
> Domino refuses a change, with HTTP 422 and one of these codes, and records nothing:
>
> | Code | Cause |
> | --- | --- |
> | `consent-wording-required` | An opt-in without wording. Withdrawals may leave it out. |
> | `consent-wording-too-long` | Wording over 2,000 characters. |
> | `consent-wording-invalid` | Wording that is not plain text: a NUL character, or half of an emoji cut off. |
> | `unsupported-consent-channel` | Any channel but `email`. |
>
> `set()` takes an optional `actionId`: retrying with the same one records the change once. Domino stamps the time itself; a body that sends one is refused.
