Webhooks

Account status events

All 15 events that track the full lifecycle of a connected LinkedIn account: creation, sync, authentication changes, restriction, errors, removal, and initial history backfill.

Quick reference

The Availability column marks how each event is delivered. Events with no marker are delivered in real time; account.restricted is normally detected by a daily poll, but can also arrive immediately from a connect that observes the restriction, and four legacy events are no_longer_realtime (not delivered on the current platform), see the callouts below.

EventAvailabilityDescription
account.createdReal-timeA new account link completed successfully for the first time.
account.connectedReal-timeA LinkedIn account is in a healthy, connected state, including when a connect reactivates an account you had previously disconnected.
account.syncedno_longer_realtimeA synchronization cycle completed successfully.
account.reconnectedReal-timeAn account was re-authorized in place, credentials refreshed.
account.reconnect_neededReal-timeCredentials expired, the account requires re-authentication.
account.restrictednot_realtimeLinkedIn restricted the account, actions on it will fail until the restriction is lifted.
account.creation_failedno_longer_realtimeThe initial account-link attempt failed before completion.
account.disconnectedReal-timeThe account is in the disconnected state and cannot act until it is reconnected.
account.pausedReal-timeAccount synchronization was stopped or paused externally.
account.errorReal-timeAccount synchronization halted due to an error, action required.
account.connectingno_longer_realtimeAn account connection flow is in progress (transient, high-frequency).
account.permission_revokedno_longer_realtimeRequired LinkedIn permissions were withdrawn from the account.
account.initial_sync.runningReal-time · opt-inThe initial history backfill started (informational, bracket open).
account.initial_sync.completedReal-time · opt-inThe initial history backfill completed, full LinkedIn history is now queryable.
account.initial_sync.failedReal-time · opt-inThe initial history backfill failed, the delivery carries a neutral reason.

Subscribe to all or a subset when creating your webhook:

curl -X POST https://api.curviate.com/v1/webhooks \
  -H "Authorization: Bearer cvt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "account_status",
    "request_url": "https://hooks.example.com/curviate",
    "account_ids": ["acc_YOUR_ACCOUNT_ID"]
  }'

Omitting events subscribes to the 12 lifecycle events by default. The three account.initial_sync.* events are opt-in. Name them explicitly in events[] to receive them.

Four events are no longer delivered

account.synced, account.creation_failed, account.connecting, and account.permission_revoked carry availability: "no_longer_realtime". They are not delivered on the current platform. For account.creation_failed and account.permission_revoked, detect the state via an on-demand account read (GET /v1/accounts/{account_id}). For account.synced and account.connecting there is no equivalent event and no direct read-path signal. This is honest deprecation, not a bug.

Initial-sync bracket

On each account connect, the initial history backfill is bracketed by a bounded 2-event sequence: exactly one account.initial_sync.running (bracket open, informational), followed by exactly one of account.initial_sync.completed or account.initial_sync.failed. account.initial_sync.completed is the actionable signal, the account's full LinkedIn history is now queryable. These three events are opt-in.

account.connecting, no longer delivered

account.connecting was historically a transient, high-frequency event fired while a link was in progress. Today it carries availability: "no_longer_realtime" and is not delivered. Do not build on receiving it. Detect link-in-progress via the account read (GET /v1/accounts/{account_id}) instead.

Connection lifecycle

account.created

Fired when a new account link completes successfully for the first time, the initial connect path. Use this event to start any post-onboarding logic in your agent.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.created",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "active",
    "occurred_at": "2026-05-28T08:00:00.000Z"
  },
  "delivered_at": "2026-05-28T08:00:01.123Z"
}

account.connected

Fired when the account is in a healthy, connected state. That covers a health check confirming the account is fine, and a connect that turns out to reactivate an account you had previously disconnected ("recovered": true) and finds it healthy.

A reactivating connect always reports the state the account actually comes back in, so if the reconnection is not clean you get the event for that state instead, one of account.reconnect_needed, account.restricted, or account.disconnected, matching the status the connect response itself returned.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.connected",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "active",
    "occurred_at": "2026-05-28T08:05:00.000Z"
  },
  "delivered_at": "2026-05-28T08:05:01.456Z"
}

account.reconnected

Fired when an account that previously required re-authentication is back online, credentials refreshed in place without creating a new account ID.

Reactivating an account you had previously disconnected does not fire this event. That connect reports the state the account comes back in, so subscribe to account.connected (and the degraded states) if you want to hear about reactivations.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.reconnected",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "active",
    "occurred_at": "2026-05-28T10:00:00.000Z"
  },
  "delivered_at": "2026-05-28T10:00:01.234Z"
}

