accounts.updateSafetyPolicy

Update an account's safety policy.

Prerequisites

  • A Curviate API key, passed as apiKey when you construct the client. Quickstart shows how to create one.

Signature

updateSafetyPolicy(accountId: string, body: SafetyPolicyUpdateBody): Promise<SafetyPolicyUpdateResult>

Example

import { Curviate } from "@curviate/sdk";
 
const curviate = new Curviate({
  apiKey: "cvt_live_...",
  baseUrl: "https://api.curviate.com",
});
 
await curviate.accounts.updateSafetyPolicy("acc_123", { warm_up: { enabled: false } });

Parameters

NameTypeRequiredDescription
accountIdstringYesThe account ID (acc_...) whose safety policy to update.
limit_profilestringNoWhich set of Curviate defaults this account's rows resolve from: basic, premium, sales_navigator or recruiter. Each inherits the one before it and raises a few rows. This is the platform product active on the account, NOT the Curviate seat entitlement, and the two can disagree: a lapsed subscription leaves the seat untouched. Curviate detects it from the platform when the account connects, re-detects it on every reconnect and, at most once a day, on an account read; setting it here pins it, and the pin wins over detection from then on. None of those paths moves a pinned value, so it holds until you release it. To release the pin, send limit_profile_source: "default" on its own: Curviate re-reads the value from LinkedIn on the account's next read or connect. Changing limit_profile and releasing it in the same request is refused with INVALID_REQUEST, because those are opposite intents. Sending the value the account already holds changes nothing and pins nothing. The change is recorded in the ledger and it moves the defaults under every row at once, so any row you have configured explicitly keeps your value. The elevated Sales Navigator and Recruiter figures apply only to calls made through the matching interface, the Sales Navigator and Recruiter operations, and Curviate holds each call to the figure for the interface it goes through. The numbers here are therefore the ones a standard-interface call is held to, on every profile; a call through one of those operations resolves from the elevated set instead, and each row publishes that figure as interface_ceiling.
timezonenullNoNot a setting: the time zone follows where this account connects from (activity_window shows it). Any value but null is a 400 INVALID_REQUEST; change where the account connects instead (its country, or your own proxy). Null is accepted and ignored, so a document read before this change can be sent back once its zone is null.
tenant_default_posturestringNoREMOVED: limits are always enforced; to send more, raise the limit. "enforce" (or null) is still accepted until 2027-01-25, changes nothing and earns a POSTURE_REMOVED notice; "warn", or any value after that day, is refused with INVALID_REQUEST.
clear_haltsarrayNoLift the platform pause on these action types now, instead of waiting for it to expire. When the platform refuses a call on an account (a rate limit, a server error on a budgeted route, an invitation resend cooldown), Curviate pauses that action type for the platform's own retry-after or a seeded cooldown, and every later call on it is refused locally without reaching the platform. Naming an action type here clears that pause: the next call goes out. Read quotas[].halt on the account to see which action types are paused and for how long. A type with no active pause is accepted and does nothing, so this is safe to send for a set you have not checked. Clearing is recorded in the account's ledger with the expiry it cut short, who cleared it and when, and it appears on the safety-events surface. It does not disarm anything: if the platform refuses the account again, the pause comes straight back, longer than the one you cleared where the platform says so. Clear a pause when you have reason to believe the refusal was a transient upstream fault rather than the platform pushing back on this account; clearing one and calling straight back into a real rate limit is itself how accounts get restricted.
posturestringNoREMOVED: limits are always enforced; to send more, raise the limit. "enforce" (or null) is still accepted until 2027-01-25, changes nothing and earns a POSTURE_REMOVED notice; "warn", or any value after that day, is refused with INVALID_REQUEST.
rowsarrayNoPer-row configuration. Each entry names its budget_row and the fields to change. A row read back from the policy document may be sent here unchanged: its read-only fields (effective_ceiling, over_default, warm_up_state) are accepted and ignored.
pacingobjectNoMotion pacing at this scope (an account, or the team default every account inherits), keyed by action type (invite.send, post.comment, message.send, inmail.send, profile.follow, profile.unfollow, skill.endorse, invite.withdraw, post.react), read and global: \{"invite.send": \{"min_gap_seconds": 600, "jitter_seconds": 360\}\}. Any subset; null clears a field or a type back to the default. Whole seconds from 0 to 86400, never clamped. A value below the default carries an over_default warning, the rows' own shape. Direct API calls are never paced.
active_hoursobjectNoThis account's own active hours: the days and local hours live writes happen in. Null falls back to the Team default. A row's own activity_window_start/end win over it, keeping these days. The read reports the hours in force and their source; sent back, it changes nothing.
objectstringNoREAD-ONLY. The document's type discriminator. Sent back on a write it is accepted and ignored, so the whole document round-trips; it is never stored and never ledgered.
limit_profile_sourcestringNoWhere limit_profile came from: default (never observed), detected (read from LinkedIn) or operator (pinned through this operation, and nothing but a release moves it). Sending default releases a pin: the value is re-read from LinkedIn on the account's next read or connect. Send it on its own; changing limit_profile in the same request is refused. Sending detected or operator is accepted and ignored, so the whole document round-trips.
limit_profile_detected_atstringNoREAD-ONLY. When limit_profile was last read from LinkedIn, or null if it never has been. Sent back on a write it is accepted and ignored, so the whole document round-trips; it is never stored and never ledgered.
posture_sourcestringNoREMOVED from the read (SF-04). Accepted and ignored until 2027-01-25 so an older document round-trips; refused after.
warm_upobjectNoThe account's warm-up. Only enabled is settable; its other keys are derived on every read, accepted and ignored, so the read's object can be sent back unchanged.
activity_windowstringNoRead-only: the time zone the activity window is read in, from where this account connects. Accepted and ignored, so the read's object can be sent back unchanged. The window's settable inputs are per row: activity_window_start, activity_window_end, activity_window_applies_to.

