Send a LinkedIn direct message (DM) to a contact, given their public slug (the 'felipegoulu' of linkedin.com/in/felipegoulu) and the message text.
{"slug":"recipient's public identifier (the segment of linkedin.com/in/<slug>, e.g. 'felipegoulu')","text":"text of the message to send (string)"}{"sent":"bool","status":"int (HTTP of createMessage, 200 = sent)","messageUrn":"string (entityUrn of the created message)","recipientUrn":"string (profileUrn resolved from the slug)"}Send a LinkedIn direct message (DM) to a contact, given their public slug (the 'felipegoulu' of linkedin.com/in/felipegoulu) and the message text. WRITE-IRREVERSIBLE: it actually sends the message via the Voyager messaging API; every run sends. Robust verified path (trace-036, Jun 2026) that avoids the whole messaging overlay UI: in a logged-in tab it does 3 in-page fetches with the session cookies (credentials:'include'): (1) resolves the recipient's slug->profileUrn via GET /voyager/api/identity/dash/profiles?q=memberIdentity&memberIdentity=<slug> taking the NEW SHAPE json.elements[0].entityUrn (urn:li:fsd_profile:...); with a fallback to the old shape data['*elements'][0]. (2) resolves your own profileUrn (mailboxUrn) via GET /voyager/api/me taking the NEW SHAPE json.miniProfile.dashEntityUrn (or normalizing json.miniProfile.entityUrn fs_miniProfile->fsd_profile); with a fallback to the old shape data['*miniProfile']. (3) POST /voyager/api/voyagerMessagingDashMessengerMessages?action=createMessage with body {message:{body:{attributes:[],text},renderContentUnions:[],originToken:<client uuid v4>},mailboxUrn,trackingId:<16 random bytes as a latin1 string>,dedupeByClientGeneratedToken:false,hostRecipientUrns:[recipientUrn]}. KEY CHANGE Jun 2026: the hostRecipientUrns path WITHOUT trackingId now returns HTTP 400; adding trackingId (16 random bytes) fixes the generic-by-slug path. Voyager headers: csrf-token = JSESSIONID cookie (ajax:... format), x-restli-protocol-version 2.0.0, accept application/json, content-type text/plain;charset=UTF-8 on the POST. Success = status 200/201 on createMessage with value.entityUrn (msg_message urn). The session/cookies are provided by the environment, NOT the tool. Params: slug (recipient's public identifier) and text (content). Requires a logged-in LinkedIn session.
params. It does the in-page work and returns the JSON in returns.( async (root, params) => { const slug = String(params.slug || '').trim(); const text = String(params.text || ''); if (!slug) throw new Error('missing slug'); if (!text) throw new Error('missing text'); const csrf = (document.cookie.match(/JSESSIONID="?([^";]+)"?/) || [])[1]; if (!csrf) throw new Error('re-auth: no JSESSIONID cookie (LinkedIn session not started)'); const H = { 'csrf-token': csrf, 'x-restli-protocol-version': '2.0.0', 'accept': 'application/json' }; const norm = (u) => String(u || '').replace('urn:li:fs_miniProfile:', 'urn:li:fsd_profile:'); // 1) slug -> recipient's profileUrn (NEW SHAPE: json.elements[0].entityUrn; old fallback data['*elements'][0]) const profRes = await fetch('https://www.linkedin.com/voyager/api/identity/dash/profiles?q=memberIdentity&memberIdentity=' + encodeURIComponent(slug), { credentials: 'include', headers: H }); if (profRes.status === 401 || profRes.status === 403) throw new Error('re-auth: HTTP ' + profRes.status + ' resolving the slug'); if (!profRes.ok) throw new Error('tool-broken: profiles HTTP ' + profRes.status); const profJson = await profRes.json(); let recipientUrn = ''; const elsNew = (profJson && profJson.elements) || []; if (elsNew.length && elsNew[0] && elsNew[0].entityUrn) { recipientUrn = norm(elsNew[0].entityUrn); } else { const elsOld = (profJson && profJson.data && profJson.data['*elements']) || []; recipientUrn = norm(elsOld[0]); } if (!recipientUrn || recipientUrn.indexOf('urn:li:fsd_profile:') !== 0) throw new Error('not-applicable: could not resolve the profileUrn of slug ' + slug); // 2) your own profileUrn (mailboxUrn) (NEW SHAPE: json.miniProfile.dashEntityUrn|entityUrn; old fallback data['*miniProfile']) const meRes = await fetch('https://www.linkedin.com/voyager/api/me', { credentials: 'include', headers: H }); if (meRes.status === 401 || meRes.status === 403) throw new Error('re-auth: HTTP ' + meRes.status + ' at /me'); if (!meRes.ok) throw new Error('tool-broken: me HTTP ' + meRes.status); const meJson = await meRes.json(); let mailboxUrn = ''; const mp = (meJson && meJson.miniProfile) || null; if (mp && (mp.dashEntityUrn || mp.entityUrn)) { mailboxUrn = norm(mp.dashEntityUrn || mp.entityUrn); } else { mailboxUrn = norm((meJson && meJson.data && meJson.data['*miniProfile']) || ''); } if (mailboxUrn.indexOf('urn:li:fsd_profile:') !== 0) throw new Error('tool-broken: could not resolve own mailboxUrn'); // originToken: client uuid v4 const uuid = (crypto && crypto.randomUUID) ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); // trackingId: 16 random bytes as a latin1 string (CHANGE Jun 2026: without this the hostRecipientUrns path returns HTTP 400) let trackingId = ''; for (let i = 0; i < 16; i++) trackingId += String.fromCharCode(Math.floor(Math.random() * 256)); // 3) POST createMessage (hostRecipientUrns + trackingId = generic path by slug) const buildPayload = (extra) => Object.assign({ message: Object.assign({ body: { attributes: [], text: text }, renderContentUnions: [], originToken: uuid }, (extra && extra.message) || {}), mailboxUrn: mailboxUrn, trackingId: trackingId, dedupeByClientGeneratedToken: false, hostRecipientUrns: [recipientUrn] }, (extra && extra.top) || {}); const doPost = (payload) => fetch('https://www.linkedin.com/voyager/api/voyagerMessagingDashMessengerMessages?action=createMessage', { method: 'POST', credentials: 'include', headers: Object.assign({}, H, { 'content-type': 'text/plain;charset=UTF-8' }), body: JSON.stringify(payload) }); let sendRes = await doPost(buildPayload()); if (sendRes.status === 401 || sendRes.status === 403) throw new Error('re-auth: HTTP ' + sendRes.status + ' at createMessage'); if (sendRes.status !== 200 && sendRes.status !== 201) throw new Error('tool-broken: createMessage returned HTTP ' + sendRes.status + ' (expected success = 200; the hostRecipientUrns+trackingId path failed)'); let sendJson = null; try { sendJson = await sendRes.json(); } catch (e) {} const messageUrn = (sendJson && sendJson.value && sendJson.value.entityUrn) || null; return { sent: true, status: sendRes.status, messageUrn: messageUrn, recipientUrn: recipientUrn }; } )
{
"type": "json",
"jsonPath": "sent"
}