npm package: @lineai/linkedin-api
Description: Professional LinkedIn API client with TypeScript support, entity classes, and developer-friendly features. Perfect for AI coders, recruiting, lead generation, market research, and content analysis. Includes comprehensive JSDoc, helper constants (LOCATIONS
Author: LineAI
Latest version: 1.3.11

# @lineai/linkedin-api

Professional LinkedIn API client with TypeScript support, entity classes, and developer-friendly features. Implements the [LinkedIn Data API from RapidAPI](https://rapidapi.com/rockapis-rockapis-default/api/linkedin-data-api).

## Quick Reference

| Operation | Method | Purpose | Key Parameters | Returns |
|-----------|--------|---------|----------------|---------|
| **Profile Search** | `searchProfiles(params)` | Find profiles by criteria | `keywords`, `geo`, `company`, `minItems` | `ProfileSearchResult` |
| **Profile Get** | `getProfile(username)` | Full profile data | `username` | `LinkedInProfile` |
| **Company Search** | `searchCompanies(params)` | Find companies by criteria | `keywords`, `industries`, `companySizes`, `minItems` | `CompanySearchResult` |
| **Company Get** | `getCompany(username)` | Full company data | `username` | `LinkedInCompany` |
| **Post Search** | `searchPosts(params)` | Find posts by criteria | `keywords`, `author`, `datePosted`, `minItems` | `PostSearchResult` |
| **Post Get** | `getPost(postId)` | Full post data | `postId` | `LinkedInPost` |

### Essential Imports
```javascript
import LinkedInAPI, { 
  LOCATIONS, 
  INDUSTRIES, 
  COMPANY_SIZES,
  LinkedInError,
  RateLimitError,
  NotFoundError 
} from '@lineai/linkedin-api';
```

### Core Entity Methods

| Entity | Key Methods | Returns |
|--------|-------------|---------|
| **LinkedInProfile** | `getFullName()`, `getHeadline()`, `getCurrentPosition()`, `getLinkedInUrl()` | Formatted strings/objects |
| **LinkedInCompany** | `getName()`, `getFollowerCount()`, `getEmployeeCount()`, `getLinkedInUrl()` | Company data |
| **LinkedInPost** | `getText()`, `getLikeCount()`, `getCommentCount()`, `getAuthor()` | Post metrics |

## 🆕 What's New in v1.3.8

- 🔍 **Improved TypeScript Types** - Added `PostSearchItemAuthorData` interface for better type safety when working with post search results
- 🎯 **Enhanced IntelliSense** - Post search author data now has proper autocompletion and type checking
- 🛠️ **Type Safety Fix** - Eliminates type mismatches between post search results and full post data author schemas

### v1.3.7 Features
- 🔧 **Next.js Build Fix** - Fixed webpack compilation error by replacing process.env.NODE_ENV manipulation with instance-based debug mode
- ⚡ **Better Debug Mode** - Debug logging now uses instance property instead of environment variable modification

### v1.3.6 Features
- 📦 **Package Optimization** - Removed examples directory to reduce package size and improve build compatibility
- 🛠️ **Build Fixes** - Resolved Next.js build issues by streamlining package structure

### v1.3.5 Features
- 🧹 **Dependency Cleanup** - Removed Prettier and related ESLint configurations to reduce package size and simplify development
- ⚡ **Streamlined Build** - Cleaner development environment with fewer dependencies

### v1.3.4 Features
- 🛠️ **Data Transformation Fix** - Fixed company search `universalName` → `username` transformation to occur in CompanySearchResult.items getter for better data consistency
- 🎯 **Unified API Experience** - Both `api.getProfile(item.username)` and `api.getCompany(item.username)` now use the same field name

### v1.3.3 Features
- 🔄 **Consistent Username Field** - Company search results now use `username` instead of `universalName` for consistency with profile search

### v1.3.2 Features
- 🗑️ **Simplified Search Results** - Removed ProfileSearchItem, CompanySearchItem, and PostSearchItem wrapper classes for direct data access
- 🎯 **Direct Data Access** - Search results now return raw data objects with helper methods for cleaner, simpler usage

### v1.3.1 Features
- 🔧 **Fixed Location Parameter Types** - LocationId is now correctly typed as `number` instead of `string`
- ✅ **API Compatibility** - Location constants (LOCATIONS) now properly work with LinkedIn API requirements
- 🎯 **Cross-Method Consistency** - Both profile search (GET) and company search (POST) handle location arrays correctly

### v1.3.0 Features
- ✨ **Enhanced Type Safety** - Complete TypeScript response type definitions prevent runtime errors
- 🔒 **Immutable Parameters** - Search parameters are never mutated, ensuring predictable behavior
- 🏎️ **Improved Caching** - Now works for all `get` methods with 100% cache hit performance
- 🛠️ **Better Error Handling** - More specific error types with better debugging information
- 🎯 **Unified API** - Consistent pagination and parameter handling across all search methods
- 🔍 **Enhanced Debugging** - Improved debug logs with accurate response structure information

## Features

- 🚀 **Easy to use** - Simple, intuitive API with high-level methods
- 📘 **TypeScript ready** - Full type definitions included
- 🎯 **Entity classes** - Clean data access without dealing with raw JSON
- 🔄 **Seamless data loading** - Load full profiles/companies/posts from search results
- 🛡️ **Safe defaults** - Never worry about null/undefined values
- ⚡ **Built-in caching** - Reduce API calls automatically (v1.3.0+)
- 🎛️ **Pagination helpers** - Easy navigation through search results
- 🏷️ **Helper constants** - No more magic strings for locations, industries, etc.
- 🔒 **Parameter immutability** - Original search params never modified (v1.3.0+)

## Installation

```bash
npm install @lineai/linkedin-api
# or
yarn add @lineai/linkedin-api
```

## Quick Start

```javascript
import { LinkedInAPI } from '@lineai/linkedin-api';

const api = new LinkedInAPI('your-rapidapi-key');

// Get a profile
const profile = await api.getProfile('satyanadella');
console.log(profile.getFullName()); // "Satya Nadella"
console.log(profile.getCurrentPosition()?.title); // "CEO"
```

## Usage Examples

### Profile Operations

```javascript
import { LinkedInAPI, LOCATIONS } from '@lineai/linkedin-api';

const api = new LinkedInAPI('your-rapidapi-key');

// Search for profiles
const searchResults = await api.searchProfiles({
  keywords: 'software engineer',
  geo: [LOCATIONS.US.SAN_FRANCISCO, LOCATIONS.US.NEW_YORK], // Auto-joined
  company: 'Google'
});

console.log(`Found ${searchResults.total} profiles`);

// Access search results
searchResults.items.forEach(item => {
  console.log(item.getFullName());
  console.log(item.getHeadline());
  console.log(item.getLinkedInUrl());
});

// Get full profile using username from search result
const firstResult = searchResults.items[0];
const fullProfile = await api.getProfile(firstResult.username);

// Access full profile data
console.log(fullProfile.getFullName());
console.log(fullProfile.getCurrentPosition()?.companyName);
console.log(fullProfile.getEducation()[0]?.schoolName);
console.log(fullProfile.getSkills().length);
console.log(fullProfile.getProfilePictureUrl('large'));

// Direct profile access
const profile = await api.getProfile('billgates');
console.log(profile.getLinkedInUrl()); // https://linkedin.com/in/billgates
```

### Company Operations

```javascript
import { LinkedInAPI, COMPANY_SIZES, INDUSTRIES, LOCATIONS } from '@lineai/linkedin-api';

const api = new LinkedInAPI('your-rapidapi-key');

// Search companies
const companies = await api.searchCompanies({
  keyword: 'artificial intelligence',
  locations: [LOCATIONS.US.ALL],
  industries: [INDUSTRIES.TECHNOLOGY],
  companySizes: [COMPANY_SIZES.LARGE, COMPANY_SIZES.XLARGE], // 201-1000 employees
  hasJobs: true
});

// Access company data
const company = companies.items[0];
console.log(company.getName());
console.log(company.getTagline());

// Get full company details using username from search result
const fullCompany = await api.getCompany(company.username);
console.log(fullCompany.getEmployeeCount());
console.log(fullCompany.getHeadquarters()?.city);
console.log(fullCompany.getSpecialties());
console.log(fullCompany.getFollowerCount());
console.log(fullCompany.getWebsite());

// Direct company access
const microsoft = await api.getCompany('microsoft');
console.log(microsoft.getName()); // "Microsoft"
console.log(microsoft.getEmployeeCount());
console.log(microsoft.getAllLocations().length);
```

### Post Operations

```javascript
const api = new LinkedInAPI('your-rapidapi-key');

// Search posts
const posts = await api.searchPosts({
  keyword: 'machine learning',
  sortBy: 'date_posted',
  fromCompany: [1441] // Google's company ID
});

console.log(`Found ${posts.total} posts`);

// Access post data
const post = posts.items[0];
console.log(post.getText());
console.log(post.getAuthorName());
console.log(post.getPostedAt());

// Get full post details using URN from search result
const fullPost = await api.getPost(post.urn);
console.log(fullPost.getTotalEngagement());
console.log(fullPost.getLikeCount());
console.log(fullPost.getCommentsCount());
console.log(fullPost.hasVideo());
console.log(fullPost.getArticle()?.title);

// Direct post access
const specificPost = await api.getPost('7219434359085252608');
console.log(specificPost.getAuthor().firstName);
console.log(specificPost.getTotalEngagement());
```

### Pagination

```javascript
// Handle pagination easily
const results = await api.searchProfiles({ keywords: 'CEO' });

console.log(`Page 1: ${results.items.length} items`);

if (results.hasNextPage()) {
  const page2 = await results.getNextPage();
  console.log(`Page 2: ${page2.items.length} items`);
}

// Process all pages
let currentPage = results;
let pageNum = 1;

while (currentPage.items.length > 0) {
  console.log(`Processing page ${pageNum}: ${currentPage.items.length} items`);
  
  // Process items
  for (const item of currentPage.items) {
    console.log(item.getFullName());
  }
  
  if (currentPage.hasNextPage()) {
    currentPage = await currentPage.getNextPage();
    pageNum++;
  } else {
    break;
  }
}
```

### Error Handling

```javascript
import { LinkedInAPI, NotFoundError, RateLimitError } from '@lineai/linkedin-api';

const api = new LinkedInAPI('your-rapidapi-key');

try {
  const profile = await api.getProfile('invalid-username-xyz');
} catch (error) {
  if (error instanceof NotFoundError) {
    console.error('Profile not found');
  } else if (error instanceof RateLimitError) {
    console.error(`Rate limited. Retry after: ${error.retryAfter}`);
  } else {
    console.error('Unexpected error:', error.message);
  }
}
```

### TypeScript Support

```typescript
import { 
  LinkedInAPI, 
  LinkedInProfile, 
  LinkedInCompany,
  ProfileSearchParams,
  LOCATIONS,
  LocationId 
} from '@lineai/linkedin-api';

const api = new LinkedInAPI('your-key');

// Full type safety
const searchParams: ProfileSearchParams = {
  keywords: 'engineer',
  geo: [LOCATIONS.US.SEATTLE] as LocationId[]
};

const results = await api.searchProfiles(searchParams);

// TypeScript knows all available methods
const profile: LinkedInProfile = await api.getProfile('username');
const position = profile.getCurrentPosition(); // Type: Position | null

if (position) {
  console.log(position.title); // TypeScript knows all Position properties
  console.log(position.companyName);
}
```

### Helper Constants

```javascript
import { LOCATIONS, COMPANY_SIZES, INDUSTRIES } from '@lineai/linkedin-api';

// Use location constants instead of numeric IDs
const usProfiles = await api.searchProfiles({
  geo: [LOCATIONS.US.ALL]
});

const techCompanies = await api.searchCompanies({
  locations: [
    LOCATIONS.US.SAN_FRANCISCO,
    LOCATIONS.US.SEATTLE,
    LOCATIONS.US.AUSTIN
  ],
  industries: [INDUSTRIES.TECHNOLOGY],
  companySizes: COMPANY_SIZES.ALL // All company sizes
});
```

## 🆕 Smart Retry with minItems (v1.2.2)

All search methods now support intelligent retry logic to ensure you get the results you need:

### Profile Search with minItems
```javascript
// Get at least 15 profiles - will retry across different pages until found
const profiles = await api.searchProfiles({
  keywords: 'software engineer',
  geo: [LOCATIONS.US.SAN_FRANCISCO],
  minItems: 15,     // Minimum results required
  maxRetries: 5     // Maximum retry attempts
});

console.log(`Found ${profiles.items.length} profiles`); // At least 15
```

### Company Search with minItems
```javascript
// Get substantial company dataset
const companies = await api.searchCompanies({
  keywords: 'fintech startup',
  industries: [INDUSTRIES.FINANCIAL_SERVICES],
  minItems: 20,     // Keep retrying until 20+ companies found
  maxRetries: 6
});
```

### Post Search with minItems  
```javascript
// Collect many posts for content analysis
const posts = await api.searchPosts({
  keywords: 'artificial intelligence',
  datePosted: 'past-week',
  minItems: 25,     // Need at least 25 posts
  maxRetries: 8
});
```

### Disable Retry Logic
```javascript
// For exact pagination control, disable retries
const exactPage = await api.searchProfiles({
  keywords: 'manager',
  start: '20',
  minItems: 0       // Disable retries - return exact page results
});
```

**Why use minItems?**
- LinkedIn has many private profiles/companies that appear in counts but don't return data
- Results can be sparse across different pages
- Perfect for data collection, analysis, or when you need substantial sample sizes
- Automatically handles pagination gaps and empty result pages

### Advanced Usage

```javascript
// Access raw data when needed
const profile = await api.getProfile('username');
console.log(profile.raw); // Original API response

// Use the low-level endpoint method
const response = await api.callEndpoint('get_profile_posts', {
  username: 'satyanadella',
  start: '0'
});

// Cache management
api.clearCache(); // Clear all cached data
api.enableCache = false; // Disable caching

// Batch operations
const usernames = ['billgates', 'satyanadella', 'sundarpichai'];
const profiles = await Promise.all(
  usernames.map(username => api.getProfile(username))
);

profiles.forEach(profile => {
  console.log(`${profile.getFullName()} - ${profile.getHeadline()}`);
});
```

## API Reference

### Main Class: LinkedInAPI

```javascript
const api = new LinkedInAPI(rapidApiKey, rapidApiHost?)
```

| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `searchProfiles(params)` | `{keywords?, geo?, company?, start?, minItems?, maxRetries?}` | `ProfileSearchResult` | Search LinkedIn profiles with smart retry |
| `getProfile(username)` | `string` | `LinkedInProfile` | Get full profile data |
| `searchCompanies(params)` | `{keywords?, industries?, companySizes?, minItems?, maxRetries?}` | `CompanySearchResult` | Search LinkedIn companies with smart retry |
| `getCompany(username)` | `string` | `LinkedInCompany` | Get full company data |
| `searchPosts(params)` | `{keywords?, author?, datePosted?, minItems?, maxRetries?}` | `PostSearchResult` | Search LinkedIn posts with smart retry |
| `getPost(postId)` | `string` | `LinkedInPost` | Get full post data |
| `clearCache()` | - | `void` | Clear all cached data |
| `validateParameters(endpoint, params)` | `string, object` | `ValidationResult` | Validate parameters |

### Entity Classes

#### LinkedInProfile
| Method | Returns | Description |
|--------|---------|-------------|
| `getFullName()` | `string` | Combined first + last name |
| `getHeadline()` | `string` | Professional headline |
| `getCurrentPosition()` | `Position \| null` | Current job position |  
| `getAllPositions()` | `Position[]` | All work experience |
| `getEducation()` | `Education[]` | Education history |
| `getSkills()` | `Skill[]` | Skills list |
| `getLinkedInUrl()` | `string` | Full LinkedIn profile URL |
| `getLocation()` | `Location` | Geographic location |
| `getProfilePictureUrl(size?)` | `string` | Profile picture URL |

#### LinkedInCompany  
| Method | Returns | Description |
|--------|---------|-------------|
| `getName()` | `string` | Company name |
| `getFollowerCount()` | `number` | LinkedIn followers |
| `getEmployeeCount()` | `number` | Employee count |
| `getLinkedInUrl()` | `string` | Full LinkedIn company URL |
| `getDescription()` | `string` | Company description |
| `getSpecialties()` | `string[]` | Company specialties |
| `getIndustries()` | `string[]` | Industry categories |
| `getWebsite()` | `string` | Company website |

#### LinkedInPost
| Method | Returns | Description |
|--------|---------|-------------|
| `getText()` | `string` | Post content text |
| `getLikeCount()` | `number` | Number of likes |  
| `getCommentCount()` | `number` | Number of comments |
| `getShareCount()` | `number` | Number of shares |
| `getAuthor()` | `Author` | Post author info |
| `getPostedAt()` | `Date` | Post publication date |
| `getTotalEngagement()` | `number` | Total engagement count |
| `hasMedia()` | `boolean` | Contains images/videos |

### Search Results Classes

All search result classes support pagination:

| Method | Returns | Description |
|--------|---------|-------------|
| `hasNextPage()` | `boolean` | More results available |
| `getNextPage()` | `Promise<SearchResult>` | Fetch next page |
| `items` | `SearchItem[]` | Current page items |
| `total` | `number` | Total result count |

### Search Item Data

Search results return raw data objects. Use the API methods to get full details:

```javascript
// Get full data from search results
const profiles = await api.searchProfiles({ keywords: 'engineer' });
const fullProfile = await api.getProfile(profiles.items[0].username);

const companies = await api.searchCompanies({ keyword: 'tech' });
const fullCompany = await api.getCompany(companies.items[0].username);

const posts = await api.searchPosts({ keyword: 'AI' });
const fullPost = await api.getPost(posts.items[0].urn);
```

## Error Types

| Error | When Thrown | Properties | Example Handling |
|-------|-------------|------------|------------------|
| `LinkedInError` | Base error class | `message`, `code` | Generic error handling |
| `RateLimitError` | Rate limit exceeded | `retryAfter` (Date) | Wait until retry time |
| `NotFoundError` | Resource not found/private | `message` | Handle missing data |
| `NetworkError` | Connection issues | `originalError` | Retry logic |

```javascript
try {
  const profile = await api.getProfile('username');
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Retry after: ${error.retryAfter}`);
  } else if (error instanceof NotFoundError) {
    console.log('Profile not found or private');
  }
}
```

## Helper Constants

### LOCATIONS
```javascript
LOCATIONS.US.NEW_YORK        // 105080838 (number)
LOCATIONS.US.SAN_FRANCISCO   // 102277331 (number)
LOCATIONS.US.LOS_ANGELES     // 102448103 (number)
// ... more cities available
```

### COMPANY_SIZES  
```javascript
COMPANY_SIZES.SELF_EMPLOYED  // 'A' (Self-employed)
COMPANY_SIZES.TINY          // 'B' (1-10 employees)
COMPANY_SIZES.SMALL         // 'C' (11-50 employees)
COMPANY_SIZES.MEDIUM        // 'D' (51-200 employees)
COMPANY_SIZES.LARGE         // 'E' (201-500 employees)
COMPANY_SIZES.XLARGE        // 'F' (501-1000 employees)
COMPANY_SIZES.ENTERPRISE    // 'G' (1001-5000 employees)
COMPANY_SIZES.MASSIVE       // 'H' (5001-10000 employees)
COMPANY_SIZES.MEGA          // 'I' (10000+ employees)
```

### INDUSTRIES
```javascript
INDUSTRIES.TECHNOLOGY         // 96
INDUSTRIES.FINANCIAL_SERVICES // 43
INDUSTRIES.HEALTHCARE         // 14
INDUSTRIES.RETAIL             // 27
// ... more industries available
```

## Troubleshooting

### Common Issues & Solutions

| Problem | Cause | Solution |
|---------|-------|----------|
| **"Invalid username"** | Username format incorrect | Use LinkedIn username (from URL) not display name |
| **"Rate limit exceeded"** | Too many requests | Implement delays, use caching, handle RateLimitError |
| **"Profile not found"** | Private profile or invalid username | Handle NotFoundError gracefully |
| **Empty search results** | Overly specific criteria | Broaden search parameters |
| **Profile search returns 0 items** | Private profiles in result set | Enable auto-retry (default) or use debug mode |
| **Slow performance** | Not using caching | Enable caching (default) or clear periodically |
| **Memory issues** | Large dataset processing | Process in batches, clear cache regularly |

### Best Practices

| Practice | Why | Example |
|----------|-----|---------|
| **Use helper constants** | Avoid magic strings | `geo: [LOCATIONS.US.NYC]` not `geo: ['105080838']` |
| **Handle errors specifically** | Better error recovery | Check for `RateLimitError`, `NotFoundError` |
| **Use entity methods** | Safe data access | `profile.getFullName()` not `profile.data.firstName` |
| **Implement delays** | Avoid rate limits | `await new Promise(r => setTimeout(r, 1000))` |
| **Load full data smartly** | Performance optimization | Search first, then load full data only when needed |

### Parameter Validation

```javascript
// Validate before making requests
const validation = api.validateParameters('search_people', {
  keywords: 'engineer'
});