Returns

Resolves to SafetyPolicyUpdateResult. Top-level fields: object, account_id, limit_profile, limit_profile_source, limit_profile_detected_at, warm_up, activity_window, active_hours, rows, pacing, notices.

Error codes

CodeWhat it means, and what to do
UNAUTHORIZEDThe API key is missing, malformed, or revoked. Check the key you passed to the client.
INVALID_REQUESTA parameter failed validation. The message names the offending field; fix the request rather than retrying it.
RATE_LIMIT_ACCOUNTThis account's own quota is exhausted. Wait for the window in the Retry-After header, then retry.
RATE_LIMIT_TENANTThe tenant-wide quota is exhausted across all accounts. Slow the whole workload, not just this call.
PLATFORM_RATE_LIMITLinkedIn is throttling this account. Back off well beyond the hinted delay; sustained pressure risks the account.
PLATFORM_ERRORAn upstream failure, usually transient: retry once with backoff before treating it as a real error. Check the response first, though. When it carries retry_likely_to_succeed: false and retry_hint: {kind: "never"}, retrying cannot help and backing off only delays the real answer.
ACCOUNT_NOT_FOUNDNo connected account with that id belongs to this tenant. List your accounts to get a current id.
PAYLOAD_TOO_LARGEThree limits can trigger this. The platform's own ceiling on total request size is the lowest and the one you are most likely to hit: it starts rejecting at roughly 1 MB, so a handful of photo-sized images is refused even though it clears everything below. Above that, this API caps one attachment at 5 MiB of file content and the whole request body at 9 MiB. Attachments travel base64-encoded, which inflates them by about a third, so a 5 MiB file costs roughly 6.7 MiB of the body budget. Send fewer or smaller attachments; retrying unchanged will not help.
RESOURCE_NOT_FOUNDThe id in the path does not exist, or is not visible to this account. Re-read it from the list endpoint that produced it.
TEAM_REQUIREDSee the error codes reference.

Every code above is a stable CurviateError.code you can branch on. The full list, the response envelope, and retry semantics are in the Error codes reference.

Next steps

  • Quickstart: connect a LinkedIn account. An empty account list means the key works and nothing is connected yet.
  • accounts.list: List the tenant's connected LinkedIn accounts, cursor-paginated.
  • accounts.listSeats: List the workspace's live seats: each seat_id, whether an account occupied it, and that account_id (null when free).
  • SDK Quick Start: installation, the client, account scoping, and pagination.