---
title: "Update the tenant safety defaults"
method: PATCH
path: "/v1/safety-policy"
tags: ["Safety"]
---

# Update the tenant safety defaults

`PATCH /v1/safety-policy`

## Request

- Base URL: `https://api.curviate.com`
- URL: `https://api.curviate.com/v1/safety-policy`
- Auth: HTTP bearer

Updates tenant-wide safety defaults per limit profile and budget row; affects every account that hasn't overridden the field, immediately. Fields, posture and precedence: /reference/safety.

## Request body

- object
  - `tenant_default_posture` 'warn' | 'enforce', nullable: The tenant-wide posture default every account inherits unless it overrides it. Null clears it back to `warn`.
  - `profiles` object[]: Tenant defaults, per limit profile.
    - `limit_profile` 'basic' | 'premium' | 'sales_navigator' | 'recruiter', required: The limit profile these defaults apply to. An account resolves from the defaults of its own `limit_profile` only.
    - `rows` object[], required: Per-row defaults for this profile. Omit a field to leave it; `null` clears the tenant default back to the Curviate default. A row read back from this document may be sent unchanged.
      - `budget_row` 'connection_requests_with_note' | 'connection_requests_no_note' | 'messages_first_degree' | 'inmail' | 'open_profile_inmail' | 'profile_views' | 'search' | 'comments' | 'post_likes' | 'follows' | 'content_poll' | 'job_poll' | 'posts' | 'endorsements' | 'invite_responses' | 'withdrawal_sweep' | 'total_actions' | 'pending_invites', required: The budget row this patch configures.
      - `ceiling` integer, nullable: What `enforce` refuses at and `warn` reports at, in this row's own unit. Any value up to 9007199254740991 is accepted and stored verbatim; a value above the Curviate default is reported back, never clamped.
      - `green` integer, nullable: Routine band. Activity at or below it is unremarkable.
      - `amber` integer, nullable: Warn-louder band, between green and the ceiling.
      - `window_kind` 'day' | 'week' | 'month', nullable: The calendar grain this row is counted in.
      - `window_span` integer, nullable: How many `window_kind` units one counting window spans.
      - `window_timezone` string, nullable: IANA zone name the counting window is aligned to, e.g. `Europe/Berlin` or `UTC`. Must be a real zone; anything else is a 400. Null counts in UTC.
      - `activity_window_start` string, nullable: Local time the activity window opens, HH:MM. Outside the window, live write operations are reported or refused per posture.
      - `activity_window_end` string, nullable: Local time the activity window closes, HH:MM.
      - `activity_window_timezone` string, nullable: IANA zone name the activity window is read in, e.g. `Europe/Berlin`. Must be a real zone; anything else is a 400. Null DISABLES the activity window: a guessed zone inverts it, which is worse than not having one.
      - `activity_window_applies_to` 'writes' | 'all', nullable: What the activity window governs. `writes` (the default) covers live writes to LinkedIn; `all` widens it to live reads. A read Curviate answers from its own store never reaches LinkedIn and so is never covered; the same read IS covered when the store cannot answer it, or when you ask for it live. Stored reads and inbound events are never subject to the window under either setting, at any hour. One action set to `all` reaches the whole account's live reads that spend no budget of their own (notifications, profile analytics, SSI, profile visitors, the invitation and saved-list listings): those calls debit no action type, so they have none to read this setting from and take the account-wide answer instead. Reads that DO debit an action type read that action type's own setting.
      - `warm_up_factor` number, nullable: Multiplier applied to this row's ceiling while ramping. 1 is not ramping.
      - `character_cap` integer, nullable: Length cap on this row's free-text field, in characters (an invitation note, an InMail body).
      - `subject_character_cap` integer, nullable: Length cap on this row's subject line, in characters.
      - `per_query_cap` integer, nullable: Bound on a single query's result lines. Search rows only.
      - `source_class` 'substrate' | 'linkedin_official' | 'practitioner' | 'inferred' | 'unseeded', nullable: How well calibrated this row's configured number is. Overriding it labels your own figure; the over-default warning always reports the class of the Curviate default.
      - `interface_ceiling` unknown
      - `over_default` unknown
  - `object` unknown
  - `impact` unknown

