Event reference
Every event Curviate can deliver to your webhook endpoint, grouped by source. All are Core-tier unless noted.
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.
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.
| Event | Source | Availability | Description |
|---|---|---|---|
message.received | messaging | Real-time | An inbound message arrived in any chat (classic message or InMail). |
message.delivered | messaging | Real-time | An outbound message was delivered. |
message.read | messaging | Real-time | A message in a chat was read by the recipient. |
message.reaction | messaging | Real-time | A reaction was added to a message. |
message.edited | messaging | Real-time | A message was edited after send. |
message.deleted | messaging | Real-time | A message was deleted. |
chat.updated | messaging | Real-time · opt-in | A chat's container state changed (e.g. archived, muted, read-state), useful for inbox automation. |
chat.deleted | messaging | Real-time · opt-in | A chat thread was deleted. |
connection.accepted | user | not_realtime (~8h) | A pending invitation was accepted by the recipient (a new relation). |
connection.new | user | not_realtime (~4h) · opt-in | Any new relation appeared on the account, not just ones you invited. |
account.created | account_status | Real-time | A new account link completed successfully for the first time. |
account.connected | account_status | Real-time | The account is in a healthy, connected state, including when a connect reactivates an account you had previously disconnected. |
account.synced | account_status | no_longer_realtime | A synchronization cycle completed successfully. |
account.reconnected | account_status | Real-time | An existing account was re-authorized in place (credentials refreshed). |
account.reconnect_needed | account_status | Real-time | Credentials expired or the session cookie was invalidated, the account needs reconnection. |
account.restricted | account_status | not_realtime | LinkedIn restricted the account, actions on it will fail until the restriction is lifted. |
account.creation_failed | account_status | no_longer_realtime | The initial account-link attempt failed before completion (terminal). |
account.disconnected | account_status | Real-time | The account is in the disconnected state and cannot act until it is reconnected. |
account.error | account_status | Real-time | Account synchronization encountered an error (needs investigation, not a credentials issue). |
account.paused | account_status | Real-time | Account synchronization was stopped or paused externally. Not an error, may resume automatically. |
account.connecting | account_status | no_longer_realtime | An account link is in progress (transient, high-frequency). |
account.permission_revoked | account_status | no_longer_realtime | A LinkedIn-side scope or permission the account relies on was withdrawn. |
account.initial_sync.running | account_status | Real-time · opt-in | The initial history backfill for a newly connected account has started (informational). |
account.initial_sync.completed | account_status | Real-time · opt-in | The initial history backfill completed, full LinkedIn history is now queryable. |
account.initial_sync.failed | account_status | Real-time · opt-in | The 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:
| Value | Meaning |
|---|---|
omitted, or realtime | Delivered in real time (the default). Most events omit the field; the
three |
not_realtime | The event is delivered, but not guaranteed sub-second. Do not
build latency-sensitive logic on it. |
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/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.
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.
| Source | Default events (when events is omitted) |
|---|---|
messaging | message.received |
user | connection.accepted |
account_status | the 12 lifecycle events (not the three account.initial_sync.*) |
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 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.
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 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.
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
| Code | HTTP | Cause | Fix |
|---|---|---|---|
INVALID_REQUEST | 400 | An 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_FOUND | 404 | An acc_... in account_ids is not owned by this workspace. | Read your ids from GET /v1/accounts. |
UNAUTHORIZED | 401 | Missing or invalid API key. | Send Authorization: Bearer cvt_live_<key>. |
RATE_LIMIT_TENANT | 429 | Workspace quota exceeded. | Honour the Retry-After header. |
Full envelope shapes are in the error reference.
Next steps
- Messaging events: full payloads for the 8 messaging events.
- User events: full payloads for the 2 connection events.
- Account status events: full payloads for all 15 account-status events.
- Verifying signatures: validate every delivery before you process it.