Webhooks

Messaging events

Eight events that fire whenever a message or chat changes state on a connected LinkedIn account, with full message content delivered in every message payload.

Before you start

Creating a messaging webhook needs an API key and at least one connected LinkedIn account: account_ids is required and must be non-empty. Read your ids with GET /v1/accounts, and if that returns an empty list, connect an account first (see Authentication and accounts).

Early access: content events

The message and chat events are newly wired on the current platform substrate. The delivery envelope is stable and safe to build against. It is the top-level event plus the data object, which always carries account_id and occurred_at. Individual data field shapes may still be refined as live validation completes, so pin your handler to the envelope and treat per-field additions as backward-compatible.

Quick reference

EventDescription
message.receivedA new inbound message arrived in the account's inbox.
message.deliveredAn outbound message was confirmed delivered to the recipient.
message.readA message in the conversation was read.
message.reactionA reaction was added to a message.
message.editedA message was edited; the payload carries the updated text.
message.deletedA message was deleted from the conversation.
chat.updatedA chat's container state changed (e.g. archived, muted, read-state). Opt-in.
chat.deletedA chat thread was deleted. Opt-in.

The six message.* events share the same data shape (with per-event additions noted below); the two chat.* events carry a minimal chat-container shape (documented at the end). All are Core-tier. Subscribe to any subset when creating your webhook, the chat.* events are opt-in and must be named explicitly:

curl -X POST https://api.curviate.com/v1/webhooks \
  -H "Authorization: Bearer cvt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "messaging",
    "request_url": "https://hooks.example.com/curviate",
    "account_ids": ["acc_YOUR_ACCOUNT_ID"],
    "events": [
      "message.received",
      "message.delivered",
      "message.read",
      "message.reaction",
      "message.edited",
      "message.deleted"
    ]
  }'

message.received

Fired when a new inbound message arrives in the connected account's inbox, classic LinkedIn messages or InMail.

Inbound only

message.received fires only for messages the account received, not for messages it sent. A message the account sent does not trigger this event. For outbound delivery confirmation use message.delivered.

{
  "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, I saw your profile and wanted to connect.",
    "sender": {
      "name":        "Alex Jordan",
      "profile_url": "https://www.linkedin.com/in/alexjordan",
      "provider_id": "urn:li:member:123456789"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T14:22:05.000Z"
  },
  "delivered_at": "2026-05-28T14:22:06.789Z"
}

Notable fields

  • text: the full message body.
  • sender: the LinkedIn member who sent the message: name, profile_url, and provider_id (the LinkedIn member URN).
  • attachments: array of attachment objects; empty when there are none. Each item carries id, type, url, mimetype, and size.
  • chat_id: identifies the conversation; consistent across all events in the same thread.

message.delivered

Fired when an outbound message sent from the connected account was confirmed delivered by the platform. Use this event to close the send loop in fire-and-forget agent patterns.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "message.delivered",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.delivered",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "text":        "Thanks for reaching out, happy to connect.",
    "sender": {
      "name":        "Jordan Lee",
      "profile_url": "https://www.linkedin.com/in/jordanlee",
      "provider_id": "urn:li:member:987654321"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T14:23:10.000Z"
  },
  "delivered_at": "2026-05-28T14:23:11.456Z"
}

message.read

Fired when a message in the conversation is read. The payload carries the standard messaging data shape, plus a read_by field when the platform discloses the reader. read_by is conditional: absent, not null, when the reader is not disclosed.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "message.read",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.read",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "text":        "Hello, I saw your profile and wanted to connect.",
    "sender": {
      "name":        "Alex Jordan",
      "profile_url": "https://www.linkedin.com/in/alexjordan",
      "provider_id": "urn:li:member:123456789"
    },
    "read_by":     "urn:li:member:987654321",
    "attachments": [],
    "occurred_at": "2026-05-28T14:25:00.000Z"
  },
  "delivered_at": "2026-05-28T14:25:01.012Z"
}

message.reaction

Fired when a reaction is added to a message in the conversation.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "message.reaction",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.reaction",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "text":        "Thanks for reaching out, happy to connect.",
    "reaction":    "👍",
    "sender": {
      "name":        "Alex Jordan",
      "profile_url": "https://www.linkedin.com/in/alexjordan",
      "provider_id": "urn:li:member:123456789"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T14:26:30.000Z"
  },
  "delivered_at": "2026-05-28T14:26:31.234Z"
}

