1. Register an endpoint
POST /api/webhooks takes a name, your target_url and the events[] you want:
POST /orchestration/v4/api/webhooks
nameis required (1 to 255 characters). Omitting it fails validation.target_urlmust behttps://and must not resolve to a loopback, private or link-local address. DNS names are resolved and checked too, not just IP literals.- Each
events[]value is validated against the platform event catalog, which currently carries 108 types across the services. - The same
target_urlcannot be registered twice in one team. events: ["*"]subscribes to the entire catalog, including event types added later.
secret, a 32-character hex string, exactly once. Store it: every later read masks it, and it is what you verify signatures with.
Webhooks are capped per plan. Creating one past the cap answers
402 with webhook_limit_reached and a body carrying used, limit and a suggested action. The free Sandbox plan includes one webhook, so subscribe that endpoint to events: ["*"] and route on your side. Paid plans carry 100. A trigger in n8n, Make or Zapier registers a webhook of its own, so on Sandbox one trigger runs at a time. Sandbox also has no realtime push: a received message reaches the webhook when a sync reads it, not within seconds (inbox and message sync). See billing and plans.Filter what you receive
Two optional filters narrow deliveries at the source, so your endpoint is not woken by events it would discard anyway:filters.account_sidrestricts deliveries to one account (ln_ac_...for LinkedIn). The sid is matched verbatim, so channels added later work here without a change.filters.wheretakes a small filter expression over the event payload. It is validated strictly at write time: nesting depth is capped at 5 and the whole expression at 20 leaf conditions, answeringfilter_grammar_invalidorfilter_leaves_limit_exceededwhen it exceeds either.
2. Verify deliveries
Every real delivery is an HTTPSPOST with a JSON body and these headers:
The body is a fixed envelope, with the event’s own data nested under
payload:
HMAC-SHA256(secret, "{t}.{raw_body}") over the raw request body, with the timestamp mixed in to block replays. Verify before parsing, and reject signatures older than about 5 minutes:
3. Test before relying on it
POST /api/webhooks/{sid}/test fires a synthetic delivery at your endpoint, so you can confirm reachability, signature handling and parsing without waiting for a real event.
Three things to know about it: it is rate-limited to 10 calls per minute per caller (the budget is shared across all your webhooks), it is rejected with invalid_transition while the webhook’s status is off, and it writes no row to the delivery log. It also sets a subset of the headers above, so build your verifier on the signature, timestamp, event and id headers rather than requiring all six.
4. Retries and auto-disable
A delivery is attempted up to 5 times, with waits of 1 minute, 5 minutes, 30 minutes and 2 hours between attempts. The fifth failure is terminal, so a dead endpoint settles about 2.5 hours after the first attempt.
After 20 consecutive failed attempts, or a single
410 Gone, the subscription flips to failed and stops delivering. Its deliveries still waiting for a retry are cancelled with cancel_reason: "webhook_failed", and the failure counter stops moving from that point. That transition itself emits webhooks.failed, so a second webhook can page you when the first one dies; the failing endpoint also gets that one event, a single attempt with no retries.
Bringing it back is three calls: fix the endpoint, PATCH /api/webhooks/{sid} with {"status": "on"} (this resets the counter), then replay what was lost:
from whose status is failed or cancelled (by the platform, never by your own cancel) back into the queue, oldest first, one per second, so your endpoint is not hit by the whole backlog at once; the response says how many rows it re-armed and when the last one is due. to defaults to now, statuses to ["failed", "cancelled"] (pass ["success"] to re-fire delivered events into a rebuilt downstream), event_types to every type. The window spans at most 30 days and one call re-arms at most 5000 rows; beyond either cap the call is refused with the cap named, so narrow the window and call again. Replayed rows keep their webhook_log_sid, so a receiver that deduplicates on it sees the same delivery, not a new one.
5. Use the delivery log
The log holds one row per event per subscription, not one per attempt: a retry updates that row in place, bumpingretry_count and overwriting the response code. The row is the delivery, and its history is the counter.
POST /api/webhook-logs/searchlists deliveries with status, response code and timing; filter by webhook or event type.POST /api/webhook-logs/{sid}/retryre-sends. Allowed from every state butin_progress:pending,retrying,failed,success(a manual re-send of a delivered event is legitimate) andcancelled, whatever cancelled it. It re-arms the row without counting as an attempt, soretry_countstays the number of attempts that actually went out (at most 5). Only while the parent subscription is still live: a deleted oroffwebhook rejects the retry.POST /api/webhook-logs/{sid}/cancelstops a delivery that has not settled. Allowed frompending,retryingandin_progress.POST /api/webhook-logs/metricsaggregates outcomes.periodwithfromandtois required and may span at most 90 days.
retry and cancel answer 409 with invalid_transition when the row is not in a state that allows the verb, and the error names the state it found.
6. Read the payload
payload is the event’s own row, under the field names the REST API uses for that entity, plus a few fields that exist only on the wire:
client_referenceonlinkedin-connection-requests.sent,.accepted,.withdrawn,.expired-detectedand onlinkedin-messages.sentechoes theclient_referenceyou passed to the send. Match on it instead of on a LinkedIn identifier. It isnullon rows the sync picked up from LinkedIn’s own UI. Every send verb accepts it:send_linkedin_connection_requestand all six message sends (the basic message, voice, InMail, Sales Navigator, Recruiter and the group opener), and the comment, post and repost verbs, which emit no event of their own. A mass-action plan step’sargs.client_referenceT reaches every row the run creates asT:{item sid}:{step id}, so match theT:prefix.counterpartonlinkedin-conversations.created,linkedin-messages.receivedandlinkedin-messages.sentis the other side of the thread:{ "full_name", "headline", "picture_url" }, ornullwhen we hold no participant data for the thread yet.picture_urlis LinkedIn’s signed CDN link and expires within hours, so render it on arrival rather than storing it.created_by, where the row has one, is a reference to the actor:{ "actor_type", "actor_sid", "actor_name", "oauth_client_sid", "reason" }.actor_nameandoauth_client_sidname the OAuth client that acted (an MCP client such as Claude, or n8n) and arenullfor a user, an API key or a system job.reasonis set on system jobs, for examplesnapshot_capture_job.nickname, the profile’s vanity URL slug, is present only where LinkedIn exposes it: the connections list and received invitations. Followers, conversations, messages and sent invitations name people by hash id, so those rows carrynicknameonly once the same person is also a 1st-degree connection of the account. Match onln_member_id(stable) orln_id(theACoAA...profile id, present on every event) rather than on the slug.linkedin-account-activity-log.task-failedfires once for each call an account dispatched that endedfailed. Its payload is the activity-log row (sid,linkedin_account_sid,action_type,error_message,duration_ms,created_at,updated_at,trace_id) withpending_age_seconds, how long the call was pending, andfailure_source, what failed it:plugin(the call’s own answer),reconcile(nobody finished the call in time) orbrowser_deleted(its browser was deleted while it ran). On a send’s row,send_outcomeandsend_outcome_reasonsay what came of the send, and they arenullon every other action.not_sent(refused) means nothing reached LinkedIn.unknown(answer_lost) means the send may be out: a failed call is not a send that did not go out, so ask check-sent before you send it again (Sending once). To receive only the sends in doubt, subscribe with"filters": {"where": {"field": "payload.send_outcome", "op": {"eq": "unknown"}}}. One kind of failed call does not fire it: a Sales Navigator call that LinkedIn refused withSALES_SEAT_REQUIREDwhile the seat is live. The platform reopens the Sales Navigator session and runs the call once more, and that second call’s own outcome is what you get (a sync run retries on its next pass instead). The refused first attempt stays in the activity log. When the seat is really gone, the failure fires as usual.deleted_aton a terminallinkedin-connection-requests.*event (accepted,withdrawn,expired-detected) is the moment we detected the transition, not LinkedIn’s own timestamp of it. Acceptance is detected by the next sync of the account’s pending invitations, so it can trail the real acceptance by up to the sync interval.removal_kindsays which transition it was.linkedin-accounts.restriction-liftedand its browser-level twinantidetect-browsers.restriction-liftedfire once when a restricted account comes back: a scheduled re-read (day 3, 7, 14, 21 and 30 after the sign-out, then every 30 days to day 180) found the member’s public profile again, and the browser went fromrestrictedback tologin_issue. The account still needs a re-login, so read the event as “ask the holder to log in”, not as “the sender works again”. Both payloads carryrestricted_for_days(whole days from the sign-out to the read),rechecks_count(how many reads this restriction took, the one that found the profile included) andobserved_at, when that read ran. The account event names the account insidand its browser inantidetect_browser_sid. The browser event names the browser insidand its account inlinkedin_account_sid, and addslogout_countandlast_logout_at, still those of the sign-out the restriction began with, anderror_reason, the read’s result in words for a person (the wording may change, so do not parse it). What happens around the lift is on browser troubleshooting.
restriction-lifted payloads, as they arrive under payload: