# Settings reference

Generated from `contracts/settings.schema.json` by `node docs/gen.mjs`. Do not edit by hand.

One JSON document per app. Read and write it in the dashboard (Settings) or with `GET` and `PUT /v1/app/settings` (see the [settings overview](/settings/overview/)). Every key has a default, so `{}` is valid. Unknown keys are refused. A `PUT` replaces the whole document and needs `If-Match` with the current version.

Cross-field rules: role keys are unique; every role named in `default_role`, `guests.role`, `policy`, `limits`, `onboarding.slides` and `calls.per_minute.payer_role` exists in `roles`; each `from`/`to` pair appears once in `policy`; `i18n.default_lang` is one of `i18n.languages`. The [settings overview](/settings/overview/) explains them.

## Top level

| Key | Type | Default | Meaning |
|---|---|---|---|
| `default_role` | role key | `"member"` | Role when the token has none. |
| `version` | `1` | `1` | Settings schema version. |

## branding

Look of the chat.

How the chat looks. The colours, the font and the corner radius apply everywhere the chat shows.

Primary is the colour of buttons, and `on_primary` is the text drawn on it, so pick a pair with enough contrast. Background, surface and text colour the chat's page, its cards and its words. A web font needs `font_css_url`, the stylesheet that defines it.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `branding.avatars` | boolean | `true` | Show the people's pictures; off draws the initial on the primary colour. |
| `branding.background` | color `#rrggbb` | `"#ffffff"` | Background color. |
| `branding.font_css_url` | https URL or null | `null` | Stylesheet that loads the font. |
| `branding.font_family` | string, up to 200 chars | `"system-ui, sans-serif"` | CSS font family. |
| `branding.heading_font_family` | string, up to 200 chars or null | `null` | CSS font family for headings; null uses font_family. |
| `branding.inbox_close_chip` | boolean | `false` | The inbox close button is a 30px tinted chip. |
| `branding.inbox_tint` | boolean | `false` | Tint the inbox background on every layout, not only two-pane. |
| `branding.launcher.enabled` | boolean | `true` | Show the launcher bubble. |
| `branding.launcher.inline_close_desktop` | boolean | `true` | Show the inbox close button on the inline page on a screen with a mouse. |
| `branding.launcher.plain` | boolean | `false` | Flat bubble colour, and Arial in the unread badge and the toast. |
| `branding.launcher.position` | `"right"`, `"left"` | `"right"` | Launcher corner. |
| `branding.logo_url` | https URL or null | `null` | Logo image. |
| `branding.on_primary` | color `#rrggbb` | `"#ffffff"` | Text on the primary color. |
| `branding.primary` | color `#rrggbb` | `"#2f6fed"` | Primary color. |
| `branding.radius_px` | integer, 0 to 32 | `12` | Corner radius in pixels. |
| `branding.surface` | color `#rrggbb` | `"#f4f5f7"` | Surface color. |
| `branding.text` | color `#rrggbb` | `"#111318"` | Text color. |

## calls

