Reference

Account safety

Every connected LinkedIn account carries a set of ceilings on what it may do per day, per week and per month. They are configuration, not constants: you read them, you change them, and you decide whether crossing one warns you or stops the call.

Three account-scoped surfaces, plus the tenant defaults every account inherits:

OperationWhat it answers
GET /v1/{account_id}/safety-policyWhat is this account allowed, and what happens at the limit?
PATCH /v1/{account_id}/safety-policyChange any of it.
GET /v1/{account_id}/safety-eventsWhat has already been refused or flagged?
GET / PATCH /v1/safety-policyWhat does every account start from, per limit profile?

The quotas array on GET /v1/accounts/{account_id} answers a fourth: where does the account stand right now, against those limits. The full schemas, every field and every example live in the interactive API reference under Safety.

Nothing is refused by default. Every scope ships on the warn posture, which lets the action through and reports the breach. Switching an account to enforce is a configuration change you make deliberately.

Reading the policy

curl https://api.curviate.com/v1/acc_01JQZK8N3XV4RTYWB2M6D5F0AC/safety-policy \
  -H "Authorization: Bearer cvt_live_..."

One request returns the whole policy: every row, plus the account-level fields that modify all of them.

{
  "object": "safety_policy",
  "account_id": "acc_01JQZK8N3XV4RTYWB2M6D5F0AC",
  "limit_profile": "basic",
  "posture": "warn",
  "posture_source": "default",
  "tenant_default_posture": null,
  "warm_up": {
    "state": "none",
    "factor": 1,
    "week": null,
    "triggers": [],
    "unevaluated": ["connections"]
  },
  "activity_window": { "timezone": null, "timezone_unset": true },
  "rows": [
    {
      "budget_row": "profile_views",
      "ceiling": 100,
      "effective_ceiling": 100,
      "green": 60,
      "amber": 90,
      "window_kind": "day",
      "window_span": 1,
      "window_timezone": null,
      "posture": "warn",
      "posture_source": "default",
      "source_class": "substrate",
      "over_default": [],
      "activity_window_start": "07:00",
      "activity_window_end": "22:00",
      "activity_window_timezone": null,
      "activity_window_applies_to": "writes",
      "warm_up_state": "none",
      "warm_up_factor": 1,
      "character_cap": null,
      "subject_character_cap": null,
      "per_query_cap": null
    }
  ]
}

Rows

A row is one kind of action: profile_views, connection_requests_no_note, inmail, search, comments, and so on. Each carries its own ceiling, its own counting window and its own posture.

  • ceiling is what you configured. effective_ceiling is what the account is actually held to, and it is the one to compare a count against. The two differ while a warm-up ramp is in force.
  • green and amber are reporting thresholds, and the only thing they move is band on the account resource's quotas view: used at or past green reads amber, at or past amber reads hard, and at or past the effective ceiling reads over. All three comparisons use the RAMPED numbers, so on an account in warm-up the bands move with everything else. They change nothing about the refusal itself: the breach payload is byte-identical whatever they are set to, and lowering amber to get a louder or earlier warning does nothing at all.
  • window_kind and window_span report the counting window (day, week or month), counted in UTC. They are reported, not yet honoured: the counter is written at one grain per row, so setting a different window on one account is accepted and changes nothing today. Read them; do not plan around changing them.
  • source_class says how well calibrated our default is: substrate, linkedin_official, practitioner, inferred, or unseeded where we have no figure at all. It is the difference between "you are above a number somebody published" and "you are above a number we guessed", and a warning without it is uniform and therefore ignorable.
  • over_default is non-empty when you configured something above our own default. It carries the field, your value, our default and that default's source class, and it persists: a limit raised six months ago is still raised today and still says so. Set the value back and it disappears.

Two rows are not counters and behave differently: total_actions is a tally across everything the account did in the window, and pending_invites is the outstanding invitation backlog, a level read from LinkedIn rather than something the account spends.