if (!validation.valid) {
  console.log('Missing:', validation.missingRequired);
  console.log('Extra:', validation.extraParams);
}
```

### Profile Search & Private Profiles

LinkedIn profile searches may show high total counts but return 0 items due to private profiles. The package automatically handles this:

```javascript
// Automatic retry (default behavior)
const results = await api.searchProfiles({ keywords: 'engineer' });
// Will try different start positions automatically

// Debug what's happening
api.setDebugMode(true);
const debugResults = await api.searchProfiles({ keywords: 'ceo' });
// Logs: "Profile search attempt 1, start=0: itemsCount=0"
//       "Profile search attempt 2, start=10: itemsCount=2" ✅

// Manual control
const exactResults = await api.searchProfiles({
  keywords: 'manager',
  autoRetryForPrivateProfiles: false,  // Get exact page
  start: '20'
});
```

## Configuration

The API client supports several configuration options:

```javascript
const api = new LinkedInAPI('your-key');

// Disable caching
api.enableCache = false;

// Adjust cache timeout (default: 5 minutes)
api.cacheTimeout = 10 * 60 * 1000; // 10 minutes

// Clear cache
api.clearCache();

// Enable debug mode (helpful for troubleshooting)
api.setDebugMode(true);
```

## Rate Limiting

The API includes rate limit information in responses. Handle rate limits gracefully:

```javascript
try {
  const profile = await api.getProfile('username');
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited until: ${error.retryAfter}`);
    // Implement retry logic
  }
}
```

## Additional Resources

- **[AI_USAGE_GUIDE.md](./AI_USAGE_GUIDE.md)** - Comprehensive guide optimized for AI coders with advanced patterns
- **[API_IMPLEMENTATION_GUIDE.md](./API_IMPLEMENTATION_GUIDE.md)** - Implementation details and development patterns
- **[LinkedIn Data API Documentation](https://rapidapi.com/rockapis-rockapis-default/api/linkedin-data-api)** - Original RapidAPI documentation

## Use Cases

| Scenario | Methods Needed | Pattern |
|----------|----------------|---------|
| **Recruiting** | `searchProfiles()`, `getProfile()` | Search → Filter → Load Full Data |
| **Lead Generation** | `searchCompanies()`, `getCompany()` | Search by Industry → Get Details |
| **Market Research** | `searchCompanies()`, `searchProfiles()` | Industry Analysis + Employee Insights |
| **Content Analysis** | `searchPosts()`, `getPost()` | Keyword Search → Engagement Analysis |
| **Competitive Intelligence** | `getCompany()`, `searchProfiles()` | Company Data + Employee Profiles |

## Contributing

See [API_IMPLEMENTATION_GUIDE.md](./API_IMPLEMENTATION_GUIDE.md) for implementation details and patterns.

## License

MIT