Video calls.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `calls.area` | string `^[A-Z_]{2,32}$` | `"GLOBAL"` | Agora area code, e.g. EUROPE. |
| `calls.codec` | `"vp8"`, `"vp9"`, `"h264"`, `"av1"` | `"vp8"` | Agora video codec. |
| `calls.confirm_start` | boolean | `false` | Ask before starting a call. |
| `calls.dock` | boolean | `false` | Every call takes the whole window, the chat button docks the call as a strip or a side rail beside the thread, and a minimise button shrinks it to a bubble. |
| `calls.enabled` | boolean | `false` | Video calls enabled. |
| `calls.max_duration_minutes` | integer, 1 to 1440 | `240` | Calls end after this. |
| `calls.offline_note` | boolean | `false` | After calling an offline peer, note in the thread that they may not answer. |
| `calls.per_minute.currency` | ISO 4217 code | `"EUR"` | ISO 4217 currency. |
| `calls.per_minute.enabled` | boolean | `false` | Charge calls per minute. |
| `calls.per_minute.payer_role` | role key or null | `null` | Role that pays; null is the caller. |
| `calls.per_minute.preauthorize_minutes` | integer, 1 to 60 | `1` | Minutes charged when both have joined. |
| `calls.per_minute.price_minor` | integer, at least 0 | `0` | Price per minute in minor units. |
| `calls.ring_timeout_seconds` | integer, 10 to 600 | `45` | Ring time before missed. |
| `calls.ring_when_closed` | boolean | `true` | An incoming call opens the chat and rings while it is closed; off shows the toast and sound, and the ring when the chat opens. |
| `calls.show_locked` | boolean | `false` | Show buyers a locked call button with a tip when they cannot call a seller. |
| `calls.single_card` | boolean | `false` | One card per call that reads Call ended when it is over, and a busy callee closes the caller's call screen instead of showing an error. |
| `calls.video.fps` | integer, 1 to 60 | `30` | Capture frame rate. |
| `calls.video.height` | integer, 120 to 2160 | `720` | Capture height in pixels. |
| `calls.video.width` | integer, 160 to 3840 | `1280` | Capture width in pixels. |
| `calls.warn_if_offline` | boolean | `true` | Warn before calling an offline peer. |

## chat

Messages.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `chat.avatar_upload` | boolean | `false` | Users may upload their own picture; it shows only after photo moderation approves it, and it wins over the host's avatar. |
| `chat.consent_allow_last` | boolean | `false` | The photo consent prompt puts Don't allow before Allow; off puts Allow first. |
| `chat.earlier_on_scroll` | boolean | `false` | Older messages load when the thread scrolls to its top; off shows a Load earlier button. |
| `chat.edit_window_seconds` | integer, 0 to 31536000 | `172800` | Senders may edit a text message this long after sending; 0 turns edits off. |
| `chat.photo_max_bytes` | integer, 1 to 50000000 | `20000000` | Maximum photo upload size in bytes. |
| `chat.photos` | boolean | `true` | Photo messages enabled. |
| `chat.refuse_blocked_send` | boolean | `false` | A message to someone who blocked the sender is refused (403 forbidden, reason undelivered) and not stored; off accepts it and drops it silently. |
| `chat.short_day_labels` | boolean | `false` | Day separators read day and month only; off adds the weekday, and the year for earlier years. |
| `chat.text_max_length` | integer, 1 to 10000 | `4000` | Maximum message length. |
| `chat.unsend_window_seconds` | integer, 0 to 31536000 | `172800` | Senders may delete a message for everyone this long after sending; 0 turns it off. Deleting for oneself is always allowed. |
| `chat.video_max_bytes` | integer, 1000000 to 209715200 | `104857600` | Maximum video size in bytes; the browser converts to fit. |
| `chat.video_max_seconds` | integer, 1 to 3600 | `900` | Maximum video length in seconds. |
| `chat.videos` | boolean | `true` | Video messages enabled. |
| `chat.view_once` | boolean | `true` | Senders may mark a photo, video or voice note view once. |
| `chat.voice` | boolean | `true` | Voice notes enabled. |
| `chat.voice_max_bytes` | integer, 100000 to 52428800 | `10485760` | Maximum voice note size in bytes. |
| `chat.voice_max_seconds` | integer, 1 to 3600 | `900` | Maximum voice note length in seconds. |

## commerce

Selling services.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `commerce.catalog_source` | `"webhook"`, `"dashboard"` | `"webhook"` | Catalog from the catalog callout or the dashboard. |
| `commerce.checkout_url_template` | https URL or null | `null` | Checkout page; placeholders {purchase_id} {item_id} {amount_minor} {currency} {seller_external_id} {buyer_external_id} {lang} {return_url}. |
| `commerce.custom_amount.enabled` | boolean | `false` | Buyers may type an amount, such as a tip. |
| `commerce.custom_amount.max_minor` | integer, at least 1 | `100000` | Largest amount in minor units. |
| `commerce.custom_amount.min_minor` | integer, at least 1 | `100` | Smallest amount in minor units. |
| `commerce.enabled` | boolean | `false` | Selling in conversations enabled. |
| `commerce.payment_mode` | `"server_charge"`, `"page_callback"`, `"checkout_url"` | `"server_charge"` | How purchases are paid. |
| `commerce.payment_window` | `"popup"`, `"same_tab"` | `"popup"` | Where checkout and top-up pages open. |
| `commerce.show_balance` | boolean | `false` | Show the balance the catalog callout returns. |
| `commerce.topup_url_template` | https URL or null | `null` | Top-up page for the purchase that ran short; same placeholders as checkout. |
| `commerce.trim_whole_prices` | boolean | `false` | A euro price on a purchase card reads `5 €` or `5.50 €`, never grouped. |

