## Update channel settings

**put** `/v1/settings`

Updates your brand's editable settings.

**Behavior**

- An absent field keeps its current value; send only what you mean to
  change.
- `bump_policy_version` is the one explicit act: a version bump
  re-notifies customers and never happens as a side effect.
- Business hours are one setting spelled in two keys: send either
  `business_hours` or `hours_timezone` and the other falls back to what
  is stored; the merged pair is validated whole and written whole, so a
  refusal leaves the stored week untouched. Clear both together with
  `"business_hours": {}` and `"hours_timezone": ""`.
- Returns the updated view, exactly as `GET /v1/settings` would.
- `escalation_handling` is the optional opt-in for having us run
  escalation (`managed`); every brand starts as `self_managed`. The
  field's own documentation carries the full contract and what the
  `chat.trigger_word` event looks like in each mode.

**Errors**

- HTTP 422 `code` 1021: `escalation_handling` is
  neither `managed` nor `self_managed`.
- HTTP 409 `code` 2019: we hold your brand on `managed`
  escalation and the update named a differing value; re-sending the
  stored value still succeeds as a no-op.
- HTTP 413 `code` 1073: the hours document exceeds 4,096 bytes.
- HTTP 422: `business_hours_invalid` (wrong shape: an unknown weekday
  key, a range that is not `["HH:MM","HH:MM"]`, a time outside
  `00:00`–`24:00`), `business_hours_end_not_after_start`,
  `business_hours_overlap`, `business_hours_never_open`,
  `hours_timezone_invalid`, and `required` when hours arrive without a
  zone or a zone without hours.

### Body Parameters

- `agent_enabled: optional boolean`

  Switch your AI agent on or off; see `agent_enabled` on the response for
  what the pause does and does not cover. Absent leaves it as it is; there
  is no other way to spell "unchanged" for a boolean, so a settings update
  that omits this can never switch your agent off by accident.

- `bump_policy_version: optional boolean`

  `true` increments `policy_version`: the explicit act that re-notifies
  customers of a policy change. NEVER implicit: editing a URL alone does not
  bump, because an accidental bump re-messages every customer.

- `business_hours: optional unknown`

  Your opening times: a LIST of `["HH:MM","HH:MM"]` ranges per lowercase
  weekday key. Absent keeps the stored document; `{}` together with
  `hours_timezone: ""` CLEARS the hours, which is how "we do not publish
  hours" is spelled.

  `business_hours` and `hours_timezone` are ONE SETTING: send either and the
  other falls back to what is stored, and the merged pair is validated
  whole. Hours without a zone are refused (`422 required` on
  `/hours_timezone`) and a zone without hours is refused the same way on
  `/business_hours`, so clearing means sending BOTH empty, not one.

  THE WRITER IS STRICTER THAN THE READER, in three ways worth knowing before
  you compose a week (each has its own code, listed on the operation):
  `end` must come AFTER `start`: a range that reads back as crossing
  midnight is refused on write, and the message names the split-shift
  spelling to use instead; two ranges on one weekday may not share a minute
  (they may touch); and the week must open somewhere.