limit_profile

limit_profile is the LinkedIn product active on the account (basic, premium, sales_navigator, recruiter) and it selects which set of Curviate defaults every row resolves from. It is detected when the account connects, re-detected on every reconnect and, at most once a day, when the account is read; setting it explicitly overrides that until the next connect. The document says where the value came from: limit_profile_source is detected (read from LinkedIn at limit_profile_detected_at), operator (set through this operation) or default (never observed, so the value is the seeded basic and says nothing about the account). To release an override, send limit_profile_source: "default" on the write; writing back the value the account already holds changes nothing.

Each call is held to the figure for the interface it goes through. The elevated Sales Navigator and Recruiter figures apply only to calls made through the matching interface, the /v1/{account_id}/sales-navigator/... and /v1/{account_id}/recruiter/... operations. So ceiling is the number an ordinary call is held to, on every profile, and interface_ceiling beside it is what a call through the elevated interface gets. On recruiter, profile_views reads ceiling: 100 and interface_ceiling: 2000; on sales_navigator, search reads ceiling: 1000 and interface_ceiling: 2500. Both are null where there is no such figure, which is every row on basic and premium.

One consequence worth reading before you act on a band. band and over_default are computed against ceiling, the standard figure, because a single counter serves both interfaces and the standard side is the conservative one to measure against. So on sales_navigator or recruiter 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 an ordinary call gets.

posture

posture is what happens when a row crosses its ceiling.

ValueEffect
warn (the default)The action goes through, and the successful response carries the breach under safety_warning.
enforceThe action is refused before it reaches LinkedIn. Nothing is sent and nothing is spent.

It resolves row, then account, then tenant, then warn, and posture_source on each row and at the top level says which of the four supplied the value, so an override is visible as an override rather than as a coincidence.

tenant_default_posture is the tenant-wide default. It is written through this same account-addressed operation, and it changes every account in the tenant, not the one in the path. It is always reported at the top level, as the raw tenant value or null, so a write that flips it is visible even on an account holding its own override.

warm_up

A brand new account running at a mature account's numbers is the most reliable way to get one restricted, so every ceiling is multiplied by a ramp factor while the account is young, thinly connected, dormant, or under or recovering from a restriction. A restriction still in force is factor 0, which is a hold rather than a ramp: under enforce every row's effective ceiling is 0 and nothing goes out at all, and under the default warn every action carries a warning instead.

{
  "state": "ramping",
  "factor": 0.15,
  "week": 0,
  "triggers": ["account_age"],
  "unevaluated": ["connections"]
}
  • The ramp is 0.15 in week 0, then 0.35, 0.60, 0.85, 1.00. After a restriction lifts it holds at 0 for 7 days, resumes at 0.40 and steps up 0.15 every 4 days.
  • warm_up is read-only and derived on every read. To ignore it for one kind of action, set that row's warm_up_factor and your value wins.
  • A dormant account restarts its ramp when it comes back. The week counts from whichever is later of the account's creation and its last return from a gap, so a two-year-old persona back after six months of silence starts at 0.15 rather than at full speed.
  • unevaluated is not "false". connections is in it on every account today, because the connected-account record LinkedIn returns carries no connection count. A trigger we could not check is reported, not assumed away.
  • Every warm-up figure is inferred. Nobody publishes a ramp.

effective_ceiling on each row is ceiling with this factor already applied. On a five-day-old account the two differ by roughly a factor of seven, and the smaller one is where your call is stopped.

activity_window

The hours the account works in, seeded 07:00 to 22:00, with both edges varied by up to 45 minutes per account per day so the schedule does not read as a timetable. Outside it, a live write to LinkedIn warns or is refused, exactly as a spent ceiling is.