## guests

Anonymous guests.

Guests get a random id `guest:<uuid>` and the role in `guests.role`. The host page keeps their token in `localStorage`; see the security model.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `guests.enabled` | boolean | `false` | Allow anonymous guests. |
| `guests.merge_on_login` | boolean | `true` | Merge a recent guest into the member who logs in on that browser. |
| `guests.name_prefix` | string, up to 32 chars | `"Guest"` | Guests show as "&lt;prefix> &lt;6 chars>". |
| `guests.notify_prompt` | boolean | `false` | While the viewer's own message is the last one, offer to tell them when the peer replies; tapping sends `navigate` reason `notify` for the host to collect an address. Same viewers as the nudge. |
| `guests.notify_prompt_signup` | boolean | `false` | The notify prompt's button reads Create account and opens `guests.nudge.signup_url` (`navigate` reason `signup`) instead of Notify me; with no `signup_url` it stays Notify me. |
| `guests.notify_prompt_sticky` | boolean | `false` | Keep the notify prompt once the conversation has a message, until the viewer taps or dismisses it, instead of only while their own message is the last one. |
| `guests.nudge.enabled` | boolean | `true` | Show guests a create-account nudge, and members whose role is `guests.role` when that is not `default_role`. |
| `guests.nudge.login_url` | https URL or null | `null` | Where the nudge sends to log in; placeholder {lang}. |
| `guests.nudge.signup_url` | https URL or null | `null` | Where the nudge sends to sign up; placeholder {lang}. |
| `guests.role` | role key | `"member"` | Role given to guests. |

## i18n

Languages and copy.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `i18n.copy` | map of `"it"`, `"en"`, `"es"`, `"fr"` to map of key to string | `{}` | Text overrides per language and message key. |
| `i18n.default_lang` | `"it"`, `"en"`, `"es"`, `"fr"` | `"en"` | Language when nothing else decides. |
| `i18n.languages` | list of `"it"`, `"en"`, `"es"`, `"fr"` | `["en"]` | Languages offered. |

## limits

Limits per role.

One entry per role. A role with no entry gets the defaults shown on each key. `null` means unlimited.

Counts run over the last 24 hours, except messages per minute. A user who reaches a cap is refused with `limit_reached` and the reason names the cap. `messages_to_unanswered` stops a sender from piling up messages before the peer has ever replied, which is the usual guard against unwanted messages to a seller. A `perms` claim in the client token replaces any of these for that one user.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `limits` | map of role key to object | `{"member":{"messages_per_minute":30,"messages_to_unanswered":null,"new_conversations_per_day":null,"photos_per_day":null,"voice_per_day":100}}` | Limits per role. |
| `limits.<role>.messages_per_minute` | integer, at least 1 or null | `30` | Messages per minute; null is unlimited. |
| `limits.<role>.messages_to_unanswered` | integer, at least 0 or null | `null` | Messages before the peer first replies; null is unlimited. |
| `limits.<role>.new_conversations_per_day` | integer, at least 0 or null | `null` | New conversations per 24h; null is unlimited. |
| `limits.<role>.photos_per_day` | integer, at least 0 or null | `null` | Photos and videos per 24h; null is unlimited. |
| `limits.<role>.voice_per_day` | integer, at least 0 or null | `100` | Voice notes per 24h; null is unlimited. |

## links