## Response `200`

The tenant safety defaults as they now read, plus the `impact` of this write.

- object
  - `object` 'tenant_safety_policy': Response type discriminator.
  - `tenant_default_posture` 'warn' | 'enforce' | 'null', nullable: The workspace-wide posture default every account in the tenant inherits, or null when none is set. Always present, and separate from `posture` on purpose: when this account overrides the tenant default, a change to the tenant default does not move `posture`, and this field is how you can still see it took effect.
  - `profiles` object[]: One entry per limit profile, always all of them.
    - `limit_profile` 'basic' | 'premium' | 'sales_navigator' | 'recruiter': The limit profile these defaults apply to. An account resolves from its own profile's defaults.
    - `rows` object[]: Every budget row, always: the tenant default where one is set, the Curviate default otherwise.
      - `budget_row` string: The counted-action row this policy governs.
      - `ceiling` integer, nullable: What `enforce` refuses at and `warn` reports at, in the row's own unit. Null means the row is uncalibrated: Curviate has no figure for it.
      - `green` integer, nullable: Routine band.
      - `amber` integer, nullable: Warn-louder band.
      - `window_kind` 'day' | 'week' | 'month': The calendar grain this row is counted in.
      - `window_span` integer: How many grain units one counting window spans.
      - `window_timezone` string, nullable: IANA zone name the counting window is aligned to. Null counts in UTC.
      - `activity_window_start` string: Local HH:MM the activity window opens.
      - `activity_window_end` string: Local HH:MM the activity window closes.
      - `activity_window_timezone` string, nullable: IANA zone name the activity window is read in. Null disables the window entirely.
      - `activity_window_applies_to` 'writes' | 'all': What the activity window governs. `writes` (the default) covers live writes to LinkedIn; `all` widens it to live reads as well, which is the setting for a persona that should not be seen browsing at 03:00. It is broader than it sounds: `all` covers every live read, including the ones about the account itself, so a nightly analytics or invitation-backlog poll is refused at 03:00 too. A read Curviate answers from its own store never reaches LinkedIn, so it is never subject to the window under either setting, at any hour; the same read IS covered when the store cannot answer it, or when you ask for it live. Inbound events are never subject to it either. One action set to `all` reaches the whole account's live reads that spend no budget of their own (notifications, profile analytics, SSI, profile visitors, the invitation and saved-list listings): those calls debit no action type, so they have none to read this setting from and take the account-wide answer instead. Reads that DO debit an action type read that action type's own setting.
      - `warm_up_factor` number, nullable: The tenant default multiplier for this row, or null when none is set. Null means each account follows its own derived warm-up ramp; 1 pins every account on this profile out of it.
      - `character_cap` integer, nullable: Length cap on the row's free-text field, in characters.
      - `subject_character_cap` integer, nullable: Length cap on the row's subject line, in characters.
      - `per_query_cap` integer, nullable: Bound on a single query's result lines. Search rows only.
      - `source_class` 'substrate' | 'linkedin_official' | 'practitioner' | 'inferred' | 'unseeded': How well calibrated this row's number is. `unseeded` means Curviate has no green, amber or ceiling figure for this row on this profile: the row is reported so nothing is silently missing, but nothing has calibrated it, so no value you set can be `over_default`.
      - `interface_ceiling` integer, nullable: What a call through the matching ELEVATED interface is held to on this row: the `/v1/{account_id}/sales-navigator/...` and `/v1/{account_id}/recruiter/...` operations. Null when this row and limit profile have no such figure, which is every row on `basic` and `premium` and most rows everywhere. Everything else on this row (`ceiling`, `effective_ceiling`, `band` and `over_default`) is the STANDARD-interface figure, because one counter serves both interfaces and the standard side is the conservative one to band against. The consequence is worth reading: on a `sales_navigator` or `recruiter` account a row can report `band: "over"` with `posture: "enforce"` while a call through the elevated interface still succeeds; the refusal the band predicts is the one a standard call gets.
      - `over_default` object[]: Empty when nothing on this row is above default. Computed against the STANDARD-interface default, like `band` and for the same reason.
        - `field` 'ceiling' | 'green' | 'amber' | 'character_cap' | 'subject_character_cap' | 'per_query_cap': The field whose configured value is above default.
        - `value` integer: The configured value.
        - `default` integer: The Curviate default it exceeds.
        - `source_class` 'substrate' | 'linkedin_official' | 'practitioner' | 'inferred' | 'unseeded': How well calibrated the DEFAULT is. `substrate` and `linkedin_official` are published figures; `practitioner` is corroborated operator experience; `inferred` is our own reasoning; `unseeded` means we have no figure at all for that row, in which case there is nothing to be above and no entry appears. Read this before deciding whether being above default matters.
  - `impact` object: What this write changed across the tenant's accounts, measured on them before and after it applied.
    - `accounts_affected` integer: Accounts on which at least one budget row's resolved ceiling changed.
    - `accounts_moved_into_over` integer: Accounts on which at least one budget row moved into `band: "over"`.
  - `safety_warning` SafetyWarning: Present on a successful response when this account crossed an account-safety ceiling while the affected budget row is on the default warn posture. The action still happened. Field-identical to the BUDGET_EXHAUSTED error body except that blocked is false and hint.message describes the outcome, so one branch handles both postures and switching the account to enforce is a configuration change rather than a client rewrite.
    - `row` string, nullable, required: The budget row that breached, for example profile_views or pending_invites. Null when the breach is not about a row: an activity_window warning on an action that spends no budget of its own (editing a message, dismissing a notification, a Recruiter or Sales Navigator admin action) is still governed by the hours the account works in. Read hint.parameter for what to change; whenever this is null it names posture, because the activity window edges are set per action type and this action has none, so the levers that reach it are the account timezone and posture.
    - `reset_at` string, nullable, required: The instant the refusal lifts: the window roll on a ceiling refusal, the next window open on an activity-window one. Null in the two cases no clock frees, the pending_invites gauge and InMail credits (row inmail).
    - `hint` object, required
      - `parameter` string, required: The settable parameter on PATCH /v1/{account_id}/safety-policy.
      - `message` string, required: What the condition is and what changing that parameter would do. This is the one field whose text differs from the BUDGET_EXHAUSTED refusal: here it says the action went through, and on a ceiling breach how far past the ceiling this row now stands, rather than telling you to set posture to warn when it already is. Branch on parameter, reason and blocked, never on this text.
    - `reason` 'ceiling' | 'activity_window', required: Which rule was crossed. ceiling means the row's configured limit is spent; activity_window means the account is outside the hours it works in, which is a different fact with a different remedy. hint.parameter names the right setting for each.
    - `blocked` boolean, required: Always false here. True on the BUDGET_EXHAUSTED refusal carrying the same payload.

## Other responses

- `400`: INVALID_REQUEST. An unknown limit profile, row name or field, or a value that is not a value (a negative ceiling). A value is never rejected for being unsafe.
- `413`: Payload too large, the request body exceeds the size limit.
- `429`: Rate limited, slow down and retry after the hinted delay.

---

[API](https://skmtc.dev/curviate/apis/curviate-api.md) · [All operations](https://skmtc.dev/curviate/apis/curviate-api/llms.txt) · [OpenAPI document](https://skmtc.dev/curviate/apis/curviate-api/revisions/eab848801d95?raw)
