Webhooks

Event reference

Every event Curviate can deliver to your webhook endpoint, grouped by source.

Before you start

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.

EventSourceAvailabilityDescription
message.receivedmessagingReal-timeInbound message or InMail arrived.
message.readmessagingReal-timeA message was read.
message.reactionmessagingReal-timeReaction added.
message.editedmessagingReal-timeMessage edited.
message.deletedmessagingReal-timeMessage deleted.
chat.updatedmessagingReal-time · opt-inThread archived, muted, or read-state changed.
chat.deletedmessagingReal-time · opt-inThread deleted.
connection.acceptedusernot_realtime (~2h)Your invitation was accepted.
connection.newusernot_realtime (~4h) · opt-inAny new connection.
account.createdaccount_statusReal-timeFirst successful link.
account.connectedaccount_statusReal-timeHealthy, including a reactivated account.
account.syncedaccount_statusno_longer_realtimeA sync cycle completed.
account.reconnectedaccount_statusReal-timeRe-authorized in place.
account.reconnect_neededaccount_statusReal-timeNeeds re-authentication.
account.restrictedaccount_statusnot_realtimeConnected, but some actions fail.
account.creation_failedaccount_statusno_longer_realtimeFirst link attempt failed.
account.disconnectedaccount_statusReal-timeDisconnected; cannot act.
account.erroraccount_statusReal-timeSync error, not a credentials issue.
account.pausedaccount_statusno_longer_realtimeSync paused, not an error.
account.connectingaccount_statusno_longer_realtimeA link is in progress.
account.permission_revokedaccount_statusno_longer_realtimeLinkedIn permissions withdrawn.
account.initial_sync.runningaccount_statusReal-time · opt-inHistory backfill started.
account.initial_sync.completedaccount_statusReal-time · opt-inBackfill done; history queryable.
account.initial_sync.failedaccount_statusReal-time · opt-inBackfill failed, with a reason.
post.publishedpostReal-timeA scheduled post was published.
post.publish_failedpostReal-timeA scheduled post could not be published.
motion.status_changedmotionReal-timeA Motion changed status.
enrollment.startedmotionReal-timeA Contact's Enrollment started.
enrollment.skippedmotionReal-timeA Contact was not enrolled.
enrollment.finishedmotionReal-timeAn Enrollment ended.
step.executedmotionReal-time · opt-inA Step ran.
step.failedmotionReal-timeA Step was skipped or failed.
motion.signalmotionReal-timeA Sequence reached an emit step.

Delivery availability

GET /v1/webhooks/events returns the live catalogue, with an availability field:

ValueMeaning
omitted, or realtime

Real time. The three account.initial_sync.* events carry realtime explicitly.

not_realtime

Delivered, not sub-second. connection.accepted and connection.new are polled (hours). account.restricted is polled (up to ~24h) unless a connect observes it, or one LinkedIn product on the account stops working.

no_longer_realtime

Not delivered. For most, read GET /v1/accounts/{account_id} instead.

The catalogue is wider than what you can subscribe to

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.

Five events are no longer delivered

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:

SourceDefault events (when events is omitted)
messagingmessage.received
userconnection.accepted
account_statusthe 12 lifecycle events (not the three account.initial_sync.*)
postpost.published and post.publish_failed
motionevery 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.

Eventdata keys besides event and occurred_at
motion.status_changedmotion_id, account_id (always null), from, to, reason, changed_by_user_id (null when Curviate made the change). No contact.
enrollment.startedmotion_id, enrollment_id, account_id, sequence_version, contact
enrollment.skippedmotion_id, enrollment_id, account_id (null when none was assigned), reason, detail, contact
enrollment.finishedmotion_id, enrollment_id, account_id, reason, replied, last_step_id, contact
step.executedmotion_id, enrollment_id, account_id, step_id, action, variant_id, used_fallback, contact
step.failedmotion_id, enrollment_id, account_id, step_id, action, variant_id, outcome (skipped or failed), reason, contact
motion.signalname (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

CodeHTTPCauseFix
INVALID_REQUEST400An event outside the chosen source, or an unknown source.The message lists the accepted values.
ACCOUNT_NOT_FOUND404An acc_... in account_ids is not owned by this tenant.Read your ids from GET /v1/accounts.
UNAUTHORIZED401Missing or invalid API key.Send Authorization: Bearer cvt_live_<key>.
RATE_LIMIT_TENANT429Tenant quota exceeded.Honour the Retry-After header.

Full envelope shapes are in the error reference.

Next steps