Links to the client's site.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `links.open_conversation_url_template` | https URL or null | `null` | Page that opens a conversation; placeholders {peer_external_id} {lang}. |
| `links.profile_sellers_only` | boolean | `false` | The conversation header opens the profile of sellers only. |
| `links.profile_url_template` | https URL or null | `null` | Profile page; placeholders {external_id} {lang}; null lets the host resolve. |
| `links.services_menu` | boolean | `true` | With services_page, buyers also get a Services menu item. |
| `links.services_page` | boolean | `false` | Buyers can open the seller's services page from a conversation. |
| `links.services_url_template` | https URL or null | `null` | Services page; placeholders {external_id} {lang}; null lets the host resolve. |
| `links.support_email` | email or null | `null` | Shown to banned users. |

## moderation

Moderation.

We host adult content. Sex, nudity, erotic talk and selling sexual content between adults are allowed: the classifiers never flag them and there is no category for them. They look only for the categories below: text and photos after delivery, and calls through Agora's moderation when `moderation.calls.agora` is on, with the peer sampling a participant whose Agora task did not start when `moderation.calls.fallback_capture` is on.

- `Minors`: any sign a participant is or could be under age, or any request for content of minors.
- `Trafficking`: a third party controls a participant (a "manager" or "owner", debts paid with sex work, no control of their own money or papers).
- `NonConsent`: sextortion and sexual coercion, including threats to show or publish images, and pressing on after a clear no. Agreed role play between adults is not this.
- `Violence`, `SelfHarm`, `Hate`, `Harassment`, `Doxxing`, `Fraud`: what the names say.
- `IllegalServices`, `Weapons`, `Drugs`: selling or showing them.
- `ExternalPayment`: the parties agree that money for the service changes hands outside your site. A business rule, not a safety one.

Actions: `off` ignores the category. `alert` opens a case in the dashboard, sends `moderation.flagged`, and fires alert rules; the content stays delivered. `hold` also hides the message or photo from the peer until a moderator acts. `block` also removes the message or rejects the photo. Every category defaults to `alert`, and a finding in a category with no key here (some photo categories) acts as `alert`, so nothing is hidden until you choose to. Most adult sites set `Minors`, `Trafficking` and `NonConsent` to `block`.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `moderation.calls.agora` | boolean | `true` | Agora call moderation enabled. |
| `moderation.calls.capture_interval_seconds` | integer, 5 to 300 | `10` | Seconds between frames a peer captures. |
| `moderation.calls.capture_max_frames` | integer, 1 to 360 | `30` | Frames captured per participant per call. |
| `moderation.calls.fallback_capture` | boolean | `true` | Peers sample a participant whose Agora task did not start. |
| `moderation.photos` | boolean | `true` | Moderate photos. |
| `moderation.rules.Alcohol` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Alcohol findings. |
| `moderation.rules.Animals` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Animals findings. |
| `moderation.rules.Doxxing` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Doxxing findings. |
| `moderation.rules.Drugs` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Drugs findings. |
| `moderation.rules.ExternalPayment` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for ExternalPayment findings. |
| `moderation.rules.Fraud` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Fraud findings. |
| `moderation.rules.Harassment` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Harassment findings. |
| `moderation.rules.Hate` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Hate findings. |
| `moderation.rules.IllegalServices` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for IllegalServices findings. |
| `moderation.rules.Minors` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Minors findings. |
| `moderation.rules.NonConsent` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for NonConsent findings. |
| `moderation.rules.Political` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Political findings. |
| `moderation.rules.SelfHarm` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for SelfHarm findings. |
| `moderation.rules.Trafficking` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Trafficking findings. |
| `moderation.rules.Violence` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Violence findings. |
| `moderation.rules.Weapons` | `"off"`, `"alert"`, `"hold"`, `"block"` | `"alert"` | Action for Weapons findings. |
| `moderation.text` | boolean | `true` | Moderate text messages. |
| `moderation.videos` | boolean | `true` | Moderate videos. |
| `moderation.voice` | boolean | `true` | Transcribe and moderate voice notes. |

## notifications

Notifications.

