Event reference
Every event Curviate can deliver to your webhook endpoint, grouped by source.
Reading the catalogue needs only an API key. Subscribing needs a
connected LinkedIn account for messaging, user, and post webhooks, because
account_ids is required and non-empty there. Read your ids with GET /v1/accounts; if it is empty,
start with the
Quickstart.
Quick reference
not_realtime means a delay; no_longer_realtime means not delivered. Message
events are inbound only.
| Event | Source | Availability | Description |
|---|---|---|---|
message.received | messaging | Real-time | Inbound message or InMail arrived. |
message.read | messaging | Real-time | A message was read. |
message.reaction | messaging | Real-time | Reaction added. |
message.edited | messaging | Real-time | Message edited. |
message.deleted | messaging | Real-time | Message deleted. |
chat.updated | messaging | Real-time · opt-in | Thread archived, muted, or read-state changed. |
chat.deleted | messaging | Real-time · opt-in | Thread deleted. |
connection.accepted | user | not_realtime (~2h) | Your invitation was accepted. |
connection.new | user | not_realtime (~4h) · opt-in | Any new connection. |
account.created | account_status | Real-time | First successful link. |
account.connected | account_status | Real-time | Healthy, including a reactivated account. |
account.synced | account_status | no_longer_realtime | A sync cycle completed. |
account.reconnected | account_status | Real-time | Re-authorized in place. |
account.reconnect_needed | account_status | Real-time | Needs re-authentication. |
account.restricted | account_status | not_realtime | Connected, but some actions fail. |
account.creation_failed | account_status | no_longer_realtime | First link attempt failed. |
account.disconnected | account_status | Real-time | Disconnected; cannot act. |
account.error | account_status | Real-time | Sync error, not a credentials issue. |
account.paused | account_status | no_longer_realtime | Sync paused, not an error. |
account.connecting | account_status | no_longer_realtime | A link is in progress. |
account.permission_revoked | account_status | no_longer_realtime | LinkedIn permissions withdrawn. |
account.initial_sync.running | account_status | Real-time · opt-in | History backfill started. |
account.initial_sync.completed | account_status | Real-time · opt-in | Backfill done; history queryable. |
account.initial_sync.failed | account_status | Real-time · opt-in | Backfill failed, with a reason. |
post.published | post | Real-time | A scheduled post was published. |
post.publish_failed | post | Real-time | A scheduled post could not be published. |
motion.status_changed | motion | Real-time | A Motion changed status. |
enrollment.started | motion | Real-time | A Contact's Enrollment started. |
enrollment.skipped | motion | Real-time | A Contact was not enrolled. |
enrollment.finished | motion | Real-time | An Enrollment ended. |
step.executed | motion | Real-time · opt-in | A Step ran. |
step.failed | motion | Real-time | A Step was skipped or failed. |
motion.signal | motion | Real-time | A Sequence reached an emit step. |
Delivery availability
GET /v1/webhooks/events returns the live catalogue, with an availability field:
| Value | Meaning |
|---|---|
omitted, or realtime | Real time. The three |
not_realtime | Delivered, not sub-second. |
no_longer_realtime | Not delivered. For most, read
|
GET /v1/webhooks/events returns 36 events across 7
sources. This article documents the 33 across 5 sources
you can subscribe to, including the 7 motion events below. Create
rejects the other sources with
400 INVALID_REQUEST; the create schema is the authority.
account.synced, account.creation_failed,
account.connecting, account.paused, and
account.permission_revoked
stay in the create enum: subscribing returns 201 and no delivery
ever arrives. If a subscription is silent, check its events here first.
Default and opt-in events
Omitting events on create subscribes to the source's default set:
| 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.*) |
post | post.published and post.publish_failed |
motion | every motion event except step.executed |
chat.updated, chat.deleted, connection.new, step.executed, and the account.initial_sync.*
events are never auto-subscribed; name them in events.
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 (wdl_...) changes on every retry. data always carries account_id, event,
and occurred_at: deduplicate on those three. Message and connection events carry
their content in data; account-status events carry state only; post events carry
ids, timestamps, and a failure code, never the post text or media; Motion events
carry ids and reason codes, plus the Contact record, never message text.
When an account needs re-authentication (account.reconnect_needed), the next call
is POST /v1/auth/intent with its account_id.
Motion events
Seven events report what a Motion does. A motion webhook needs
a non-empty account_ids, like messaging. Omit events and it gets all but
step.executed, which is opt-in because of its volume.
curl -X POST https://api.curviate.com/v1/webhooks \
-H "Authorization: Bearer cvt_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "motion",
"request_url": "https://hooks.example.com/curviate",
"account_ids": ["acc_YOUR_ACCOUNT_ID"],
"motion_ids": ["mot_YOUR_MOTION_ID"]
}'Every data carries event and occurred_at, plus these keys. Nothing else is
delivered: no message text or any text a Step rendered.
| Event | data keys besides event and occurred_at |
|---|---|
motion.status_changed | motion_id, account_id (always null), from, to, reason, changed_by_user_id (null when Curviate made the change). No contact. |
enrollment.started | motion_id, enrollment_id, account_id, sequence_version, contact |
enrollment.skipped | motion_id, enrollment_id, account_id (null when none was assigned), reason, detail, contact |
enrollment.finished | motion_id, enrollment_id, account_id, reason, replied, last_step_id, contact |
step.executed | motion_id, enrollment_id, account_id, step_id, action, variant_id, used_fallback, contact |
step.failed | motion_id, enrollment_id, account_id, step_id, action, variant_id, outcome (skipped or failed), reason, contact |
motion.signal | name (the emitted signal), motion_id, enrollment_id, step_id, account_id, contact |
contact is your own team's Contact record, whole (id, linkedin_url,
full_name, first_name, last_name, headline, job_title, company, email,
location, about, extra, tags), or null once that Contact is deleted.
An event with an account_id goes to the webhooks that list that account.
motion.status_changed, and an enrollment.skipped with no account yet, go once to
each webhook whose account_ids overlap the Motion's sending accounts, with
account_id null. Motion events are not kept in the 24-hour event log.
Filter by Motion. Every webhook, of any source, takes an optional motion_ids
(up to 100 of your own mot_ ids) on create and update. When set, only events whose
data.motion_id is listed are delivered; an event without a motion_id is not.
Another team's id is a 400.
Motion chats. The message.* events carry motion_id and enrollment_id in
data: set when the chat belongs to an Enrollment of that account, null otherwise.
Errors you may hit
| Code | HTTP | Cause | Fix |
|---|---|---|---|
INVALID_REQUEST | 400 | An event outside the chosen source, or an unknown source. | The message lists the accepted values. |
ACCOUNT_NOT_FOUND | 404 | An acc_... in account_ids is not owned by this tenant. | Read your ids from GET /v1/accounts. |
UNAUTHORIZED | 401 | Missing or invalid API key. | Send Authorization: Bearer cvt_live_<key>. |
RATE_LIMIT_TENANT | 429 | Tenant quota exceeded. | Honour the Retry-After header. |
Full envelope shapes are in the error reference.
Next steps
- Messaging events: full payloads for the 7 messaging events.
- User events: full payloads for the 2 connection events.
- Account status events: full payloads for all 15 account-status events.
- Post events: payloads for scheduled-post outcomes.
- Motion events: the seven
motionevents,motion_ids, and the Motion keys onmessage.*. - Verifying signatures: validate every delivery before you process it.