The window needs a timezone, and the zone resolves row, then account, then unset. A row's own activity_window_timezone wins; with none, the account's timezone applies; with neither, the window is disabled for that row and never fires at any hour. Curviate does not infer the zone, deliberately: a zone eight hours wrong inverts the window, refusing every daytime write and permitting every nocturnal one, which is worse than not having one.

activity_window.timezone_unset reports the ACCOUNT rung only. It is true whenever the account has no timezone, including when a row carries its own and is enforcing normally. So timezone_unset: true does not mean "the window is off everywhere". If you are debugging a refusal at 03:00, check the row's activity_window_timezone before concluding the window cannot be the cause.

Set the account timezone. That is the switch that works for every row, and it is what the window is designed around. A per-row zone exists, but the window is evaluated once per call from the FIRST budget row that call would debit, so on an operation touching more than one row a per-row zone on the others is not consulted. Use it to carve out a single-row exception, not as the way to turn the window on.

curl -X PATCH https://api.curviate.com/v1/acc_01JQZK8N3XV4RTYWB2M6D5F0AC/safety-policy \
  -H "Authorization: Bearer cvt_live_..." \
  -H "Content-Type: application/json" \
  -d '{"timezone": "Europe/Berlin"}'

activity_window_applies_to chooses what the window covers: writes (the default) or all, which widens it to live reads as well. all is broader than it sounds. It covers every live read, including the ones about the account itself, so a nightly analytics or backlog poll is refused at 03:00 too. Anything Curviate serves from its own store, and every inbound event, is unaffected at any hour under either setting.

The field is set per action type, and one of its answers is account-wide. The live reads about the account itself spend no budget of their own, so they have no action type to read this setting from and take the account's answer instead: setting all on any one action type governs every one of them. A read that does debit an action type reads that action type's own setting, so an account can hold all on one action type and writes on the rest while those account-level reads are governed. The policy document reports the field per action type only, so it does not yet show the account-wide answer back to you.

Changing the policy

One PATCH, partial, one row or many:

curl -X PATCH https://api.curviate.com/v1/acc_01JQZK8N3XV4RTYWB2M6D5F0AC/safety-policy \
  -H "Authorization: Bearer cvt_live_..." \
  -H "Content-Type: application/json" \
  -d '{"rows": [{"budget_row": "profile_views", "ceiling": 100000}]}'

Curviate never clamps a value. Any limit may be set to anything; a value above our default is a loud warning on every response that carries the policy, never a refusal. An agent may make these changes on the same footing as a human operator, and every change is written to the account's ledger with the actor who made it.

Sending the document back

Reading the policy and sending it back with one field edited is the obvious way to use a partial-update API, and it works. Three things make it safe, and one of them is a rule you have to follow:

  • A value that does not move is not a change. A ceiling, band, window or cap sent at the value it already resolves to is neither stored as an override nor ledgered, so a round trip does not pin every field against a future corrected default.
  • Derived fields are accepted and dropped. effective_ceiling, posture_source, over_default, warm_up and each row's warm_up_state are computed on every read. Sending them back changes nothing.
  • Send the activity-window zone as the top-level timezone, not as the activity_window object. The read reports the zone under activity_window; the field that SETS it is timezone. Sending the object is rejected with INVALID_REQUEST rather than accepted and quietly ignored, because silently swallowing an edit of the zone is worse than a 400 that names the field.
  • Posture is an OVERRIDE at every scope, so writing back a posture you merely inherit PINS it. That is a real change and it is ledgered: the override is what stops a later tenant-wide flip from moving that scope. If you did not mean to pin it, omit posture from the document you send back.

Tenant defaults

Set a limit once for every account on a limit profile, including accounts you connect later and before you connect the first one:

curl -X PATCH https://api.curviate.com/v1/safety-policy \
  -H "Authorization: Bearer cvt_live_..." \
  -H "Content-Type: application/json" \
  -d '{"profiles": [{"limit_profile": "basic", "rows": [{"budget_row": "connection_requests_no_note", "ceiling": 50}]}]}'