In-frame alerts while the chat is open. mondochat never emails your users; see `webhooks`.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `notifications.banner` | boolean | `true` | Show a banner on new messages. |
| `notifications.banner_throttle` | boolean | `true` | At most one banner per 5s; off replaces it on every message (the sound stays throttled). |
| `notifications.sound` | boolean | `true` | Play a sound on new messages. |
| `notifications.toast_opens` | `"conversation"`, `"panel"` | `"conversation"` | Clicking the launcher toast opens its conversation, or only the panel. |

## onboarding

Onboarding slides.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `onboarding.enabled` | boolean | `false` | Show onboarding slides. |
| `onboarding.repeat_hours` | integer, at least 1 or null | `null` | Due again this many hours after it was last shown; null shows it once. |
| `onboarding.reshow_hours` | integer, at least 0 | `24` | Show again after this many hours. |
| `onboarding.slides` | map of role key to object | `{}` | Slides per role. |
| `onboarding.slides.<role>[].art` | `"composer"`, `"call_button"`, `"incoming_call"`, `"mail"` or null | `null` | Built-in drawing above the title, in the theme colours. |
| `onboarding.slides.<role>[].body` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Body per language. |
| `onboarding.slides.<role>[].bullets` | list of objects | `[]` | Icon bullets. |
| `onboarding.slides.<role>[].bullets[].body` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Body per language. |
| `onboarding.slides.<role>[].bullets[].icon` | `"card"`, `"shield"`, `"mail"`, `"lock"`, `"coin"` | required | Bullet icon. |
| `onboarding.slides.<role>[].bullets[].signup` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Link text per language that opens guests.nudge.signup_url. |
| `onboarding.slides.<role>[].bullets[].title` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Title per language. |
| `onboarding.slides.<role>[].image_url` | https URL or null | `null` | Slide image. |
| `onboarding.slides.<role>[].quote` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Highlighted note per language. |
| `onboarding.slides.<role>[].title` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Title per language. |

## policy

One row per ordered role pair; a missing pair allows nothing.

Rows are directional: `from` is the acting user's role, `to` the other user's. A `perms` claim in the client token overrides any policy flag (for every `to` role) and any limit for that one user, for example `{"call": false, "photos_per_day": 5}`.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `policy` | list of objects | `[{"block":true,"buy":false,"call":false,"from":"member","message":true,"offers":false,"photo_consent_required":false,"photos":true,"require_vouch":false,"see_presence":true,"start_conversation":true,"to":"member"}]` | One row per ordered role pair; a missing pair allows nothing. |
| `policy[].block` | boolean | `true` | May block the peer. |
| `policy[].buy` | boolean | `false` | May buy from the peer. |
| `policy[].call` | boolean | `false` | May start calls. |
| `policy[].from` | role key | required | Acting role. |
| `policy[].message` | boolean | `true` | May send messages. |
| `policy[].offers` | boolean | `false` | May send ad hoc offer cards. |
| `policy[].photo_consent_required` | boolean | `false` | The receiver must accept photos per conversation. |
| `policy[].photos` | boolean | `true` | May send photos. |
| `policy[].require_vouch` | boolean | `false` | Starting needs a vouch token or the session's peer. |
| `policy[].see_presence` | boolean | `true` | May see the peer's presence. |
| `policy[].start_conversation` | boolean | `true` | May start a conversation. |
| `policy[].to` | role key | required | Target role. |

## presence

Online status.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `presence.enabled` | boolean | `true` | Presence enabled. |
| `presence.fanout` | boolean | `true` | Also push presence changes to each peer's own channel. Turn off once every client subscribes with presence_pass. |
| `presence.in_inbox` | boolean | `true` | Show the online dot and last seen in inbox rows. |
| `presence.show_last_seen` | boolean | `true` | Show "last seen" times. |

## pwa

undefined

Lets people install the chat as an app on their phone and get notifications, without an app store. Turn on `enabled` only after you fill in the name and a 512 px icon, because it is refused without them.