Notable fields

  • reaction: the reaction value added to the message (e.g. an emoji string).

message.edited

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

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "message.edited",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.edited",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "text":        "Thanks for reaching out, let's schedule a call.",
    "sender": {
      "name":        "Jordan Lee",
      "profile_url": "https://www.linkedin.com/in/jordanlee",
      "provider_id": "urn:li:member:987654321"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T14:27:45.000Z"
  },
  "delivered_at": "2026-05-28T14:27:46.789Z"
}

Notable fields

  • text: the post-edit content. The original message text is not included.

message.deleted

Fired when a message is deleted from the conversation. The text field may be absent when the platform does not include the deleted content.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "message.deleted",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "message.deleted",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "message_id":  "msg_YOUR_MESSAGE_ID",
    "sender": {
      "name":        "Jordan Lee",
      "profile_url": "https://www.linkedin.com/in/jordanlee",
      "provider_id": "urn:li:member:987654321"
    },
    "attachments": [],
    "occurred_at": "2026-05-28T14:28:00.000Z"
  },
  "delivered_at": "2026-05-28T14:28:01.345Z"
}

Notable fields

  • text: may be absent on deletion; do not depend on it being present when handling this event.

chat.updated

Fired when a chat's container state changed, for example the thread was archived, muted, or its read-state changed. Useful for inbox automation that mirrors thread state. This event is about a chat thread, not a single message.

Opt-in event

chat.updated is not in the messaging default set. Name it explicitly in the events array when creating or updating the webhook to receive it.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "chat.updated",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "chat.updated",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "occurred_at": "2026-05-28T14:30:00.000Z"
  },
  "delivered_at": "2026-05-28T14:30:01.000Z"
}

Notable fields

  • account_id and chat_id are always present, chat_id identifies the thread whose container state changed. The delivery carries these identifying fields; use them to look up the current thread state on your side.

chat.deleted

Fired when a chat thread was deleted, actionable for inbox agents that mirror thread state.

Opt-in event

chat.deleted is not in the messaging default set. Name it explicitly in the events array to receive it.

{
  "id":          "wdl_YOUR_DELIVERY_ID",
  "webhook_id":  "wh_YOUR_WEBHOOK_ID",
  "event":       "chat.deleted",
  "data": {
    "account_id":  "acc_YOUR_ACCOUNT_ID",
    "event":       "chat.deleted",
    "chat_id":     "chat_YOUR_CHAT_ID",
    "occurred_at": "2026-05-28T14:31:00.000Z"
  },
  "delivered_at": "2026-05-28T14:31:01.000Z"
}

Notable fields

  • account_id and chat_id are always present, chat_id identifies the deleted thread.

Field remapping

Not active yet

The data array is accepted, stored, and echoed back when you read the webhook, but it does not currently change what is delivered. The delivered data object is the fixed shape documented above for every event. Set it if you want it recorded for later; do not build a handler that depends on it, and do not spend time tuning it when a field is missing.

The data array is not validated against a key list on this source, so a typo is accepted at create time and produces no error anywhere. (The user source does validate, which is why the two behave differently.) The 27 keys intended for the messaging source:

account_id        account_type      account_info
chat_id           timestamp         webhook_name
message_id        message           mentions
reaction          reaction_sender   read_by
sender            is_sender         attendees
attachments       subject           provider_chat_id
provider_message_id  is_event       chat_pinned
quoted            is_forwarded      chat_content_type
message_type      is_group          folder

When data is omitted, and today whatever you set, the default payload shape documented above is delivered.

is_sender appears in the list above but is never delivered: it is an internal routing signal, so data.is_sender is always undefined. To tell sent from received, branch on the event name: message.received for inbound, message.delivered for messages the account sent.

Errors you may hit

CodeHTTPCauseFix
INVALID_REQUEST400An event in events[] does not belong to the messaging source, request_url is not HTTPS, or a field is missing.Read the message, it names the field.
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>.
PAYMENT_REQUIRED402No active subscription.Add a seat in the dashboard.
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