- `escalation_handling: optional "managed" or "self_managed"`

  Who runs escalation for this brand. OPTIONAL and an OPT-IN: every brand
  starts as `self_managed`, absent leaves your stored choice unchanged, and
  re-sending your stored value is a no-op, never an error. To opt into our
  desk, `PUT /v1/settings` with `{"escalation_handling": "managed"}`; the
  response is the full settings view with the new value. To take
  escalation back, send `"self_managed"` the same way.

  `self_managed` (the default): escalation is yours. We detect the
  request for a person and deliver the `chat.trigger_word` event with
  `handled_by: "partner"` and NO delegation fields: no `queued_for_human`,
  no `ack_by`. Nothing is queued, the chat's owner does not move, and your
  agent keeps the floor; your ordinary sends keep answering exactly as on
  any partner-owned chat, and none of them meet `409 chat_owned`, because
  the ownership that refusal guards never moved. The ABSENCE of
  `queued_for_human` on that event is how your webhook handler knows this
  escalation is yours; from there you decide whether to answer in place,
  hand the chat to your own human desk and tell us with `/handoff`, or
  call `/clarify` to opt that turn back into our escalation. The way to a
  person is your escalation experience, the one your Experience Review
  approved, and the live-human obligation in that review is yours. The
  event's own documentation shows a worked payload for each shape.

  `managed` (the opt-in): when a customer asks for a person, we queue
  them for a human at that moment and you enrich the acknowledgement,
  by `ack_by`.

  We may force a brand onto `managed` if monitoring shows its human path
  is not answering; while forced, a differing value is refused
  `409 escalation_handling_locked` and the response shows
  `escalation_handling_locked: true`. A value other than the two above is
  refused `422 escalation_handling_invalid`.

  - `"managed"`

  - `"self_managed"`

- `hours_timezone: optional string`

  The IANA zone your hours are stated in (`America/New_York`). Absent keeps
  the stored zone; `""` alongside an empty `business_hours` clears the pair.
  `Local` is refused (`422 hours_timezone_invalid`); it resolves to the
  serving container's zone, not yours.

- `privacy_policy_url: optional string`

  Absolute `https` URL, at most 2048 characters, or `""` to unset. `http` is refused (`400 invalid_url`): these URLs are rendered to customers.

- `richlink_default_image_att_id: optional string`

  An `att_…` id your brand uploaded (a PNG ≤ 200 KB), or `""` to clear back to the platform default. Validated at set time, same rules as a rich link's image.

- `support_contact: optional string`

  Free text, trimmed; stored to 320 characters.

- `terms_of_use_url: optional string`

  Same rules as `privacy_policy_url`.

### Returns