A person signs in once from your own page, through `login_url`, and the device then stays signed in for 30 days of use. Pushes show "New message" unless `push_preview` is on, and only go to people who are offline and have not read the message. Off keeps devices and refuses them until it is turned on again.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `pwa.enabled` | boolean | `false` | The chat as an installable app: device sign-in, manifest and pushes; off keeps devices but refuses them. |
| `pwa.icon_url` | https URL or null | `null` | Square PNG of at least 512 px, edge to edge with the art inside the centre 80% circle (Android crops it to its shape); required when enabled. |
| `pwa.login_url` | https URL or null | `null` | Page of the client that signs the user in and redirects to a fresh app link; placeholder {lang}. |
| `pwa.name` | string, up to 45 chars or null | `null` | The name under the icon, 1 to 45 characters; required when enabled. |
| `pwa.push_preview` | boolean | `false` | Notifications show the message preview instead of "New message". |

## rate_limits

Rate limits.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `rate_limits.app_requests_per_second` | integer, 1 to 10000 | `50` | User requests per second per app. |

## retention

Data retention.

Deletions run daily. `null` keeps data until the user is deleted through the server API.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `retention.ip_log_days` | integer, at least 1 | `90` | Delete IP logs after. |
| `retention.messages_days` | integer, at least 1 or null | `null` | Delete messages after; null keeps. |
| `retention.photos_days` | integer, at least 1 or null | `null` | Delete photos after; null keeps. |

## roles

User roles.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `roles` | list of objects | `[{"key":"member","label":{"en":"Member"},"seller":false}]` | User roles. |
| `roles[].key` | role key | required | Role key, the value of the `role` claim. |
| `roles[].label` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Display name per language. |
| `roles[].seller` | boolean | `false` | Users with this role sell services. |

## safety

Blocking and reporting.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `safety.ban_close_button` | boolean | `true` | The banned panel has its own Close button. |
| `safety.report_reasons` | boolean | `true` | A report asks for a reason; off is a plain confirm. |

## webhooks

Webhook event options.

See https://docs.mondochat.io/webhooks/overview/. To notify users while the chat is closed, subscribe an endpoint to `message.created`, `conversation.unread` and `call.ringing` and send your own mail, push or SMS. Unread timers run only while an enabled endpoint subscribes to `conversation.unread`.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `webhooks.message_text` | boolean | `false` | message.created carries the text. |
| `webhooks.notify_kinds` | list of `"text"`, `"photo"`, `"video"`, `"voice"` | `["text","photo","video","voice"]` | Message kinds that make a first contact or an unread streak. |
| `webhooks.unread_after_minutes` | list of integer, 1 to 10080 | `[3]` | Send conversation.unread at each delay while the recipient still has unread messages; [] never. |
| `webhooks.unread_rearm` | boolean | `false` | After the last delay has fired, the next message opens a new streak, so a recipient who never reads is notified again. |

## workspace

Organising conversations.

| Key | Type | Default | Meaning |
|---|---|---|---|
| `workspace.aliases` | boolean | `false` | Users may give peers a private name. |
| `workspace.away_reply` | boolean | `false` | Sellers may set a reply sent for them, once a day per chat, while away. |
| `workspace.inbound` | boolean | `false` | Members may take messages from members only. |
| `workspace.notes` | boolean | `false` | Users may keep a private note on a peer. |
| `workspace.pins` | boolean | `false` | Users may pin up to 10 conversations. |
| `workspace.queue` | boolean | `false` | Inbox views, archive, search, spent and purchases, and the sign-up invite. |
| `workspace.quick_replies` | boolean | `false` | Sellers keep up to 20 saved replies and send one with a tap. |
| `workspace.reply_time` | boolean | `false` | Conversations with a seller carry her typical reply time. |
| `workspace.tiers` | list of objects | `[]` | Client tiers, lowest first; users get one by key. |
| `workspace.tiers[].color` | color `#rrggbb` | required | Badge color. |
| `workspace.tiers[].key` | role key | required | Tier key, the value of a user's `tier`. |
| `workspace.tiers[].label` | map of `"it"`, `"en"`, `"es"`, `"fr"` to string | `{}` | Display name per language. |