Skip to content

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.

  • 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.
  1. 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.

  2. Or read the document from your server with GET /v1/app/settings. The answer carries an ETag header 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"
  3. Replace the document with PUT /v1/app/settings. It replaces the whole document and needs If-Match with the version of your last GET. A stale version answers 412 precondition_failed. A successful write bumps the version and returns the new ETag.

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

    The route behaves like the dashboard’s settings endpoint for that app.

  4. 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.role and limits must name existing roles, so replace the default member there 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 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 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.

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.

A save is refused with 422 validation when:

  • A role named by default_role, guests.role, a policy row’s from or to, calls.per_minute.payer_role, a key of limits or a key of onboarding.slides is not a key of roles. guests.role is not allowed implicitly. There is one error per setting, and its loc is ["body", ...] followed by the path to that setting.
  • A URL template is not https.
  • pwa.enabled is on without pwa.name and pwa.icon_url.
  • workspace.away_reply or workspace.reply_time is on while presence.enabled is off.

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.

  • GET /v1/app/settings returns your document, and its ETag is one higher after a PUT.
  • A stale If-Match answers 412.
  • The chat reflects a change: for example, a policy row with call: true shows the call button.
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.
  • 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.
  • limits and onboarding.slides keys must be roles, and roles needs at least one entry.