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. Timing between actions is yours. Curviate does not pace, delay or space calls, and does not detect duplicates: it contacts whoever you name, as often as you ask. Spread your agent's actions over the day yourself.

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. 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. The window needs a timezone and is off until you set one: send the account timezone (for example Europe/Berlin). Curviate never guesses it.

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