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:
ceilingis what you configured;effective_ceilingis what the account is held to after warm-up. Compare counts againsteffective_ceiling.greenandamberonly move thebandreported in thequotasarray ofGET /v1/accounts/{account_id}; they change nothing about a refusal.over_defaultlists values you set above the Curviate default;source_classsays how that default was sourced.total_actionsis a tally that never refuses;pending_invitesis 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
| Posture | At the ceiling |
|---|---|
warn (default) | The action goes through, and the response carries safety_warning. |
enforce | The 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
}| Field | What to do with it |
|---|---|
row | Which budget row stopped the call. |
reason | ceiling (spent) or activity_window (outside hours). Different fixes. |
reset_at | When the refusal lifts. null on pending_invites (the backlog) and inmail credits, where no clock frees the account. |
hint.parameter | The setting to change on PATCH /v1/{account_id}/safety-policy. |
blocked | true on the refusal, false on the safety_warning. |
safety_warningon a successful response:warn, the action counted.429 BUDGET_EXHAUSTED:enforce, a number you set. Not a rate limit: wait forreset_ator changehint.parameter. There is noRetry-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;quotasshowshalt.retry_after_seconds.clear_haltslifts 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:
| Operation | Does |
|---|---|
GET /v1/{account_id}/safety-policy | Read an account's full policy. |
PATCH /v1/{account_id}/safety-policy | Change 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-policy | Read the tenant defaults per limit profile. |
PATCH /v1/safety-policy | Set tenant defaults. |
GET /v1/{account_id}/safety-events | List 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.