Webhooks Overview
Receive workflow, account, and LinkedIn activity events in real time. Instead of polling for a workflow result, register a webhook once and Linked API delivers an event to your endpoint whenever a workflow changes state, a LinkedIn account changes status, a message is observed in a monitored inbox, or a connection changes in a monitored network. Once signals launch, the webhook will also receive an event whenever a signal detects something new.
In the Node SDK, verifyWebhookSignature() checks a delivery and parseWebhookEvent() turns it into a typed event – see Receiving events. The webhook itself is managed with admin.webhooks – see Managing webhooks.
In the Python SDK, verify_webhook_signature() checks a delivery and parse_webhook_event() turns it into a typed event – see Receiving events. The webhook itself is managed with admin.webhooks – see Managing webhooks.
In the CLI you register and manage the webhook with linkedin admin webhook commands – see Managing webhooks. Receiving deliveries is the job of your own endpoint.
How it works
- You register a single endpoint URL that receives events. A client may hold one active webhook at a time.
- Every event your webhook receives is delivered as an HTTP
POSTwith a JSON body (the event envelope below). By default that is every event; you can choose which events it receives. - Respond with any
2xxstatus to acknowledge. A non-2xxresponse, or a timeout, is retried with exponential backoff for up to 8 attempts before the delivery is marked failed. - Two optional settings secure the delivery: custom headers, so an endpoint that requires authentication lets us in, and a signature, so you can prove a delivery came from Linked API. Both are off by default and neither changes anything until you turn it on.
Set this up either way: in the platform dashboard, or programmatically through the Admin API. Both reach the same webhook, so you can register it in the dashboard and change it from code, or the other way round.
In the SDKs, the Admin API's webhook endpoints are the methods of admin.webhooks on the admin client. Each call is shown on Managing Webhooks.
In the CLI, they are the linkedin admin webhook commands. Each command is shown on Managing Webhooks.
Event envelope
Every delivery has the same shape:
{
"id": "workflow.completed:wf-64835e7c-...",
"type": "workflow.completed",
"createdAt": "2026-06-25T12:00:00.000Z",
"data": {
"workflowId": "wf-64835e7c-...",
"accountId": "f9b4346a-...",
"status": "completed",
"result": { }
}
}id– stable, unique event identifier. Use it to deduplicate: a retried delivery reuses the sameid.type– event type, one of the event types.createdAt– ISO 8601 timestamp of when the event was produced.data– event-specific payload, described per event type on Event Types.
Delivery order is best-effort and not guaranteed. Correlate events by
type,data.status, andcreatedAtrather than by arrival order.
Payload modes
The payload mode controls how much data workflow.completed carries:
fat(default) – the full workflow result is inlined indata.result.thin–data.resultis omitted; fetch the result via the workflow API usingdata.workflowId.
You can switch the mode at any time when managing the webhook.
Receiving events
Your endpoint should:
- If signing is on, verify the signature against the raw body, before parsing it.
- Read the JSON body and branch on
type. - Respond
2xxquickly to acknowledge. Do heavy work asynchronously – a slow endpoint will time out and be retried. - Deduplicate on the envelope
id. Deliveries are at-least-once, so the same event can arrive more than once.
The Node SDK ships helpers for the first two steps: verifyWebhookSignature checks the signature, and parseWebhookEvent turns the body into a typed event, so branching on type narrows data. Both take the raw body, so read it unparsed – in Express, with express.raw():
The Python SDK ships helpers for the first two steps: verify_webhook_signature checks the signature, and parse_webhook_event turns the body into a typed event – branch on its class with isinstance to reach typed data. Both take the raw body, so read it unparsed – in FastAPI, with await request.body():
Without an SDK the endpoint is a few lines in any language. Here it is in Node.js with Express, reading the body unparsed with express.raw() and checking it with the reference verifyWebhookSignature from Signature Verification:
The CLI configures webhooks but does not receive them: your endpoint is a small server in any language. Here it is in Node.js with Express, reading the body unparsed with express.raw() and checking it with the reference verifyWebhookSignature from Signature Verification:
const express = require("express")
// The reference implementation from Signature Verification, saved next to this file as verify-webhook-signature.js.
const { verifyWebhookSignature } = require("./verify-webhook-signature")
const app = express()
app.use(express.raw({ type: "application/json" }))
app.post("/hooks/linkedapi", (req, res) => {
// Skip this check if signing is off for your webhook.
const isAuthentic = verifyWebhookSignature({
rawBody: req.body,
timestamp: req.headers["linked-api-signature-timestamp"],
signature: req.headers["linked-api-signature"],
secret: process.env.LINKED_API_WEBHOOK_SECRET,
nowSeconds: Math.floor(Date.now() / 1000),
})
if (!isAuthentic) {
res.sendStatus(401)
return
}
const event = JSON.parse(req.body.toString("utf8"))
switch (event.type) {
case "workflow.completed":
console.log(`Workflow ${event.data.workflowId} finished: ${event.data.status}`)
console.log("Result:", event.data.result) // present in fat mode only
break
case "account.reconnectionRequired":
console.log(`Account ${event.data.accountId} needs reconnection.`)
break
case "inbox.messageReceived":
// personHashedUrl is always present; personPublicUrl is null until the person is resolved.
console.log(`New message from ${event.data.personPublicUrl ?? event.data.personHashedUrl}: ${event.data.text}`)
break
case "inbox.messageSent":
console.log(`Outgoing message in thread ${event.data.threadId}`)
break
case "network.connectionAccepted":
console.log(`${event.data.personUrl} accepted your connection request`)
break
case "network.connectionRequestReceived":
console.log(`New connection request from ${event.data.personUrl}`)
break
case "webhook.test":
console.log("Test event:", event.data.message)
break
}
// Acknowledge fast. A non-2xx response makes Linked API retry with backoff.
res.sendStatus(200)
})parseWebhookEvent throws an Error when the body is not valid JSON or lacks the envelope id or type. It checks only that type is a string, so an event type added after your SDK version reaches your code as it is. It returns TWebhookEvent, a union of one type per area – see Event Types.
parse_webhook_event raises ValueError when the body is not valid JSON or lacks the envelope id or type. Since linkedapi 1.4.7, an event type the installed version doesn't know yet no longer raises: it is returned as UnknownWebhookEvent, with its payload as a plain dict in data. It returns WebhookEvent, a union of one model per area plus UnknownWebhookEvent, with snake_case data fields – see Event Types. Every event's type is an open string, so branch with isinstance on the event classes, as above, rather than on type alone.