Each field resolves from the account's own value where it set one, otherwise the tenant default for the account's limit_profile, otherwise the Curviate default. null clears a tenant default. The change applies immediately to every account that has not set that field itself, so under enforce lowering a default can start refusing actions across the tenant in one request. The response carries impact: accounts_affected (accounts whose ceiling changed) and accounts_moved_into_over, measured on those accounts before and after the write. Row posture is not a tenant default; use tenant_default_posture.

What a breach looks like

The same payload in both directions. It differs by exactly one field you branch on, blocked, and by the wording of hint.message, which describes what happened.

Under warn: on the successful response

{
  "object": "user",
  "id": "...",
  "safety_warning": {
    "row": "profile_views",
    "reset_at": "2026-09-06T00:00:00.000Z",
    "hint": {
      "parameter": "profile_views.ceiling",
      "message": "This account is at its ceiling for profile_views. This account is in warm-up, so the number it was stopped at is not the number on the row: effective ceiling 15 = 100 x warm-up 0.15 (week 0, account_age). Raising the ceiling raises the effective ceiling by the same factor. This action went through because profile_views.posture is warn, and it counted: 16 of 15 this window. Raise profile_views.ceiling, or wait for the window to reset at 2026-09-06T00:00:00.000Z."
    },
    "reason": "ceiling",
    "blocked": false
  }
}

Under enforce: 429 BUDGET_EXHAUSTED

{
  "code": "BUDGET_EXHAUSTED",
  "message": "The profile_views budget for this account is spent.",
  "row": "profile_views",
  "reset_at": "2026-09-06T00:00:00.000Z",
  "hint": {
    "parameter": "profile_views.ceiling",
    "message": "This account is at its ceiling for profile_views. This account is in warm-up, so the number it was stopped at is not the number on the row: effective ceiling 15 = 100 x warm-up 0.15 (week 0, account_age). Raising the ceiling raises the effective ceiling by the same factor. Wait for the window to reset at 2026-09-06T00:00:00.000Z, raise profile_views.ceiling, or set profile_views.posture to warn to let the action through."
  },
  "reason": "ceiling",
  "blocked": true
}

row, reset_at, hint.parameter and reason are identical; only blocked differs, plus the prose in hint.message: the refusal names posture as the lever that lets the action through, the warning says the action went through and, on a ceiling breach, how far past the ceiling the row now stands. One branch of your code handles both, and moving an account to enforce is a configuration change rather than a client rewrite. Never branch on the message.

BUDGET_EXHAUSTED is not a rate limit, even though both are 429. PLATFORM_RATE_LIMIT means LinkedIn refused; this means Curviate did, on a number you set. Back off for the first. For this one, wait until reset_at when there is one, and otherwise change hint.parameter. There is deliberately no Retry-After header and no delay retry hint: a monthly row's reset can be weeks out, and a client that honoured the header would sleep it.

FieldWhat to do with it
rowWhich budget row. On the invitation backlog it is the row that blocked, not the row you were spending.
reasonceiling (the limit is spent) or activity_window (the account is outside its hours). Different fixes, so branch on this.
reset_atThe instant the refusal lifts. It is set on a ceiling refusal (the window roll) and on an activity-window refusal (the next window open). It is null in two cases, where no clock frees the account: the pending-invitations gauge (row: pending_invites) and InMail credits (row: inmail). Null-check it before handing it to a scheduler; reason and hint.parameter say what to do instead.
hint.parameterAddressable on PATCH /v1/{account_id}/safety-policy, so reconfiguring needs no prose parsing.
blockedtrue on the refusal, false on the warning.

A few things worth knowing about specific rows:

  • InMail has two bounds and both apply. Credit exhaustion is read from the account's live LinkedIn balance, never from a local count, because a reply within 90 days refunds the credit and a count is wrong exactly when it matters; it is checked first, and its hint names posture, because no ceiling change buys a credit. The send RATE is an ordinary ceiling on the inmail row like any other, counted locally, with the hint naming inmail.ceiling. On the basic profile that ceiling is seeded 0, because basic gets no InMail allowance, so an account holding credits there warns on every send until you raise the ceiling or move the profile.
  • pending_invites is a backlog level, not a budget you draw down. At its ceiling it blocks both invitation rows even with weekly quota left, and withdrawing invitations does not give that quota back. Its own posture is reported but is not the one that acts: the row blocks invitations rather than being spent itself, so whether a full backlog warns or refuses is the posture on the INVITATION row. Setting enforce on invitations is enough.
  • total_actions refuses at no value under any posture. It is a tally, and LinkedIn's own limits are per action type and do not accumulate.
  • A refusal debits nothing. The counter does not move, and the attempt is recorded as refused.

When LinkedIn pauses a row

BUDGET_EXHAUSTED is a number you set. The other pause is not yours at all: when LinkedIn refuses a call on an account, Curviate holds that action type for LinkedIn's own retry-after or a seeded cooldown, and every later call on it is refused locally with PLATFORM_RATE_LIMIT without leaving the process. The account resource reports the live ones:

{
  "budget_row": "profile_views",
  "used": 41,
  "halt": { "reason": "platform_fault", "retry_after_seconds": 214 }
}

Retrying into a real rate limit is how accounts get restricted, so the pause is the product behaviour rather than an inconvenience. It is not permanent, and it is not a state you have to wait out blind:

  • It expires on its own, and retry_after_seconds is the only number to act on. It is LinkedIn's own retry-after where one was supplied, and a Curviate default otherwise. Do not assume every pause is the same length: a server error on a READ gets a much shorter default than a rate limit does, because a 500 on a read is far more often an upstream blip than LinkedIn pushing back. A 429, and any refused write, keeps the long one.
  • You can lift one early, on the same PATCH as everything else:
curl -X PATCH https://api.curviate.com/v1/acc_01JQZK8N3XV4RTYWB2M6D5F0AC/safety-policy \
  -H "Authorization: Bearer cvt_live_..." \
  -H "Content-Type: application/json" \
  -d '{"clear_halts": ["profile_views"]}'

The next call goes out. An action type with no active pause is accepted and does nothing, so it is safe to send for a set you have not checked; only the ones actually lifted are recorded, each with the expiry it cut short, who lifted it and when, and each appears on the events surface below with reason: "halt_cleared". Lifting a pause disarms nothing: if LinkedIn refuses the account again, the pause comes straight back. Lift one when you have reason to believe the refusal was a transient upstream fault. Lifting one and calling straight back into a real rate limit is the thing this whole mechanism exists to stop you doing.

What already happened

safety_warning on one response is a warning nobody reads twice. The events surface is the durable record, newest first:

curl "https://api.curviate.com/v1/acc_01JQZK8N3XV4RTYWB2M6D5F0AC/safety-events?budget_row=profile_views" \
  -H "Authorization: Bearer cvt_live_..."
{
  "object": "safety_event_list",
  "items": [
    {
      "object": "safety_event",
      "id": "aln_01JQZ8T7K3M9V2X4B6C8D0E1F2",
      "budget_row": "profile_views",
      "reason": "budget_exhausted",
      "blocked": false,
      "method": "getUserProfile",
      "occurred_at": "2026-09-04T09:14:02.000Z"
    }
  ],
  "cursor": null
}
  • blocked: false is the common case, not an anomaly. On the default posture the action went through, and the event is the only lasting record that it crossed a limit.
  • reason has four values and each needs a different response. budget_exhausted is a ceiling you configured and can raise; activity_window is the account acting outside its working hours, where no ceiling is the constraint, so wait for the window or move its edges; rate_limited is LinkedIn having refused this account on that row recently, which no ceiling you raise lifts, though clear_halts lifts it deliberately. The refusal ERROR spells the same rules ceiling and activity_window, so do not reuse one mapping across both: ceiling there is budget_exhausted here.
  • halt_cleared is the one entry that is not a refusal. It records a rate_limited pause being lifted early with clear_halts, so the row was open again from that instant. It carries blocked: false and its method is update_safety_policy. Read it beside the rate_limited entry it lifts, or you will report a row as paused that is not.
  • Filter with budget_row, since and until; page until cursor is null.
  • The field is budget_row here, and row on the error envelope. Same fact, and every read surface in the product spells it budget_row.

