{"_id":"lighthouse-private-markets-sdk","_rev":"62-f3926f0fbd879e4ba902893987ceeaff","name":"lighthouse-private-markets-sdk","dist-tags":{"latest":"4.2.0"},"versions":{"3.7.2":{"name":"lighthouse-private-markets-sdk","version":"3.7.2","keywords":["lighthouse","private-markets","sdk","api","client","private-equity","venture-capital","crm"],"author":{"name":"Alberto Marzetta"},"license":"MIT","_id":"lighthouse-private-markets-sdk@3.7.2","maintainers":[{"name":"lghsdk","email":"alberto@trylighthouse.vc"}],"homepage":"https://trylighthouse.vc","dist":{"shasum":"ce3282f2a5a06974b32c3b8265ec658238ae3b07","tarball":"https://registry.npmjs.org/lighthouse-private-markets-sdk/-/lighthouse-private-markets-sdk-3.7.2.tgz","fileCount":6,"integrity":"sha512-vJpTOyuF2b+x1iyBkPAJV1toayAMvqjl7i3/Of2WmOke9hwVDOaOSP4O9CnhMLy/QH5nrxujBrxDk+3XVxjCkQ==","signatures":[{"sig":"MEYCIQCmXF4qVt6FA9QZWZWVFXf4plzjdD6m5LgQkmnSNeDRVQIhALJ5g7sTIpYqUOCX+PzHXWa/vp50YZu7VFRZPIILSSOK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIDh12szJbNLLBYWzrsU0OMJNKyp5CykPt+t2YlTfRqNRAiEAurm+3z1LEP8DnIRspxdkMkseC6j0dviy0VlZtKksEFU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":109952},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","gitHead":"b9f2a61c29d3a1862a2c654d1f7fd5c61dfeec63","scripts":{"build":"tsup src/index.ts --format cjs,esm --dts --clean && node scripts/no-comments.mjs --dist","prepare":"npm run build","prebuild":"node scripts/no-comments.mjs"},"_npmUser":{"name":"lghsdk","email":"alberto@trylighthouse.vc"},"overrides":{"esbuild":"^0.28.2"},"_npmVersion":"10.9.4","description":"Official Lighthouse SDK, a thin REST client for the Lighthouse API (CRM, Discovery, Reports, Documents).","directories":{},"_nodeVersion":"22.22.1","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/lighthouse-private-markets-sdk_3.7.2_1789714546233_0.4601064736971301","host":"s3://npm-registry-packages-npm-production"}},"4.1.1":{"name":"lighthouse-private-markets-sdk","version":"4.1.1","keywords":["lighthouse","private-markets","sdk","api","client","private-equity","venture-capital","crm"],"author":{"name":"Alberto Marzetta"},"license":"MIT","_id":"lighthouse-private-markets-sdk@4.1.1","maintainers":[{"name":"lghsdk","email":"alberto@trylighthouse.vc"}],"homepage":"https://trylighthouse.vc","dist":{"shasum":"a9c8efd888d60d141aa9855b72db0f3a8e011033","tarball":"https://registry.npmjs.org/lighthouse-private-markets-sdk/-/lighthouse-private-markets-sdk-4.1.1.tgz","fileCount":6,"integrity":"sha512-2bXzxLBuU6q/K1FijwZvdOxLN9VISf8fXFsp754vNVp8oqS13DSs2VHElRP4cmVyQPWeS580Aoj9wwaTW8TO9w==","signatures":[{"sig":"MEUCIDZWzFHMy4YuwSyuwS/D8Zkla0EoZDlxFL5rX3358q6QAiEA0DenD6JbsOC1ufC7ntlu9BiqI8oo0jscBHWO+UyUINE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCYkhFLQI7DuUlrcnObc9gxshk3A8Adm6SKp/DsXakAUwIgerdMXGTpv+JioxjOGstPDXg2hsyjJ32JXIQi/dUvFSA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":150051},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","gitHead":"a87d81e91576e0149167e6b32597d3fb669e8b5a","scripts":{"build":"tsup src/index.ts --format cjs,esm --dts --clean && node scripts/no-comments.mjs --dist","prepare":"npm run build","prebuild":"node scripts/no-comments.mjs"},"_npmUser":{"name":"lghsdk","email":"alberto@trylighthouse.vc"},"overrides":{"esbuild":"^0.28.2"},"_npmVersion":"10.9.4","description":"Official Lighthouse SDK, a thin REST client for the Lighthouse API (CRM, Discovery, Reports, Documents).","directories":{},"_nodeVersion":"22.22.1","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/lighthouse-private-markets-sdk_4.1.1_1789909205948_0.32940666757578385","host":"s3://npm-registry-packages-npm-production"}},"4.2.0":{"_id":"lighthouse-private-markets-sdk@4.2.0","dist":{"shasum":"004990f13d1d77e674831f9d8a0f3900f5f32e4c","tarball":"https://registry.npmjs.org/lighthouse-private-markets-sdk/-/lighthouse-private-markets-sdk-4.2.0.tgz","fileCount":6,"integrity":"sha512-RYfuz9JDH0gFhs9fvEf49XfFHSl/f9n1QuMyQbpXocX376yA0VJFeDea4mkd8COs1i3JCz8jnhn35xrPSe4clQ==","signatures":[{"sig":"MEUCIQCeSFcOcKwJGLGcilh6ilje0YPCeBt1YpwKOThihskoNgIgAWoBIqQfCHJk00VJxfhe4nsBoHp0w9L7K2T+TXfEubU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDU9VkosJTkTQa34vZWaQ1jANBBIZ5RR47FOFLB4/3JWwIhAPVNIwn5mJ9QCjm/H7KNrFnoi4xpU3emCFcvqzft2wcE"}],"unpackedSize":169688},"main":"dist/index.js","name":"lighthouse-private-markets-sdk","types":"dist/index.d.ts","author":{"name":"Alberto Marzetta"},"module":"dist/index.mjs","gitHead":"d3c38e4fd1b050de3a651b43c6431def1d2c1a2e","license":"MIT","scripts":{"test":"npm run build && node --test test/*.test.mjs","build":"tsup src/index.ts --format cjs,esm --dts --clean && node scripts/no-comments.mjs --dist","prepare":"npm run build","prebuild":"node scripts/no-comments.mjs"},"version":"4.2.0","_npmUser":{"name":"lghsdk","email":"alberto@trylighthouse.vc"},"homepage":"https://trylighthouse.vc","keywords":["lighthouse","private-markets","sdk","api","client","private-equity","venture-capital","crm"],"overrides":{"esbuild":"^0.28.2"},"_npmVersion":"10.9.4","description":"Official Lighthouse SDK, a thin REST client for the Lighthouse API (CRM, Discovery, Reports, Documents).","directories":{},"maintainers":[{"name":"lghsdk","email":"alberto@trylighthouse.vc"}],"_nodeVersion":"22.22.1","dependencies":{},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lighthouse-private-markets-sdk_4.2.0_1791021901757_0.9813526088846911"}}},"time":{"created":"2025-02-12T23:23:15.948Z","modified":"2026-10-03T10:05:02.003Z","1.0.0":"2025-02-06T21:30:59.303Z","1.0.1":"2025-02-06T21:36:23.529Z","1.0.2":"2025-02-06T21:39:10.589Z","1.0.3":"2025-02-12T23:23:16.373Z","1.0.4":"2025-02-12T23:30:35.274Z","1.0.5":"2025-02-12T23:38:22.986Z","1.0.6":"2025-02-12T23:40:22.265Z","1.0.7":"2025-02-12T23:47:08.419Z","1.0.8":"2025-02-12T23:54:34.331Z","1.0.9":"2025-02-13T01:03:35.976Z","1.1.0":"2025-02-14T21:13:14.004Z","1.1.1":"2025-02-14T23:45:55.003Z","1.1.2":"2025-02-15T14:10:34.448Z","1.1.3":"2025-02-15T14:24:16.998Z","1.1.4":"2025-02-15T16:07:21.879Z","1.1.5":"2025-06-29T20:29:49.196Z","2.0.0":"2026-05-19T13:31:40.632Z","2.0.1":"2026-05-24T16:08:26.985Z","2.0.2":"2026-05-29T10:34:02.611Z","2.0.3":"2026-06-01T08:35:21.724Z","3.0.0":"2026-06-01T09:31:54.611Z","3.0.1":"2026-06-01T10:01:32.822Z","3.0.2":"2026-06-01T10:34:18.864Z","3.1.0":"2026-06-24T13:16:44.486Z","3.2.0":"2026-07-01T20:52:17.133Z","3.2.2":"2026-07-30T12:40:48.006Z","3.3.0":"2026-08-03T16:26:31.654Z","3.5.0":"2026-08-05T16:12:16.544Z","3.6.0":"2026-08-29T17:46:04.413Z","3.7.1":"2026-09-18T06:19:12.305Z","3.7.2":"2026-09-18T06:55:46.319Z","4.1.1":"2026-09-20T13:00:06.030Z","4.2.0":"2026-10-03T10:05:01.851Z"},"author":{"name":"Alberto Marzetta"},"license":"MIT","homepage":"https://trylighthouse.vc","keywords":["lighthouse","private-markets","sdk","api","client","private-equity","venture-capital","crm"],"description":"Official Lighthouse SDK, a thin REST client for the Lighthouse API (CRM, Discovery, Reports, Documents).","maintainers":[{"name":"lghsdk","email":"alberto@trylighthouse.vc"}],"readme":"# Lighthouse JS SDK\n\nOfficial JavaScript/TypeScript client for the [Lighthouse](https://trylighthouse.vc) REST API. Wraps the CRM, Discovery, Reports, and Documents endpoints behind a single typed `Lighthouse` class. Zero runtime dependencies: it uses the global `fetch`.\n\n## Installation\n\n```bash\nnpm i lighthouse-private-markets-sdk\n# or\nyarn add lighthouse-private-markets-sdk\n```\n\nWorks in Node 18+, modern browsers, Deno, Bun, and edge runtimes.\n\n## Quick Start\n\nGenerate an API key at **Settings → API Keys** in your Lighthouse workspace.\n\n```ts\nimport { Lighthouse } from 'lighthouse-private-markets-sdk'\n\nconst client = new Lighthouse('lgt_live_...')\n\n// Who am I? Profile, workspace role, permissions and teams.\nconst me = await client.users.me()\nconsole.log(me.data)\n\n// Search your CRM\nconst res = await client.records.search('company', {\n  filters: {\n    operator: 'AND',\n    conditions: [\n      { field: 'domain', operator: 'contains_any', value: ['acme.com'] },\n    ],\n  },\n  limit: 10,\n})\nconsole.log(res.data, res.meta)\n```\n\nEvery method resolves to an `APIResponse`:\n\n```ts\ninterface APIResponse<T = unknown> {\n  data:   T | null\n  error:  unknown | null\n  meta?:  Record<string, unknown>\n  status: number\n}\n```\n\n## Authentication\n\nThe client sends `Authorization: Bearer <api_key>` on every request. Keep API keys secret. Never ship them in client-side code.\n\n## Resources\n\n| Namespace             | Endpoints |\n|-----------------------|-----------|\n| `client.records`      | search / get / create / update / delete records (company, person, deal, custom objects) |\n| `client.lists`        | list / search / get / create / update / delete, getRecords, addRecord, removeRecord, shareWithTeams, revokeTeams, updateSharing |\n| `client.notes`        | list / get / create / delete, linkRecord, unlinkRecord |\n| `client.tasks`        | list / get / create / update / delete |\n| `client.attributes`   | list / get / create / update / delete, colors |\n| `client.options`      | list / create / delete select-field options |\n| `client.views`        | list / get / update / delete, shareWithTeams, revokeTeams, updateSharing |\n| `client.users`        | me, list, get |\n| `client.teams`        | list, get |\n| `client.dashboards`   | list / get / create / update / delete, listWidgets, createWidget, updateWidget, deleteWidget, widgetData, shareWithTeams, revokeTeams |\n| `client.discovery`    | getAttributes, searchCompanies, searchPeople, semanticSearchCompanies, semanticSearchPeople, searchEntities, getEntityByName, lookupCompaniesByDomain, lookupCompaniesByLinkedin, lookupPeopleByLinkedin, getSavedSearches, getSearchResults, getLocations, getIndustries, getTags, searchInvestors (deprecated), searchOrganizations (deprecated), searchSchools (deprecated) |\n| `client.documents`    | folders + files (v2): listFolders, createFolder, getFolder, updateFolder, deleteFolder, link/unlinkFolder, uploadUrl, getFile, getFileContent, updateFile, deleteFile, link/unlinkFile, listRecordDocuments, `upload()` one-shot helper |\n| `client.inbox`        | emailAccountMe, emailAccounts, emailThreads, emailThread, createEmailDraft, emailAttachmentContent, downloadEmailAttachment, linkedinAccountMe, linkedinAccounts, linkedinChats, linkedinChatMessages, linkedinDrafts, createLinkedinDraft, deleteLinkedinDraft, downloadLinkedinAttachment, calendarAccountMe, calendarAccounts, calendarMeetings, calendarMeeting, calendarMeetingNotes, calendarMeetingTranscript |\n| `client.network`      | introPaths |\n| `client.aiReports`    | list, get |\n\n## Examples\n\n### Inbox\n\nEmail, LinkedIn and calendar speak one vocabulary. The same concept is the same\nkey on every channel: `id`, `date`, `from`, `direction`, `text`, `is_read`,\n`participants`, `last_message`, `attachments`, `account`. People are objects\n(`{ name, email }` on email and calendar, a profile object on LinkedIn), empty\nis `null`, `[]` or `false`, a fact reads `is_...` or `has_...`, and a link ends\nin `_url`. Every response type below is exported.\n\n```ts\n// Accounts you can access (own and shared), with the permission flags.\n// One account shape on all three channels: { id, name, email, is_owner,\n// provider, status, status_reason, owner, permissions, created_at }.\n// On LinkedIn, email is null.\nconst { data: accounts } = await client.inbox.emailAccounts()\nconst own = accounts.filter((a) => a.is_owner)\n\n// Email threads with a CRM company (its domains), newest first.\n// emails and domains take one address, a comma list or an array.\nconst { data: threads, meta } = await client.inbox.emailThreads({\n  record_id: companyId,\n  record_type: 'company',\n  is_read: false,\n  limit: 50,\n})\n// meta.accounts reports one entry per account you can see:\n// { id, name, email, is_owner, ok, error, count }, where error is\n// rate_limited, account_disconnected, forbidden and the like.\n// A thread's preview lives inside last_message, beside who wrote it.\nconst { subject, last_message, account } = threads[0]\nconsole.log(subject, last_message?.snippet, last_message?.from?.name, account?.is_owner)\n\n// Read one thread. account_id is required (the row's account.id).\n// order is 'desc' (the default) or 'asc', direction is 'sent' or 'received'.\n// An oldest-first read answers one page and no next_cursor.\nconst { data: detail } = await client.inbox.emailThread(threads[0].id, {\n  account_id: threads[0].account.id,\n  order: 'asc',\n})\n// detail.has_limited_access is true when the account is shared for metadata\n// only: body, text and both file lists come back empty.\n\n// A message says who wrote it as an object, and carries the HTML body beside\n// text, which is this message's own words with the quoted history removed.\nconst msg = detail.messages[0]\nconsole.log(msg.from?.name, msg.from?.email, msg.text)\n\n// Download an attachment from a message (files embedded in the body are\n// listed apart, under inline_images). Every file names its own message.\nif (msg.attachments.length) {\n  const file = await client.inbox.downloadEmailAttachment(msg.id, msg.attachments[0].id, {\n    account_id: msg.account.id,\n  })\n  // file.data is an ArrayBuffer, file.contentType the media type.\n  // Attachments over 25 MB are not served (413 attachment_too_large).\n}\n\n// Read what an attachment SAYS instead of downloading it: PDFs, Word, Excel\n// and PowerPoint files (and their OpenDocument versions), text, CSV and HTML.\n// A long file comes back in windows: page with offset_chars while\n// meta.has_more is true. When text is null, reason and note say why (an image,\n// an older .doc or .xls, a scan with no text).\nif (msg.attachments.length) {\n  const { data: read, meta } = await client.inbox.emailAttachmentContent(msg.id, msg.attachments[0].id, {\n    account_id: msg.account.id,\n  })\n  console.log(read?.kind, read?.text ?? read?.note, meta?.has_more)\n}\n\n// The same for a file kept in Documents.\n// const { data: memo } = await client.documents.getFileContent(fileId)\n\n// Draft a reply. The draft is saved in the mailbox, never sent, and comes back\n// as an ordinary message with direction 'draft'. to, cc and bcc take an\n// address, a comma list, an array or { name, email } objects, so a message's\n// own from or cc can be handed straight back. Write the message as text\n// (plain) or as body (HTML), not both.\nconst { data: draft } = await client.inbox.createEmailDraft({\n  account_id: msg.account.id,\n  to: [msg.from],\n  subject: `Re: ${msg.subject}`,\n  text: 'Thanks for the note, following up shortly.',\n  thread_id: detail.thread.id,\n  reply_to_message_id: msg.id,\n})\n\n// LinkedIn: chats with a person, then one chat. is_read false lists the chats\n// that have unread messages; after and before read the chat's own date and\n// both include the moment they name.\nconst { data: chats } = await client.inbox.linkedinChats({\n  person_linkedin: 'https://linkedin.com/in/username',\n  is_read: false,\n})\nconst { data: chatDetail } = await client.inbox.linkedinChatMessages(chats[0].id)\nconsole.log(chatDetail.chat?.participants.map((p) => p.name), chatDetail.messages[0].from?.name)\n\n// Only the replies after a moment, oldest first. direction is 'sent' or\n// 'received', order is 'desc' (the default) or 'asc'. A date-only before\n// includes that whole day. On a message the account sent, from is the\n// account's own profile (connection_degree 'self') and is_seen / is_delivered\n// are the other side's receipts; both are null on a message it received.\nconst { data: replies } = await client.inbox.linkedinChatMessages(chats[0].id, {\n  after: '2024-03-01T09:30:00Z',\n  direction: 'received',\n  order: 'asc',\n})\n\n// Drafts, the LinkedIn twin of createEmailDraft: the message you are still\n// writing to a person, the same draft the Lighthouse app autosaves, one per\n// LinkedIn account and person, and one per group chat. account_id is required\n// (the id from linkedinAccounts). Name the conversation by chat_id (a group\n// chat included), or the person by person_linkedin even when you have never\n// messaged them: the draft then starts a new conversation (chat_id null),\n// listed under Drafts in the Lighthouse inbox. Nothing is ever sent from\n// here: sending happens in the app, and a send clears the draft. Creating a\n// draft where one exists replaces its text; files attached in the app stay.\nconst { data: draft } = await client.inbox.createLinkedinDraft({\n  account_id: chats[0].account.id,\n  person_linkedin: 'https://www.linkedin.com/in/username',\n  text: 'Great to meet you last week. Could we find 20 minutes on Thursday?',\n})\nconsole.log(draft.id, draft.chat_id, draft.recipient?.name, draft.updated_by?.full_name)\n\n// Every draft you can read, most recently edited first, then discard one. A\n// group chat's draft has recipient null. A chat also carries its draft\n// (chats[0].draft), and linkedinChats({ has_draft: true }) lists the chats with one.\nconst { data: drafts } = await client.inbox.linkedinDrafts({ account_id: chats[0].account.id })\nawait client.inbox.deleteLinkedinDraft(drafts[0].id)\n\n// Meetings on a connected calendar. meta.window is { after, before }.\nconst { data: meetings } = await client.inbox.calendarMeetings({\n  domains: ['example.com'],\n  upcoming: true,\n})\nconsole.log(meetings[0].meeting_url, meetings[0].attendees[0].response_status)\n\n// One recorded meeting, then its notes and its transcript. meeting_key is the\n// one handle that is the same for everybody who sat in the meeting, and a row\n// carries it once the meeting was recorded.\nconst key = meetings.find((m) => m.recording?.has_recording)?.meeting_key\nconst { data: recorded } = await client.inbox.calendarMeeting(key)\nif (recorded.recording.has_notes) {\n  const { data: notes } = await client.inbox.calendarMeetingNotes(recorded.meeting_key)\n  console.log(notes.content_markdown, notes.is_edited)\n}\n// meta is { total_chars, offset_chars, returned_chars, has_more, next_offset_chars }\nconst { data: transcript } = await client.inbox.calendarMeetingTranscript(recorded.meeting_key, {\n  offset_chars: 0,\n  max_chars: 20000,\n})\n```\n\nA recorded meeting's notes and transcript are returned only to a user who\nrecorded the meeting, was invited to it (as a guest, declined included, or as its\norganizer), or had its meeting notes shared with them by a teammate who recorded it.\nEach occurrence of a recurring meeting is judged on its own; anything else is not\nfound. A meeting note carries the same `meeting_key` in its `meeting` block, so a\nuser who can read the note can read its transcript (when `meeting.has_transcript` is\ntrue) without access to the recorder's calendar. The recording itself is not\navailable through the API. A calendar account's `permissions` carry `calendar_view` (read its meetings)\nand `meeting_notes_view` (read the notes of the meetings its owner recorded).\n\nResponse types: `Address`, `LinkedinPerson`, `WorkspaceMember`, `AccountRef`,\n`Account`, `Attachment`, `InlineImage`, `EmailThread`, `EmailMessage`,\n`EmailDraft`, `EmailThreadDetail`, `LinkedinChat`, `LinkedinDraft`,\n`LinkedinDraftRow`, `LinkedinMessage`, `LinkedinChatDetail`, `Meeting`, `Recording`, `RecordedMeeting`,\n`MeetingNotes`, `MeetingTranscript`.\n\nThree older parameter names are refused with a sentence rather than ignored, so\na filter can never be dropped in silence: use `emails` and `domains` in place of\n`email` and `domain` on threads and meetings, and `is_read` in place of `unread`\non chats (`is_read: false` lists the chats that have unread messages).\n\n### Network\n\nWho can introduce you to a person, from what your team's LinkedIn networks\nalready say. This endpoint reads what is already known and never starts a new\nsearch, so it is safe to call as often as you like. Searches are run from the\nperson's Network tab in Lighthouse.\n\n```ts\n// Ask by CRM record, or by LinkedIn address for somebody who is not in your\n// CRM. Send exactly one of record_id and linkedin: both together, or neither,\n// is answered 400 invalid_input with a sentence naming the rule.\nconst { data, meta } = await client.network.introPaths({ record_id: personId })\n\n// Best paths first: the person who can make the introduction, the colleagues\n// whose network found them, and how many connections they share with the\n// person you want to reach.\nfor (const path of data.paths) {\n  console.log(path.person.name, path.found_via.map((m) => m.full_name), path.shared_connections_count)\n}\n// connection_degree on a path is relative to the colleague who found them, not\n// to you: 'first' means a direct connection of that colleague, the warm intro.\n\n// Why a path is missing is on the answer, so nothing has to be guessed.\n// data.is_searched false: nobody has looked for this person yet.\n// data.is_searched true with meta.total 0: a search found no shared connection.\n// An account with is_searched false says why in not_searched_reason\n// ('account_disconnected', 'account_pending', 'no_searches_left' or\n// 'not_searched_yet'), and searches_used / searches_limit say how much of this\n// month is left on it.\nfor (const account of data.accounts) {\n  if (!account.is_searched) {\n    console.log(account.name, account.not_searched_reason, account.searches_used, account.searches_limit)\n  }\n}\n\n// Narrow to certain accounts with account_ids: one id, a comma list or an\n// array of the ids linkedinAccounts() returns. Every account you can see stays\n// in accounts, and the ones you left out come back with is_included false. An\n// id you cannot use is answered 404 not_found.\nconst { data: myAccounts } = await client.inbox.linkedinAccounts()\nconst { data: throughMyAccounts } = await client.network.introPaths({\n  linkedin: 'https://www.linkedin.com/in/username',\n  account_ids: myAccounts.filter((a) => a.is_owner).map((a) => a.id),\n  limit: 50,\n})\n\n// meta is { total, limit, offset, has_more, note }. total counts the paths\n// before paging. note is one sentence for a person to read, or null: branch on\n// the keys, never on that text.\n```\n\nThe answer covers your own LinkedIn accounts and the ones colleagues have\nshared with you. A colleague who has not shared their network is not\nrepresented at all, so a missing path can also mean a network this key cannot\nsee.\n\nResponse types: `IntroPaths`, `IntroPath`, `IntroPathAccount`,\n`NotSearchedReason`, over the same `LinkedinPerson`, `WorkspaceMember` and\n`AccountRef` shapes the inbox uses.\n\n### Records\n\n```ts\n// Create\nconst { data: created } = await client.records.create('company', {\n  name: 'Acme',\n  domain: ['acme.com'],\n})\nconst recordId = (created as any).id\n\n// Update: relation fields are replaced entirely, so pass the full desired array\nawait client.records.update('company', recordId, {\n  domain: ['acme.com', 'acme.io'],\n})\n\n// Get\nawait client.records.get('company', recordId)\n\n// Delete\nawait client.records.delete('company', recordId)\n\n// Upsert: find-or-create by a unique attribute. The value to match on is the\n// fourth argument, so it never has to be repeated inside data; leave it out to\n// keep the older form, where the attribute inside data carries it.\nawait client.records.upsert('company', 'domain', { name: 'Acme' }, 'acme.com')\n\n// In a batch the attribute is one per call and the value rides on the item,\n// as matching_value or as the attribute itself. The two forms mix.\nawait client.records.bulkUpsert('person', 'email', [\n  { matching_value: 'jane@acme.com', first_name: 'Jane', last_name: 'Doe' },\n  { emails: ['sam@globex.com'], first_name: 'Sam', last_name: 'Rivera' },\n])\n```\n\n### Filtering with attributes\n\n```ts\nconst attrs = await client.attributes.list({ record_type: 'company' })\n// Scope to a list to see the custom fields that belong to it. A list you\n// cannot open answers 404 not_found, the same as one that does not exist.\nconst scoped = await client.attributes.list({ record_type: 'company', list_id: '…uuid…' })\n// pick a field permalink from attrs.data, then:\nawait client.records.search('company', {\n  filters: {\n    operator: 'AND',\n    conditions: [{ field: 'headcount', operator: 'gte', value: 50 }],\n  },\n  sort: [{ field: 'name', direction: 'asc' }],\n  limit: 25,\n})\n```\n\n### Lists\n\n```ts\n// Find a list by its name: only the lists whose name contains every word, in\n// any case (\"reviewed\" finds \"Deals Reviewed\", never \"Deals to Review\").\n// meta.total counts the matches. `query` is accepted as another name for `search`.\nconst { data: found } = await client.lists.search({ search: 'deals reviewed' })\n\nconst { data: newList } = await client.lists.create({\n  name: 'Hot leads',\n  record_type: 'company',\n})\nconst listId = (newList as any).id\n\n// Adding a record that is already on the list, and removing one that is not,\n// both succeed and write nothing. `reason` is what tells that apart from a\n// real change: a sentence when nothing changed, null when something did. It is\n// on the single writes and on every entry of the two bulk ones, where a failed\n// entry carries the refusal in words there instead.\nconst { data: added } = await client.lists.addRecord(listId, '…record_uuid…')\nif (added.reason) console.log(added.reason) // \"This record was already on the list.\"\nawait client.lists.getRecords(listId, { limit: 50 })\n```\n\n### Sharing views and dashboards\n\n```ts\nawait client.views.updateSharing('…view_uuid…', 'workspace', { shareAlong: true })\nawait client.views.shareWithTeams('…view_uuid…', ['…team_uuid…'], { shareAlong: true })\n\nawait client.dashboards.update('…dashboard_uuid…', {\n  sharing: 'workspace',\n  share_along: true,\n})\nawait client.dashboards.shareWithTeams('…dashboard_uuid…', ['…team_uuid…'], {\n  shareAlong: true,\n})\n```\n\nThese four calls always answer `data.shared_along` (what the call shared along, empty when nothing was) and `data.audience_cannot_open`. `audience_cannot_open` lists the lists the view filters on, or the dashboard's widgets filter on, that some of the people it is now shared with cannot open. For those people a filter on such a list is not applied. Only lists you can open are listed. An entry with `is_parent: true` is the list a list view belongs to: people who cannot open it do not see the view at all.\n\n`shareAlong` (`share_along` on `dashboards.update`) also shares the lists you own with the same people. Every one of them is shared along, so read `audience_cannot_open` first if you want to choose. A list is shared along only when your role can share lists. Nothing is shared along unless you pass `true`. `revokeTeams` never shares anything along and ignores the option.\n\n### Notes\n\n```ts\nawait client.notes.create({\n  title: 'Intro call',\n  content: '<p>Met the founder, looking strong.</p>',\n  records: { company: ['…uuid…'], person: ['…uuid…'] },\n  tags: ['follow_up'],\n})\n\n// Manage the workspace tag vocabulary and attach tags to a note.\nconst { data: tag } = await client.noteTags.create({ label: 'Follow up' })\nawait client.notes.addTags('…note_uuid…', [tag.permalink])\n```\n\nA note now reads back who it mentions: `tagged_users` is an array of the member\nobject (`{ id, first_name, last_name, full_name, avatar_url, email }`), `[]`\nwhen it mentions nobody. It is read from the mention spans in `content`, not\nfrom the ids a write sent, so pair every id in `tagged_users` with its span\n(`<span data-type=\"mention\" data-id=\"…\" data-label=\"First Last\">@First Last</span>`)\nas the reference says, or the note reads back `[]`.\n\n### Tasks\n\n```ts\nawait client.tasks.create({\n  title: 'Send follow-up',\n  due_date: '2026-06-01',\n  assigned_to: ['…user_uuid…'],\n  records: { company: ['…uuid…'] },\n})\n```\n\n`status` and `priority` answer the stored value and nothing else. The words\nyour workspace puts on those values come from `options.list()` for the `task`\nrecord type, one call that covers every task.\n\n`tasks.list()` filters and sorts on these fields: `title`, `status`,\n`priority`, `due_date`, `created_at`, `updated_at`, `assigned_to`,\n`created_by`, `records`, plus any custom task field permalinks. Status values\nare workspace-specific (fetch the valid values with\n`client.options.list('task', 'status')`). Each option row is `{ value, label, color,\npriority, is_default, enabled }`, the same object an attribute's\n`options[]` carries; a built-in value the workspace switched off is listed\nwith `enabled: false`, so filter on `enabled` before offering one.\n\n```ts\nawait client.tasks.list({\n  filters: {\n    operator: 'AND',\n    conditions: [{ field: 'status', operator: 'eq', value: 'to_do' }],\n  },\n  sort: [{ field: 'due_date', direction: 'asc' }],\n  limit: 50,\n})\n```\n\n### Discovery\n\nDiscovery takes its sort as a single object, `DiscoverySort`\n(`{ by, dir }`), where the CRM record, task and note searches take an array of\n`Sort` items (`[{ field, direction }]`). The two shapes are not\ninterchangeable. `searchCompanies`, `searchPeople`, `records.search`,\n`notes.search` and `tasks.list` refuse the wrong one with a 400 that names the\nshape they expected; `semanticSearchCompanies`, `semanticSearchPeople` and\n`lists.getRecords` do not refuse it. They apply what they can, answer the rest\nin their default order, and name every sort they could not use in\n`meta.invalid_sort_fields`, so read that array rather than assuming the order\nyou asked for. Only companies can be sorted, by `founded_on` or\n`total_funding_amount_usd`; people always come back in relevance order. Omit\n`sort` for the default order.\n\n```ts\n// Always inspect attributes first to discover valid field keys\nawait client.discovery.getAttributes({ record_type: 'company' })\n\n// Search Lighthouse's global database\nconst companies = await client.discovery.searchCompanies({\n  filters: {\n    operator: 'AND',\n    conditions: [\n      { field: 'company_hq_country', operator: 'contains_any', value: ['US'] },\n      {\n        field: 'company_headcount',\n        operator: 'between',\n        value: { min: 10, max: 200 },\n      },\n    ],\n  },\n  sort: { by: 'founded_on', dir: 'desc' },\n  limit: 25,\n})\n\n// Enrich by domain / LinkedIn\nawait client.discovery.lookupCompaniesByDomain(['stripe.com', 'openai.com'])\n\n// Find an entity by name (typo-tolerant, ranked by relevance then popularity).\n// record_type is one of company, person, investor, school. Use a returned id\n// in a filter, or pass it to a deep read. Pages with limit / offset and\n// reports meta.has_more.\nconst { data: matches, meta } = await client.discovery.searchEntities({\n  record_type: 'company',\n  query: 'relay robotics',\n  limit: 10,\n})\n\n// Resolve a name straight to the single best match: the full profile for a\n// company or a person, the identity row for an investor or a school. 404 when\n// nothing matches, so prefer searchEntities when the name is ambiguous.\nconst { data: company } = await client.discovery.getEntityByName({\n  record_type: 'company',\n  name: 'Relay Robotics',\n})\n\n// Semantic search: rank by a plain-language query (optionally narrowed by\n// the same filters). min_similarity (0 to 0.99) drops weak matches.\nawait client.discovery.semanticSearchCompanies({\n  query: 'AI-native developer tooling startups in Europe',\n  filters: {\n    operator: 'AND',\n    conditions: [{ field: 'company_hq_country', operator: 'contains_any', value: ['GB', 'DE', 'FR'] }],\n  },\n  min_similarity: 0.4,\n  limit: 25,\n})\n```\n\n`searchInvestors`, `searchOrganizations` and `searchSchools` are deprecated: use\n`searchEntities` with `record_type` `'investor'`, `'company'` or `'school'`\ninstead (typo-tolerant ranking, paging, and richer matches). They keep working.\n\nThe shared vocabularies (`getLocations`, `getIndustries`, `getTags`,\n`getFundingTypes`) answer `value` (what a filter takes) beside `label` (what to\nshow someone). `name` carries the same string as `value` on funding types and\nthe same string as `label` on the other three; it is kept for compatibility, so\nread `value` and `label`.\n\nA discovery person's `associated_companies` and `education` entries carry `id`,\nthe company or school id `getCompany` takes. `org_id` holds the same value and\nis kept for compatibility.\n\nEvery discovery company and person row carries `crm_id`: the id of your own CRM\nrecord for that entity, and `null` when it is not in your CRM. It is on the\nsearch pages, the two lookups, the saved-search results and the full profiles\nalike, so \"which of these do we already track\" is answered by the row you have\nrather than by a search per result. Pass it to\n`client.records.get('company', crm_id)` to open the record, and use\n`client.records.upsert` on the rows where it is `null` to bring one in without\ncreating a duplicate. It is the same name, with the same meaning, that a CRM\nrecord already uses when it points at the global database (a company's\n`investors`, a person's `experience` and `education`).\n\n### Documents\n\n```ts\n// One-shot helper: reserves the file, uploads the content and confirms it\n// (the size is measured on our side, you never pass it)\nconst file = await client.documents.upload({\n  name: 'pitch.pdf',\n  content: pitchBlob, // Blob | ArrayBuffer | Uint8Array\n  content_type: 'application/pdf',\n  sharing: 'workspace',\n})\nconst fileId = (file.data as any).id\n\nawait client.documents.linkFileToRecord(fileId, 'company', '…uuid…')\n```\n\n## Error handling\n\n```ts\nimport { Lighthouse, LighthouseError } from 'lighthouse-private-markets-sdk'\n\nconst client = new Lighthouse('lgt_live_…')\nconst res = await client.records.search('company')\nif (res.error) {\n  console.error('API error:', res.status, res.error)\n} else {\n  console.log(res.data)\n}\n```\n\nNetwork failures and malformed JSON responses throw `LighthouseError`.\n\n## Rate limits\n\nA `429` with the code `rate_limit_exceeded` is retried for you, up to 4 times, before the client hands you the `429` exactly as the API sent it. Before each retry the client waits at least the seconds in the `Retry-After` header (1 second if there is none, at most 30). Each retry also draws a random wait that can grow with every attempt: up to 2 seconds before the first retry, then up to 4, 8 and 16 (never more than 30), and the client waits the longer of the two. Calls refused at the same moment therefore spread out instead of all coming back together and being refused again. The API refuses such a request before running it, so a retried `POST` is never applied twice. No other error is retried, `credit_limit_exceeded` included. `maxRetries` sets the number of retries, and `0` turns retrying off:\n\n```ts\nconst client = new Lighthouse('lgt_live_...', { maxRetries: 0 })\n```\n\n## Low-level escape hatch\n\nFor endpoints not yet wrapped by a namespace:\n\n```ts\nawait client.request('GET', '/v1/some/new/endpoint', {\n  params: { foo: 'bar' },\n})\n\nawait client.request('POST', '/v1/some/other', {\n  body: { hello: 'world' },\n})\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}