account.reconnect_needed

Fired when the account's session expires or its credentials become invalid. The account remains in the workspace but is inactive until the user completes re-authentication. Trigger a reconnect flow from the dashboard or call POST /v1/auth/intent with the account's account_id in the body to re-authenticate it in place.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.reconnect_needed",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "reconnect_needed",
    "occurred_at": "2026-05-28T11:30:00.000Z"
  },
  "delivered_at": "2026-05-28T11:30:01.567Z"
}

Sync states

account.synced · no_longer_realtime

Historically fired when a synchronization cycle completed successfully. It now carries availability: "no_longer_realtime" and is not delivered on the current platform. there is no equivalent event and no read-path signal. Do not build on it. The shape it carried is shown below for reference:

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.synced",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "active",
    "occurred_at": "2026-05-28T08:10:00.000Z"
  },
  "delivered_at": "2026-05-28T08:10:01.789Z"
}

Error and pause states

account.error vs account.paused

These are distinct states. account.error means synchronization halted due to an error and action is required to restore the account. account.paused means synchronization was intentionally stopped or paused, a deliberate state, not a failure condition. Your handler should treat them differently: account.error warrants an alert or automated reconnect attempt; account.paused is expected and requires no immediate action.

account.error

Fired when account synchronization halted due to an error. Action is required to restore the account, check the dashboard for details and consider triggering a reconnect flow.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.error",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "reconnect_needed",
    "occurred_at": "2026-05-28T12:00:00.000Z"
  },
  "delivered_at": "2026-05-28T12:00:01.890Z"
}

account.paused

Fired when account synchronization was intentionally paused, a deliberate, non-error state. No immediate action is required.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.paused",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "reconnect_needed",
    "occurred_at": "2026-05-28T13:00:00.000Z"
  },
  "delivered_at": "2026-05-28T13:00:01.012Z"
}

account.restricted · not_realtime

LinkedIn restricted the account. Actions on it will fail until the restriction is lifted, so treat this as a stop signal: pause outreach for the account and surface it for a human to look at.

A daily account reconciliation is what normally notices the restriction, so allow up to 24 hours for that path. A connect that observes the account already restricted can also deliver this event immediately, so do not assume every delivery lagged a full day. Either way, an on-demand read with GET /v1/accounts/{account_id} always reports the current state.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.restricted",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "restricted",
    "occurred_at": "2026-05-28T13:30:00.000Z"
  },
  "delivered_at": "2026-05-28T13:30:01.204Z"
}

account.permission_revoked · no_longer_realtime

A LinkedIn-side scope or permission the account relies on was withdrawn; the account may require re-authorization to restore access. This event carries availability: "no_longer_realtime" and is not delivered on the current platform. detect the state via an on-demand account read (GET /v1/accounts/{account_id}). The shape it carried:

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.permission_revoked",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "restricted",
    "occurred_at": "2026-05-28T14:00:00.000Z"
  },
  "delivered_at": "2026-05-28T14:00:01.234Z"
}

Terminal states

account.creation_failed · no_longer_realtime

The initial account-link attempt failed before completion, terminal, and no account was established for operational use. This event carries availability: "no_longer_realtime" and is not delivered on the current platform. detect the outcome via an on-demand account read (GET /v1/accounts/{account_id}). The shape it carried:

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.creation_failed",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "disconnected",
    "occurred_at": "2026-05-28T07:55:00.000Z"
  },
  "delivered_at": "2026-05-28T07:55:01.345Z"
}

account.disconnected

Fired when the account is in the disconnected state and cannot act until it is reconnected; data.status carries that state. Causes include an explicit disconnect, removal via the dashboard or the DELETE /v1/accounts/{id} API, and a connect or reconnect attempt that observes an already-disconnected account without removing it.

Where the account was actually removed, its account ID will subsequently 404 and any webhooks scoped to it will stop delivering, but do not assume every delivery of this event means the account is gone: read the account back (GET /v1/accounts/{account_id}) if you need to tell the two cases apart.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.disconnected",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "disconnected",
    "occurred_at": "2026-05-28T15:00:00.000Z"
  },
  "delivered_at": "2026-05-28T15:00:01.456Z"
}

In-progress state

account.connecting · no_longer_realtime