This surface and quotas never disagree, because they read the same records for different questions: quotas is where the account stands right now, and this is what already happened.

Next steps

  • Rate limits: the request-rate ceilings, which are a different thing from these and are always enforced.
  • Errors: the full error-code reference, including BUDGET_EXHAUSTED.
  • Open the API Reference: every field, every example, under Safety.
COMPANY · LEGAL

Privacy Policy

Redmer Holding GmbHLast updated August 4, 2026

Who we are

Curviate is operated by Redmer Holding GmbH ("Curviate", "we", "us"), a German GmbH registered at Amtsgericht Bonn, HRB 29957, registered address Hostertstraße 16, 53332 Bornheim, Germany. Full company details are on our Imprint. We haven't appointed a statutory Data Protection Officer, since our processing doesn't reach the scale or sensitivity that requires one. Privacy questions go to privacy@curviate.com.

The two roles we play

When you create an account and use Curviate, we process your own data (identity, billing, API keys, connector authorizations). For that data, we are the controller.

When you use Curviate to act on your own connected LinkedIn account, viewing profiles, sending messages, managing engagement, that content and those contacts belong to that account and its people. You are the controller of that data; we are the processor, acting only on your instructions, under a Data Processing Agreement available on request (see below). If one of your contacts has a question about being reached through Curviate, you're who they should contact first; email privacy@curviate.com if you need help routing it.

What we collect, and why

DataWhy
Account identity (name, email, sign-in method)Create and secure your account
Your LinkedIn credentialsOperate the actions you request
LinkedIn content returned by an API callFulfil that specific request, nothing more
API keys and connector (OAuth) authorizationsAuthenticate your API, CLI, MCP, or SDK requests
Billing detailsCharge you correctly and meet our tax obligations
Usage and security logsKeep the service reliable and abuse-free
Support messagesRespond to you
Website analytics, only if you opt inUnderstand how the site is used

We rely on our contract with you, our legitimate interest in running and securing the service, our legal obligations (tax law, for example), and, for analytics, your consent. We never sell your data or use it to train models.

Where it's processed, and who else touches it

Our infrastructure runs in the EU. Hosting: Railway. Database and auth: Supabase, Ireland. Email: Resend. Payments: Stripe. Network security: a DDoS-protection provider sits in front of our app and never sees or stores request content. LinkedIn connectivity: a third-party infrastructure provider that lets us execute LinkedIn actions on your behalf. Error tracking: Sentry, Frankfurt. Product analytics: PostHog, Frankfurt. Uptime monitoring: Better Stack.

We give the current, named list of every provider above to any customer who asks: security@curviate.com.

Data processing agreement

A data processing agreement under Article 28 of the GDPR is available to business customers on request. Email security@curviate.com and we will send you the current version.

Outside the EU

All customer LinkedIn data, account data, and telemetry are processed and stored exclusively in EU regions of our sub-processors. A few providers we rely on (Stripe and Sentry, for example) are headquartered outside the EU/EEA; where that applies, it's covered by their own GDPR safeguards, typically the EU Standard Contractual Clauses.

How long we keep it

