Settings overview
An app has one settings document for everything that can differ between sites: roles, policy, limits, calls, branding, languages, moderation rules and webhook options. Every key has a default, so {} is valid and omitted keys keep their defaults. Unknown keys are rejected.
Before you start
Section titled “Before you start”- Admin or owner access to the dashboard, or the app’s server secret and app id. See Tenants and apps.
- The roles you need. Every setting that names a role must name a key of
roles.
-
Edit in the dashboard. Open App. The form is built from the settings schema, and every field shows its default and description. The settings reference lists every key by group.
-
Or read the document from your server with
GET /v1/app/settings. The answer carries anETagheader with the current version, quoted:Terminal window curl -i https://app.mondochat.io/v1/app/settings \-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" -
Replace the document with
PUT /v1/app/settings. It replaces the whole document and needsIf-Matchwith the version of your lastGET. A stale version answers 412precondition_failed. A successful write bumps the version and returns the newETag.Terminal window curl -X PUT https://app.mondochat.io/v1/app/settings \-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \-H 'If-Match: "1"' -H 'content-type: application/json' -d @settings.jsonThe route behaves like the dashboard’s settings endpoint for that app.
-
Start from a typical cam site: fans and models, fans start conversations, both sides can call.
{"roles": [{"key": "fan", "label": {"en": "Fan"}},{"key": "model", "label": {"en": "Model"}}],"default_role": "fan","guests": {"role": "fan"},"policy": [{"from": "fan", "to": "model", "start_conversation": true, "call": true},{"from": "model", "to": "fan", "start_conversation": false, "call": true}],"limits": {"fan": {}, "model": {}},"calls": {"enabled": true},"links": {"open_conversation_url_template": "https://www.example.com/{lang}/messages?with={peer_external_id}"}}guests.roleandlimitsmust name existing roles, so replace the defaultmemberthere too.
roles lists at least one role: {key, label: {lang: text}, seller}. seller: true marks the roles that get seller features such as the installed app. A role claim in a client token carries one of these keys, and default_role applies when the claim is missing.
Policy
Section titled “Policy”policy has one row per ordered pair of roles, from and to. A missing pair allows nothing. Each row sets these flags:
| Flag | Meaning |
|---|---|
start_conversation |
from may start a conversation with to. |
message |
from may message to. |
call |
from may call to. |
photos |
from may send photos, videos and voice notes. |
photo_consent_required |
to must accept photos per conversation. |
block |
from may block to. |
see_presence |
from may see to’s presence. |
require_vouch |
from needs a vouch to start a conversation. See Embed. |
offers |
from may send ad hoc offer cards. |
buy |
from may buy from to. |
When a row forbids an action, the API answers 403 policy_denied with reason set to the flag name. The chat gets the resolved policy for the viewer and hides what the API would refuse.
Limits
Section titled “Limits”limits holds a limit set per role, and null means unlimited. The defaults are 30 messages per minute and 100 voice notes per day. The other limits are new_conversations_per_day, messages_to_unanswered (messages before the peer first replies) and photos_per_day (photos and videos together). A reached limit answers 403 limit_reached with reason set to the limit name. Per-app request rate is rate_limits.app_requests_per_second, 50 by default.
Per-user overrides with perms
Section titled “Per-user overrides with perms”A perms claim in the client token, or perms on a server upsert, overrides any policy field, applied to every to role, and any limits field for that user. For example {"call": false, "photos_per_day": 5}. The claims are stored on the user.
Cross-field rules
Section titled “Cross-field rules”A save is refused with 422 validation when:
- A role named by
default_role,guests.role, a policy row’sfromorto,calls.per_minute.payer_role, a key oflimitsor a key ofonboarding.slidesis not a key ofroles.guests.roleis not allowed implicitly. There is one error per setting, and itslocis["body", ...]followed by the path to that setting. - A URL template is not https.
pwa.enabledis on withoutpwa.nameandpwa.icon_url.workspace.away_replyorworkspace.reply_timeis on whilepresence.enabledis off.
URL templates and their fallbacks
Section titled “URL templates and their fallbacks”Placeholders such as {lang} are filled by the chat or by the API, and values are URL-encoded. While a template is unset the host gets a navigate event with no url, or the field that would carry the link stays empty.
| Key | Placeholders | What a set value does, and what happens while it is unset |
|---|---|---|
links.open_conversation_url_template |
{peer_external_id} {lang} |
Builds open_url, the link back to the conversation, in message.created, conversation.unread and call.ringing, in the recipient’s language. While it is unset, open_url stays empty (null). |
links.profile_url_template |
{external_id} {lang} |
Where the conversation header opens the peer’s profile. While it is unset, the host gets a navigate event with reason profile, no url and the peer’s external_id, and resolves the page itself. |
links.services_url_template |
{external_id} {lang} |
Where a buyer opens a seller’s services page. While it is unset, the host gets a navigate event with reason services, no url and the seller’s external_id. |
commerce.checkout_url_template |
{purchase_id} {item_id} {amount_minor} {currency} {seller_external_id} {buyer_external_id} {lang} {return_url} |
Used with commerce.payment_mode set to checkout_url: the purchase answers a redirect to it and the chat sends a navigate event with reason checkout, or opens it in a popup. While it is unset in that mode, no navigate event is sent: the purchase is refused with 403 policy_denied and reason checkout_url_template. |
commerce.topup_url_template |
same as checkout | Where a buyer goes when a charge answers insufficient and the callout gave no topup_url: a navigate event with reason topup and this url. While it is unset, the host gets a navigate event with reason topup, no url, and purchase_id, amount_minor, currency and the seller’s external_id, so your site runs its own top-up. |
guests.nudge.signup_url |
{lang} |
The nudge’s sign-up button sends a navigate event with reason signup. While it is unset, that button is hidden, the invite to sign up is refused with 403 policy_denied and reason invite, and a notify prompt keeps its Notify me button. |
guests.nudge.login_url |
{lang} |
The nudge’s log-in button sends a navigate event with reason login. While it is unset, that button is hidden and no navigate event is sent. |
The nudge shows only while guests.nudge.enabled is on. notify navigations have no URL template: the host verifies the viewer and collects an address itself. See Embed.
Check it works
Section titled “Check it works”GET /v1/app/settingsreturns your document, and itsETagis one higher after aPUT.- A stale
If-Matchanswers 412. - The chat reflects a change: for example, a policy row with
call: trueshows the call button.
When it fails
Section titled “When it fails”| Answer | Cause |
|---|---|
412 precondition_failed |
If-Match is not the current version. GET again and resend. |
422 validation |
An unknown key, a role that is not in roles, a template that is not https, or another cross-field rule. errors[].loc points at the setting. |
401 unauthorized |
The secret or the app id is missing, wrong or revoked. |
403 policy_denied or limit_reached |
Your settings forbid the user’s action. reason names the rule. |
Limits
Section titled “Limits”- The whole document is replaced on every
PUT, so send every key you want to keep. - Each API process caches settings for 30 seconds, and a write clears the cache of the process that handled it.
- Every write is audited.
limitsandonboarding.slideskeys must be roles, androlesneeds at least one entry.