Historically a transient, high-frequency event fired while an account connection flow was in progress. It now carries availability: "no_longer_realtime" and is not delivered on the current platform, and there is no equivalent event. Do not build on receiving it. Detect link-in-progress via the account read (GET /v1/accounts/{account_id}), and wait for account.created or account.connected as the completion signal. The shape it carried:

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.connecting",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "status":      "connecting",
    "occurred_at": "2026-05-28T07:59:30.000Z"
  },
  "delivered_at": "2026-05-28T07:59:31.678Z"
}

Initial sync

The initial history backfill for a newly connected account is reported by three opt-in events that form a bounded bracket, exactly one account.initial_sync.running, followed by exactly one of account.initial_sync.completed or account.initial_sync.failed. Name them in events[] to receive them.

account.initial_sync.running

The initial history backfill has started. Informational, bracket open.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.initial_sync.running",
  "data": {
    "status":      "active",
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "account.initial_sync.running",
    "occurred_at": "2026-05-28T08:00:00.000Z"
  },
  "delivered_at": "2026-05-28T08:00:01.000Z"
}

account.initial_sync.completed

The initial history backfill completed. This is the actionable signal: the account's full LinkedIn history is now queryable.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.initial_sync.completed",
  "data": {
    "status":      "active",
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "account.initial_sync.completed",
    "occurred_at": "2026-05-28T08:12:00.000Z"
  },
  "delivered_at": "2026-05-28T08:12:01.000Z"
}

account.initial_sync.failed

The initial history backfill failed. The delivery carries a neutral reason whose value is exactly one of internal, proxy, or provider.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "account.initial_sync.failed",
  "data": {
    "status":      "active",
    "reason":      "provider",
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "account.initial_sync.failed",
    "occurred_at": "2026-05-28T08:12:00.000Z"
  },
  "delivered_at": "2026-05-28T08:12:01.000Z"
}
The reason enum

On account.initial_sync.failed, reason is exactly one of internal, proxy, or provider. Treat it as a closed set; no other value is delivered.

Delivery payload

Every account-status data object carries account_id, event, occurred_at, and status. account.initial_sync.failed adds reason. There is no timestamp field on any Curviate delivery.

Lifecycle events (the 12):

{
  "status":      "active",
  "account_id":  "acc_YOUR_ACCOUNT_ID",
  "event":       "account.connected",
  "occurred_at": "2026-05-28T11:30:00.000Z"
}

account.initial_sync.* events (the 3):

{
  "status":      "active",
  "reason":      "provider",
  "account_id":  "acc_YOUR_ACCOUNT_ID",
  "event":       "account.initial_sync.failed",
  "occurred_at": "2026-05-28T08:12:00.000Z"
}
  • account_id: the acc_... ID of the affected account in your workspace.
  • event: the canonical Curviate event name. Guaranteed on every delivery, and identical to the top-level event.
  • occurred_at: when the state transition occurred. Guaranteed on every delivery.
  • status: the account's lifecycle status once the event has been applied. One of active, connecting, reconnect_needed, restricted, disconnected. Guaranteed on every delivery, and the same value for a given event whichever way Curviate noticed the change. It is the same vocabulary GET /v1/accounts/{account_id} returns in its status field.
  • reason: only on account.initial_sync.failed; one of internal, proxy, provider.

The table below is the complete event to status mapping. Every delivery of an event carries the status shown, so you can branch on either field.

eventdata.status
account.createdactive
account.connectedactive
account.reconnectedactive
account.syncedactive
account.connectingconnecting
account.reconnect_neededreconnect_needed
account.errorreconnect_needed
account.pausedreconnect_needed
account.restrictedrestricted
account.permission_revokedrestricted
account.disconnecteddisconnected
account.creation_faileddisconnected
account.initial_sync.runningactive
account.initial_sync.completedactive
account.initial_sync.failedactive
Two events can report the same status

The mapping is many-to-one, so data.status narrows the account state but does not identify the event. account.restricted and account.permission_revoked both report "status": "restricted", and both are in the default subscription set, so a default subscriber can receive either. The three account.initial_sync.* events report "status": "active" because the history backfill does not change the account's state. When you need to know exactly what happened, branch on event; when you need to know the resulting account state, read data.status.

Account status events carry structural state metadata only: no LinkedIn message content, profile text, or attachment data.

The data[] field-remapping array documented on other sources does not apply here. Note that the API currently accepts a data array on an account_status create and silently ignores it rather than rejecting it, so do not read a successful create as confirmation that a remapping took effect.

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
curviate-themeRemembers light/dark mode (local storage, not a cookie)Persistent
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