Guides

Safety settings

Every connected LinkedIn account has a ceiling per kind of action. You read them, change them, and decide whether crossing one warns you or stops the call.

Limits are enforced by Curviate. Curviate paces Motion actions; direct core calls are yours to pace. Curviate does not delay or space your direct calls, and does not detect duplicates on them: it contacts whoever you name, as often as you ask. Spread your agent's actions over the day yourself. A Motion runs on its own schedule with a minimum gap and jitter per action type.

What a limit is

A limit is a budget row: one kind of action (profile_views, connection_requests_no_note, inmail, search, comments and so on) with a ceiling per window (day, week or month, counted in UTC). On each row of the policy:

  • ceiling is what you configured; effective_ceiling is what the account is held to after warm-up. Compare counts against effective_ceiling.
  • green and amber only move the band reported in the quotas array of GET /v1/accounts/{account_id}; they change nothing about a refusal.
  • over_default lists values you set above the Curviate default; source_class says how that default was sourced.
  • total_actions is a tally that never refuses; pending_invites is the outstanding invitation backlog, and at its ceiling it blocks both invitation rows. Withdrawing invitations does not give weekly quota back.

limit_profile (basic, premium, sales_navigator, recruiter) is detected from the account and selects which defaults apply. Setting it pins it; send limit_profile_source: "default" alone to release the pin. Sales Navigator and Recruiter figures (interface_ceiling) apply only to calls through those interfaces.

Posture: warn or enforce

PostureAt the ceiling
warn (default)The action goes through, and the response carries safety_warning.
enforceThe action is refused with 429 BUDGET_EXHAUSTED before it reaches LinkedIn. Nothing is sent or spent.

No ceiling refuses by default: every row starts on warn. That applies to your direct calls only: a Motion never exceeds effective_ceiling under either posture, and waits for the next window instead. A Motion also sends each action at most once: an action whose outcome is unknown is never retried, so a crash can drop an action but never repeat one. A LinkedIn pause (below) refuses whatever the posture. Posture resolves row, then account, then tenant default (tenant_default_posture), then warn; posture_source says which one applied. Writing back a posture you merely inherit pins it.

Warm-up for young accounts

Every ceiling is multiplied by a ramp factor while the account is young, dormant, or recovering from a restriction: 0.15 in week 0, then 0.35, 0.60, 0.85, 1.00. A restriction in force is factor 0. After it lifts the factor holds at 0 for 7 days, resumes at 0.40 and rises 0.15 every 4 days. warm_up is read-only; set a row's warm_up_factor to override it for that row.

Activity window

Each row has working hours, seeded 07:00 to 22:00. Outside them a live write warns or is refused like a spent ceiling. It is read in the account's timezone when you set one (for example Europe/Berlin), else the zone the user's browser reported, else one inferred from the LinkedIn profile location, else Europe/Berlin. activity_window.effective_timezone and activity_window.source (account, browser, location or default) on the account and the policy show which zone is in force. Move the edges per row with activity_window_start and activity_window_end, or set timezone to choose the zone.

Tenant default or account override

Each field resolves from the account's own value, else the tenant default for its limit_profile, else the Curviate default. A tenant default applies at once to every account without its own value, including future ones. Its response carries impact (accounts_affected, accounts_moved_into_over), because under enforce lowering a default can start refusing across the tenant. null clears a tenant default.

What a caller sees at a limit

The warning and the refusal carry the same fields; only blocked differs. Branch on the fields, never on hint.message.

{
  "code": "BUDGET_EXHAUSTED",
  "row": "profile_views",
  "reset_at": "2026-09-06T00:00:00.000Z",
  "hint": { "parameter": "profile_views.ceiling", "message": "..." },
  "reason": "ceiling",
  "blocked": true
}
FieldWhat to do with it
rowWhich budget row stopped the call.
reasonceiling (spent) or activity_window (outside hours). Different fixes.
reset_atWhen the refusal lifts. null on pending_invites (the backlog) and inmail credits, where no clock frees the account.
hint.parameterThe setting to change on PATCH /v1/{account_id}/safety-policy.
blockedtrue on the refusal, false on the safety_warning.
  • safety_warning on a successful response: warn, the action counted.
  • 429 BUDGET_EXHAUSTED: enforce, a number you set. Not a rate limit: wait for reset_at or change hint.parameter. There is no Retry-After.
  • 429 PLATFORM_RATE_LIMIT: LinkedIn refused this account on that row recently, so Curviate pauses it for LinkedIn's retry-after or a default cooldown. Raising a ceiling does not lift it; quotas shows halt.retry_after_seconds. clear_halts lifts it early; use it only for a transient fault.

InMail has two bounds: the live credit balance (hint names posture, since no ceiling buys a credit) and the inmail row's send rate, seeded 0 on basic.

Change the settings

Dashboard: Safety (/safety) shows usage per account and a Tenant defaults tab for ceilings and posture. Each account's Limits opens its own overrides.

REST:

OperationDoes
GET /v1/{account_id}/safety-policyRead an account's full policy.
PATCH /v1/{account_id}/safety-policyChange rows, posture, timezone, limit_profile, tenant_default_posture, clear_halts. Partial. tenant_default_posture is a team setting: in a team only an Owner changes it, and a request changing it from anyone else is refused whole with 403 INSUFFICIENT_ROLE. Sending the value the team already holds changes nothing.
GET /v1/safety-policyRead the tenant defaults per limit profile.
PATCH /v1/safety-policySet tenant defaults.
GET /v1/{account_id}/safety-eventsList limit hits.
curl -X PATCH https://api.curviate.com/v1/acc_YOUR_ACCOUNT_ID/safety-policy \
  -H "Authorization: Bearer $CURVIATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"posture": "enforce", "rows": [{"budget_row": "profile_views", "ceiling": 80}]}'

Curviate never clamps: a value above our default is reported in over_default, not refused; only a non-value such as a negative ceiling is 400 INVALID_REQUEST. Reading the policy and sending it back is safe: unchanged values and derived fields (effective_ceiling, posture_source, over_default, warm_up) are ignored. Send the zone as timezone, not the activity_window object. Every change is recorded with its actor.

MCP: get_safety_policy, update_safety_policy and list_safety_events, in the accounts toolset.

The CLI and the SDK have no safety commands; use REST or MCP.

Reading limit hits

Limit hits are read by polling List limit hits, GET /v1/{account_id}/safety-events. There is no webhook for them. Entries are newest first; filter with budget_row, since and until, and page until cursor is null. reason is budget_exhausted, activity_window, rate_limited, or halt_cleared (a pause lifted with clear_halts, not a refusal). blocked: false is the common case: the action went through under warn.

Next steps

  • Rate limits: request-rate ceilings, always enforced.
  • Errors: every error code, including BUDGET_EXHAUSTED.
  • API reference: every safety field and example, under Safety.