DataRetention
Account and workspace dataWhile your account is active
Closed accountDeleted immediately and irreversibly; see Deleting your account below
LinkedIn credentialsUntil you disconnect that account
LinkedIn contentNot stored; any transient cache clears within 1 hour, never indexed, never used for training
API keysUntil you revoke or rotate them
Connector (OAuth) authorizationsAccess token ~1 hour; refresh token up to ~12 months, or until you revoke it, whichever comes first
Billing recordsAs required by German tax law, currently up to 10 years
LogsA short operational window; metadata only, never message content

The 12-month figure above is a server-side credential for a connected AI agent or app. It is not a cookie and doesn't touch your browser session; see Cookies below for that. You can see and revoke every connector from Authorized applications in your dashboard at any time.

Cookies

We keep cookies to a minimum, and ask before anything beyond the essentials runs.

Strictly necessary, no consent needed:

NamePurposeExpiry
cc_cookieRemembers your cookie choice12 months
sb-*-auth-tokenKeeps you signed inWhile active; cleared on sign-out

Analytics, only if you accept:

NamePurposeExpiry
_gaGoogle Analytics: distinguishes visitors2 years
_gidGoogle Analytics: distinguishes visitors24 hours
_ga_<container id>Google Analytics: persists session state2 years

No advertising cookies, ever. Accept and reject are equally easy, and you can change your mind any time via Cookie Preferences in the footer; we won't ask again for 12 months unless something material changes. Our LinkedIn connect flow and OAuth authorization screen never set anything beyond the essentials, so no banner appears there.

Connecting an AI agent or app

Curviate is built for AI agents and automated clients as much as for people. If you connect an app like Claude, or your own code, via an API key or an OAuth connector, it can act on your workspace within the access you gave it. What it does with anything it receives back, including what it sends to its own AI model, is between you and that provider; review its practices before connecting it. Review and revoke any connection any time from your dashboard.

Deleting your account

You can delete your account yourself, from Settings in your dashboard. It takes effect immediately and it cannot be undone. There is no grace period and nothing to restore afterwards, so export anything you want to keep before you start.

Deleting removes your sign-in identity, which frees your email address for reuse straight away, along with your profile, your workspace membership and settings, your API keys, and your seats. For any connected LinkedIn account, we instruct our infrastructure provider to delete it, and your access ends immediately. Records of the connection itself can remain in our systems; email privacy@curviate.com if you need those removed as well. LinkedIn content was never stored in the first place, so there is none of it to delete.

A few things are kept on purpose. We would rather name them than claim a clean sweep:

  • Billing records, for as long as German tax law requires. They hold plan, seat count, amount, and payment references; no name, no email, no LinkedIn data.
  • A record that the deletion happened, so we can show you or a regulator that we did it.
  • A security log of which requests were made, kept for 90 days and then removed automatically. It records that a request happened, never what was in it.
  • A one-way fingerprint, if you used a free trial, that lets us recognise a repeat trial. It holds no readable identifier and cannot be read back into your name, your email, or your LinkedIn profile.

Internal workspace identifiers can also remain in operational records such as queue entries and rate-limit counters. Those carry no name, no email, and no content. If you want to know exactly what is left for your own account, ask us at privacy@curviate.com.

Your rights

You can access, correct, delete, restrict, or object to your data, port it elsewhere, and withdraw consent at any time: email privacy@curviate.com. A copy of your data in a machine-readable format is available on request. We don't make automated decisions about you that have a legal or similarly significant effect. You can also complain to a supervisory authority; ours is the Landesbeauftragte für Datenschutz und Informationsfreiheit Nordrhein-Westfalen (LDI NRW), www.ldi.nrw.de, though you're free to complain to the one in your own country instead.

Keeping it secure

Credentials are encrypted and never logged, returned, or shared. LinkedIn actions run through native, humanized flows; full detail is on our Security & Compliance page. If a breach puts your rights at risk, we'll notify the authorities and you, as GDPR requires. Curviate isn't directed at, or offered to, anyone under 16.

Changes

We'll update this page when our practices change, and reset the cookie prompt if the change is material.

Contact