Webhooks

Event reference

Every event Curviate can deliver to your webhook endpoint, grouped by source. All are Core-tier unless noted.

Before you start

Reading this catalogue needs only an API key. Subscribing to any of it needs a connected LinkedIn account, because account_ids is required and non-empty on create. Read your ids with GET /v1/accounts; if that returns an empty list, connect an account first (see Authentication and accounts).

Quick reference

The Availability column marks how an event is delivered. Events with no marker are delivered in real time. See Delivery availability below for the full meaning of each marker.

Early access: latency and directionality

The Availability markers below, and the inbound-only direction note on the message.* events, both reflect the current platform substrate and are an early surface. The event names, sources, and payload envelope are stable and safe to build against; the exact latency labels and directionality are refined as live validation completes.

EventSourceAvailabilityDescription
message.receivedmessagingReal-timeAn inbound message arrived in any chat (classic message or InMail).
message.deliveredmessagingReal-timeAn outbound message was delivered.
message.readmessagingReal-timeA message in a chat was read by the recipient.
message.reactionmessagingReal-timeA reaction was added to a message.
message.editedmessagingReal-timeA message was edited after send.
message.deletedmessagingReal-timeA message was deleted.
chat.updatedmessagingReal-time · opt-inA chat's container state changed (e.g. archived, muted, read-state), useful for inbox automation.
chat.deletedmessagingReal-time · opt-inA chat thread was deleted.
connection.acceptedusernot_realtime (~8h)A pending invitation was accepted by the recipient (a new relation).
connection.newusernot_realtime (~4h) · opt-inAny new relation appeared on the account, not just ones you invited.
account.createdaccount_statusReal-timeA new account link completed successfully for the first time.
account.connectedaccount_statusReal-timeThe account is in a healthy, connected state, including when a connect reactivates an account you had previously disconnected.
account.syncedaccount_statusno_longer_realtimeA synchronization cycle completed successfully.
account.reconnectedaccount_statusReal-timeAn existing account was re-authorized in place (credentials refreshed).
account.reconnect_neededaccount_statusReal-timeCredentials expired or the session cookie was invalidated, the account needs reconnection.
account.restrictedaccount_statusnot_realtimeLinkedIn restricted the account, actions on it will fail until the restriction is lifted.
account.creation_failedaccount_statusno_longer_realtimeThe initial account-link attempt failed before completion (terminal).
account.disconnectedaccount_statusReal-timeThe account is in the disconnected state and cannot act until it is reconnected.
account.erroraccount_statusReal-timeAccount synchronization encountered an error (needs investigation, not a credentials issue).
account.pausedaccount_statusReal-timeAccount synchronization was stopped or paused externally. Not an error, may resume automatically.
account.connectingaccount_statusno_longer_realtimeAn account link is in progress (transient, high-frequency).
account.permission_revokedaccount_statusno_longer_realtimeA LinkedIn-side scope or permission the account relies on was withdrawn.
account.initial_sync.runningaccount_statusReal-time · opt-inThe initial history backfill for a newly connected account has started (informational).
account.initial_sync.completedaccount_statusReal-time · opt-inThe initial history backfill completed, full LinkedIn history is now queryable.
account.initial_sync.failedaccount_statusReal-time · opt-inThe initial history backfill failed, the delivery carries a neutral reason.

Delivery availability

GET /v1/webhooks/events returns the live catalogue, with an availability field on the events that are not delivered in real time. Three values are possible:

ValueMeaning
omitted, or realtime

Delivered in real time (the default). Most events omit the field; the three account.initial_sync.* events carry realtime explicitly.

not_realtime

The event is delivered, but not guaranteed sub-second. Do not build latency-sensitive logic on it. connection.accepted and connection.new are poll-only, a delay of hours, not seconds. account.restricted is normally poll-detected too (up to ~24h), but a connect that observes the restriction can also deliver it immediately.

no_longer_realtime

The event is no longer delivered on the current platform. Use the read-path alternative named in the event's description; for most that is an on-demand account read via GET /v1/accounts/{account_id}. Applies to account.synced, account.creation_failed, account.connecting, and account.permission_revoked.

The catalogue is wider than what you can subscribe to

GET /v1/webhooks/events returns 28 events across 5 sources. This article documents the 25 across 3 sources you can actually subscribe to. The extra source values in the catalogue response are rejected by create with 400 INVALID_REQUEST: source: Invalid discriminator value. Expected 'messaging' | 'user' | 'account_status', so treat the create schema, not the catalogue, as the authority on what you can register.

A dead event still returns 201

The four no_longer_realtime events remain in the create enum. Naming one in events[] succeeds, returns 201, and warns you about nothing; you simply never receive a delivery. If a subscription is silent, check its events against the availability markers above before you debug your endpoint.

Default and opt-in events

When you create a webhook without an events array, it subscribes to that source's default set. Any event listed for the source can also be subscribed explicitly.

SourceDefault events (when events is omitted)
messagingmessage.received
userconnection.accepted
account_statusthe 12 lifecycle events (not the three account.initial_sync.*)
Opt-in only

chat.updated, chat.deleted, connection.new, and the three account.initial_sync.* events are never auto-subscribed. To receive them, name them explicitly in the events array when creating (or updating) the webhook.

Payload structure

Every delivery shares the same outer envelope:

{
  "id":           "wdl_YOUR_DELIVERY_ID",
  "webhook_id":   "wh_YOUR_WEBHOOK_ID",
  "event":        "message.received",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.received",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "text":        "Hello, how can I help?",
    "sender": {
      "name":        "Alex Jordan",
      "profile_url": "https://www.linkedin.com/in/alexjordan",
      "provider_id": "urn:li:member:123456789"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T09:15:00.000Z"
  },
  "delivered_at": "2026-05-28T09:15:01.234Z"
}