- `BrandSettings object { agent_enabled, app_extensions, brand_id, 16 more }`

  The brand's channel settings: what a brand edits itself, plus the managed groups and the link state, read-only.

  - `agent_enabled: boolean`

    Whether your AI agent is switched on. `true` for a brand that has never
    changed it.

    While this is `false` we stop delivering webhooks to your brand's
    endpoints, so your agent is told nothing and answers nothing. Events that
    occur during the pause are **never** delivered; not held and replayed
    when you switch back on, which is what makes this safe to use during an
    incident: your agent will not wake up and answer an hour of stale
    messages.

    The mute covers your WHOLE webhook feed, not only the events an agent
    answers: `message.sent` / `message.failed`, `consent.expired` and the
    invitation-batch events travel the same fan-out, so delivery outcomes and
    invitation status go dark too and do not backfill. The read side is
    unaffected and is the during-mute channel; `GET /v1/messages/{msg}`,
    `GET /v1/invitations/{inv}` and `GET /v1/chats/{chat}/events` all still
    answer, so nothing is lost from the record, only from the push.

    It does **not** stop you sending. Your API key keeps working while the
    switch is off, which is what lets a human desk carry a conversation your
    agent is muted for.

  - `app_extensions: object { apps }`

    The registered apps that are OPERATIVE for your brand, keyed by the same
    `app_key` space `PUT /v1/settings/integrations/app_extensions` writes:
    your stored registry when you have one, the deploy default otherwise, and
    `managed[name=app_extensions].source` says which.

    **Read this before assembling a write.** The registry's precedence unit is
    the WHOLE set: a write replaces your stored registry outright, and a stored
    registry masks the deploy default entirely. So an app you leave out of a
    write stops resolving on the send path: a `200` and a silently
    unregistered app. This object is what makes a correct whole-set write
    possible: it is the complete operative registry, so send it back with your
    edits applied rather than assembling one from your own records.

    Unlike `apple_pay` and `oauth` above, this key is ALWAYS PRESENT: an
    empty `apps` is how "nothing is registered" is said, and it is safe to
    edit from. A registry is never "unconfigured" the way a merchant can be:
    one is always operative, so there is always an answer. The only way this
    key is absent is a platform build older than the field, and a client that
    cares about the difference should read an absent key as "cannot speak to
    the registry" and refuse to write the group; never as an empty one.

    App icons are never returned, on the same terms as the certificate and the
    client secret above: `app_icon_set` per entry is the whole of it.

    - `apps: unknown`

      The complete operative registry, `app_key` → app. Empty when nothing is
      registered. Every key here is a key `PUT /v1/settings/integrations/app_extensions`
      accepts, and that write replaces this whole map, so edit this object and
      send it back rather than composing one.

  - `brand_id: string`

    The brand this key is scoped to: the same id `GET /v1/me` reports.

  - `business_id: string`

    The linked business id, `""` when no channel is linked. The full row is `GET /v1/channels`.

  - `consent_mode: "linq_managed" or "brand_attested"`

    Who captures consent GRANTS for this brand. `linq_managed` (the
    default): we capture grants, and a grant attested through
    `POST /v1/consent` answers HTTP 409 `code` 2014.
    `brand_attested`: you attest grants yourself through that endpoint.
    Read-only here: the mode is operator-set; contact support to change it.
    Opt-outs are honored in both modes; the mode governs only who may
    record a GRANT.

    - `"linq_managed"`

    - `"brand_attested"`

  - `display: string`

    The brand's display name.

  - `escalation_handling: "managed" or "self_managed"`

    Who runs escalation for this brand: your stored choice, exactly as you
    set it, and `self_managed` until you set one. See `escalation_handling`
    on the update request for what each value means. While
    `escalation_handling_locked` is `true` this stored value is not
    operative: we have forced the brand to `managed` escalation.

    - `"managed"`

    - `"self_managed"`

  - `escalation_handling_locked: boolean`

    `true` while we hold this brand on `managed` escalation regardless of
    its stored choice. It is the state we use when monitoring shows a
    partner-run human path not answering. Updates to `escalation_handling` are
    refused while it is `true`; contact support to release it.

  - `link_origin: string`

    The link's origin (`seed` | `connect` | `control`), `""` when no channel is linked.

  - `managed: array of ManagedSetting`

    The read-only groups, as display strings with a managed-by marker.

    - `managed_by: "linq"`

      Who changes it. Always `linq` today: these are registered with the platform as well as with us, so they change together; write to your support contact.

      - `"linq"`

    - `name: "apple_pay_merchant" or "app_extensions" or "oauth"`

      Which setting: the Apple Pay merchant, the registered iMessage apps, or customer authentication (OAuth).

      - `"apple_pay_merchant"`

      - `"app_extensions"`

      - `"oauth"`

    - `set: boolean`

      Whether the group is operative for a send, evaluated on the RESOLVED
      view: your brand's stored configuration when present, the deploy
      default otherwise. `false` means a send needing this group is refused
      (`409`) until it is configured.

    - `source: "brand" or "platform"`

      Which copy is operative: `brand` when your brand's stored configuration answers, `platform` when the deploy default does.

      - `"brand"`

      - `"platform"`

    - `value: string`

      A display string; `merchant.com.example`, `2 registered`, `configured`,
      `not configured`. Never key material, secrets or certificate references;
      this field is safe to render verbatim.

  - `policy_version: number`

    Monotonic policy version. Bumping it (see the update's `bump_policy_version`) is what re-notifies customers of a policy change, so it only ever moves explicitly.

  - `privacy_policy_url: string`

    Absolute `https` URL of the privacy policy, `""` when unset.

  - `richlink_default_image_att_id: string`

    The brand's rich-link default card image (`att_…`), `""` when the platform default stands.

  - `support_contact: string`

    The brand's support contact, `""` when unset.

  - `terms_of_use_url: string`

    Absolute `https` URL of the terms of use, `""` when unset.

  - `apple_pay: optional object { display_name, domain, merchant_id, merchant_identity_cert_set }`

    The brand's Apple Pay configuration, non-secret fields only. ABSENT when
    the group is not configured; never an object of empty strings, the same
    absent-means-unconfigured promise `business_hours` makes.

    The merchant-identity certificate is NEVER returned, here or anywhere:
    it goes up write-only and lives in the secret store. `merchant_identity_cert_set`
    is the whole of what this contract says about it, so a client can offer
    "leave blank to keep the stored certificate" without learning anything.

    - `display_name: string`

      The business name Apple renders on the payment sheet.

    - `domain: string`

      The verified payment domain the merchant session is requested for.

    - `merchant_id: string`

      The Apple Pay merchant identifier (`merchant.example.brand`).

    - `merchant_identity_cert_set: boolean`

      Whether a merchant-identity certificate is on file. The certificate
      itself is never returned; omit `merchant_identity_cert_pem` on a write to
      keep the stored one.

  - `business_hours: optional unknown`

    Business hours as stored: a LIST of `["HH:MM","HH:MM"]` ranges per
    lowercase weekday key (`mon`…`sun`), so a lunch break or a split shift is
    expressible.

    ABSENT: the key is not present at all; when the brand publishes no
    hours, never `{}` and never an invented 9-5. That is the same promise
    `GET /v1/chats/{chat}`'s `brand` makes about the same document: an
    unconfigured brand has no hours model and the platform treats it as always
    open, which is a different fact from "open 24/7 by policy" and is not
    spelled the same way.

  - `hours_timezone: optional string`

    The IANA zone the hours are stated in (`America/New_York`). Absent when
    unset. Hours are never configured without it; the two keys are
    independently optional here only because a direct operator edit can leave
    a zone behind with no hours.

  - `oauth: optional object { authorize_url, client_id, client_secret_set, 4 more }`

    The brand's OAuth configuration, non-secret fields only. ABSENT when the
    group is not configured. The client secret is never returned, on the same
    terms as the certificate above.

    - `authorize_url: string`

      Where the customer is sent to authorize.

    - `client_id: string`

      The OAuth client id.

    - `client_secret_set: boolean`

      Whether a client secret is on file. The secret itself is never returned;
      omit `client_secret` on a write to keep the stored one.

    - `issuer_url: string`

      The identity provider's issuer URL.

    - `redirect_uri: string`

      The redirect the provider returns to.

    - `scope: string`

      The space-separated scopes requested.

    - `token_url: string`

      Where the authorization code is exchanged.

### Example

```http
curl https://messages.api.linqapp.com/v1/settings \
    -X PUT \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{}'
```

#### Response

```json
{
  "brand_id": "biz_9f2c1a",
  "display": "CurbFare",
  "privacy_policy_url": "https://example.com/privacy",
  "terms_of_use_url": "https://example.com/terms",
  "policy_version": 3,
  "support_contact": "support@example.com",
  "richlink_default_image_att_id": "",
  "agent_enabled": true,
  "escalation_handling": "managed",
  "escalation_handling_locked": false,
  "consent_mode": "linq_managed",
  "business_hours": {
    "mon": [
      [
        "09:00",
        "12:00"
      ],
      [
        "13:00",
        "17:00"
      ]
    ],
    "sat": [
      [
        "10:00",
        "14:00"
      ]
    ]
  },
  "hours_timezone": "America/New_York",
  "business_id": "a884eddf-b0ad-4be4-9c0e-071531638768",
  "link_origin": "connect",
  "managed": [
    {
      "name": "apple_pay_merchant",
      "value": "not configured",
      "managed_by": "linq",
      "set": false,
      "source": "platform"
    },
    {
      "name": "app_extensions",
      "value": "none registered",
      "managed_by": "linq",
      "set": false,
      "source": "platform"
    },
    {
      "name": "oauth",
      "value": "configured",
      "managed_by": "linq",
      "set": true,
      "source": "brand"
    }
  ],
  "app_extensions": {
    "apps": {}
  }
}
```