id is the delivery ID (wdl_...); webhook_id (wh_...) references the webhook.

Three fields are guaranteed inside data on every delivery, whatever the source: account_id, event (the canonical event name, identical to the top-level event), and occurred_at. Together they are the recommended deduplication key, because the top-level id changes on every retry.

Content-bearing payloads

Content events (the messaging message.* events and the connection.* events) carry the event's content directly in data (text, sender, and attachments for messaging; the new contact's profile fields for connections), so your handler receives everything it needs in one delivery. Structural events (the account-status lifecycle events and account.initial_sync.*) carry account state only, not LinkedIn content. See the per-source articles for the full payload shape.

Messaging events

Messaging events notify your endpoint whenever a message or chat changes state on a connected account. The message.* payloads include the full message content; the chat.* events describe a chat thread rather than a single message.

message.received

Fired when an inbound message arrives in any chat, classic LinkedIn messages or InMail. Fires for inbound messages only; a message the account sent does not fire it (use message.delivered for outbound confirmation).

message.delivered

Fired when an outbound message was delivered. Use it to close the send loop in fire-and-forget agent patterns.

message.read

Fired when a message in a chat was read by the recipient. The payload may include reader identity where available.

message.reaction

Fired when a reaction is added to a message. The data object includes a reaction field with the reaction value.

message.edited

Fired when a message is edited after send. The text field carries the post-edit content.

message.deleted

Fired when a message is deleted. The text field may be absent.

chat.updated

Fired when a chat's container state changed, for example archived, muted, or read-state. Useful for inbox automation. chat.updated is opt-in, name it in events[] to receive it. See the Messaging events article for the delivered data shape.

chat.deleted

Fired when a chat thread was deleted. chat.deleted is opt-in, name it in events[] to receive it.

User events

connection.accepted

Fired when a pending invitation you sent was accepted by the recipient. Once received, the two members are connected and messaging is available without InMail credits.

connection.new

Fired when any new relation appears on the account, not just ones you invited, so it is a superset of connection.accepted. connection.new is opt-in, name it in events[] to receive it.

User events are polled, not real-time

Both user events carry availability: "not_realtime": LinkedIn relationship state is polled on a schedule, so delivery lags the real event. connection.accepted may arrive up to ~8 hours after the recipient accepts; connection.new up to ~4 hours after the relation appears. A freshly connected account may also receive a backfill burst of connection.new events for its pre-existing network during the initial-sync window. This is a platform polling constraint, not a Curviate bug, design for the delay and for volume tolerance.

Account status events

Account events track the lifecycle of connected LinkedIn accounts, from creation through session expiry and eventual removal.

account.error vs account.paused

account.error means synchronization halted due to an error (needs investigation, not a credentials issue). account.paused means synchronization was stopped or paused externally, not an error, and it may resume automatically. Treat them differently in your handler.

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.

account.created

Fired when a new account link completes successfully for the first time, the initial connect path.

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. The event name and data.status in the same delivery never disagree.

account.synced · no_longer_realtime

Historically fired when a synchronization cycle completed. No longer delivered on the current platform, and there is no equivalent event or read-path, do not build on it.

account.reconnected

Fired when an existing account was re-authorized in place, credentials refreshed 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.

account.reconnect_needed

Fired when credentials expire or the session cookie is invalidated. The account remains in your workspace but is inactive until 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.

account.restricted · not_realtime

Fired when 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. A daily account reconciliation normally notices the restriction (allow up to 24 hours for that path), but a connect that observes the account already restricted can also deliver this event immediately. Read the account directly with GET /v1/accounts/{account_id} if you need the state sooner than either.

account.creation_failed · no_longer_realtime

The initial account-link attempt failed before completion (terminal, no account was established). No longer delivered, detect via an on-demand account read (GET /v1/accounts/{account_id}).

account.disconnected

Fired when the account is in the disconnected state and cannot act until it is reconnected. Causes include an explicit disconnect or deletion, 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 stop delivering; where it was not, read the account back to confirm.

account.error

Fired when account synchronization encountered an error (needs investigation, not a credentials issue).

account.paused

Fired when account synchronization was stopped or paused externally, not an error, and it may resume automatically.

account.connecting · no_longer_realtime

Historically a transient, high-frequency event fired while an account link was in progress. No longer delivered on the current platform. Detect link-in-progress via the account read (GET /v1/accounts/{account_id}) instead.

account.permission_revoked · no_longer_realtime

A LinkedIn-side scope or permission the account relies on was withdrawn. No longer delivered, detect via an on-demand account read (GET /v1/accounts/{account_id}).

account.initial_sync.running · account.initial_sync.completed · account.initial_sync.failed

The initial history backfill for a newly connected account is bracketed by these three opt-in events: exactly one account.initial_sync.running (informational), followed by exactly one of account.initial_sync.completed (the actionable signal, full LinkedIn history is now queryable) or account.initial_sync.failed. The failed delivery carries a neutral reason, one of internal, proxy, or provider. See the Account status events article for the full treatment.

Errors you may hit

CodeHTTPCauseFix
INVALID_REQUEST400An event that does not belong to the chosen source, or a source outside messaging, user, account_status.Read the message, it lists the accepted values.
ACCOUNT_NOT_FOUND404An acc_... in account_ids is not owned by this workspace.Read your ids from GET /v1/accounts.
UNAUTHORIZED401Missing or invalid API key.Send Authorization: Bearer cvt_live_<key>.
RATE_LIMIT_TENANT429Workspace quota exceeded.Honour the Retry-After header.

Full envelope shapes are in the error reference.

Next steps

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