Documentation
¶
Overview ¶
Package api is the LinkedIn Voyager client core. Voyager is LinkedIn's INTERNAL, unofficial web API (the same endpoints linkedin.com's SPA calls). Auth is a borrowed browser session: the li_at session cookie + the JSESSIONID cookie, with a csrf-token header derived from JSESSIONID. Plain net/http is sufficient for read Voyager — no TLS-fingerprint spoofing. The client sends the exact header set the web client sends, paces requests for ban-safety, and never retries a throttle signal.
Index ¶
- Constants
- func ParseSinceSeconds(value string) (int, error)
- type APIError
- type Bool
- type Client
- func (c *Client) Do(ctx context.Context, path string, q url.Values) (int, json.RawMessage, error)
- func (c *Client) GetCompany(ctx context.Context, slug string) (json.RawMessage, error)
- func (c *Client) GetJob(ctx context.Context, id string, now time.Time) (json.RawMessage, error)
- func (c *Client) Pacer() *Pacer
- func (c *Client) ResolveGeo(ctx context.Context, name string) ([]GeoHit, error)
- func (c *Client) SearchJobs(ctx context.Context, f SearchFilters, start, count int) (*SearchResult, error)
- func (c *Client) SearchJobsAll(ctx context.Context, f SearchFilters, count, limit int, all bool) ([]voyager.JobCard, error)
- func (c *Client) VoyagerBaseURL() string
- func (c *Client) WebBaseURL() string
- type CookieFunc
- type GeoCache
- type GeoHit
- type ID
- type Int
- type Option
- type Pacer
- type SearchFilters
- type SearchResult
- type StringOrSlice
Constants ¶
const ( // DefaultVoyagerBaseURL is the authenticated internal API base. DefaultVoyagerBaseURL = "https://www.linkedin.com/voyager/api" // DefaultWebBaseURL is the host for the unauthenticated jobs-guest geo typeahead. DefaultWebBaseURL = "https://www.linkedin.com" // DefaultUserAgent is a CURRENT desktop Chrome UA. Voyager rejects a bare Go UA, and a // STALE UA is itself a fingerprint that looks automated — keep this single constant fresh. // Overridable via LINKEDIN_USER_AGENT. DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" )
Default hosts. Both are overridable (config/flags, and tests point them at one httptest server that routes by path).
const GeoIDPlaceholder = "<GEO_ID>"
GeoIDPlaceholder stands in for a resolved geoId during a --location dry-run, where the geo typeahead can't run offline. It is kept literal (not percent-encoded) in the previewed job-search curl so a human sees locationUnion:(geoId:<GEO_ID>).
const StatusSoftBlock = 999
StatusSoftBlock is LinkedIn's non-standard "999" soft-block status. It is not a real HTTP status; LinkedIn returns it when it throttles or challenges an unofficial client. It must NEVER be retried — hammering a soft-block is exactly how an account gets flagged.
Variables ¶
This section is empty.
Functions ¶
func ParseSinceSeconds ¶
ParseSinceSeconds converts a --since window into seconds for timePostedRange:List(r<secs>). It accepts a relative shorthand Nh/Nd/Nw (e.g. 24h, 7d, 2w) or the named windows day/week/month. An empty value returns 0 (any time). The three canonical LinkedIn windows are r86400 (24h), r604800 (7d) and r2592000 (30d).
Types ¶
type APIError ¶
type APIError struct {
StatusCode int
Message string
Body []byte
// Challenge is set when LinkedIn returned a checkpoint/challenge (a security-verification
// interstitial), signalling the session needs human re-verification in a browser.
Challenge bool
}
APIError is a LinkedIn Voyager error with an actionable, ban-aware hint keyed by status.
type Bool ¶
type Bool bool
Bool accepts a real bool OR "true"/"1"/"yes". LinkedIn's workRemoteAllowed is a real bool, but some flags arrive as strings.
func (*Bool) UnmarshalJSON ¶
type Client ¶
type Client struct {
// DryRun prints the equivalent curl to DryRunOut instead of sending the request.
DryRun bool
DryRunOut io.Writer
// ShowToken reveals the session cookies in dry-run output (redacted by default).
ShowToken bool
Verbose bool
VerboseOut io.Writer
// contains filtered or unexported fields
}
Client is a LinkedIn Voyager HTTP client.
func NewClientWithBaseURL ¶
NewClientWithBaseURL points both hosts at the same base URL. Tests drive every endpoint against one httptest server (routing by path); it also backs a single-host --base-url override.
func (*Client) Do ¶
Do sends one raw authenticated Voyager request and returns status, headers, and body — the escape hatch behind `linkedin api`. Only GET is ever sent for read Voyager; method is validated by the caller. A dry-run returns status 0.
func (*Client) GetCompany ¶
GetCompany fetches an organization by its universalName (the slug in a company URL, linkedin.com/company/<slug>) and returns the trustworthy Company entity's raw JSON.
func (*Client) GetJob ¶
GetJob fetches one job posting's full detail and returns the trustworthy JobPosting entity's raw JSON. It CHARGES the daily job-detail cap first (ban-safety): if today's budget is spent, it refuses rather than fetching. now is injected for deterministic tests.
func (*Client) ResolveGeo ¶
ResolveGeo resolves a location name to LinkedIn geo hits via the unauthenticated jobs-guest typeahead. It returns every hit (the first is the best match). "remote" is NOT a geo — it is a workplaceType filter — so callers must not route "remote" through here.
func (*Client) SearchJobs ¶
func (c *Client) SearchJobs(ctx context.Context, f SearchFilters, start, count int) (*SearchResult, error)
SearchJobs runs one page of a job search (start/count) and returns the parsed page + raw JSON.
func (*Client) SearchJobsAll ¶
func (c *Client) SearchJobsAll(ctx context.Context, f SearchFilters, count, limit int, all bool) ([]voyager.JobCard, error)
SearchJobsAll walks pages until it collects `limit` cards (limit<=0 && !all → one page), pacing every request via the ban-safety Pacer. It de-duplicates by job id as a safety net.
func (*Client) VoyagerBaseURL ¶
VoyagerBaseURL returns the resolved Voyager host.
func (*Client) WebBaseURL ¶
WebBaseURL returns the resolved web (typeahead) host.
type CookieFunc ¶
CookieFunc supplies the borrowed session cookies per request: li_at (session) and JSESSIONID (whose value looks like `"ajax:1234567890"` WITH the surrounding quotes). It may be nil for the unauthenticated typeahead path.
type GeoCache ¶
type GeoCache struct {
// contains filtered or unexported fields
}
GeoCache is a name→geoId cache persisted in the config dir, so a repeated --location resolves without another typeahead round-trip (fewer requests = safer). Keys are lowercased names.
func NewGeoCache ¶
NewGeoCache loads (or starts) the cache at dir/geocache.json.
type ID ¶
type ID string
ID unmarshals from a JSON string OR number and always marshals as a string, so ids render consistently and never lose precision above 2^53.
func (ID) MarshalJSON ¶
func (*ID) UnmarshalJSON ¶
type Int ¶
type Int int64
Int accepts a JSON number OR a numeric string, decoding int64 before float64 so ids above 2^53 keep precision. NaN/Inf and malformed numbers are rejected.
func (*Int) UnmarshalJSON ¶
type Option ¶
type Option func(*Client)
Option configures a Client.
func WithCookies ¶
func WithCookies(f CookieFunc) Option
WithCookies sets the borrowed-session cookie source.
func WithDryRun ¶
WithDryRun enables curl-printing mode.
func WithHTTPClient ¶
WithHTTPClient overrides the HTTP transport (tests point it at httptest servers).
func WithMaxRetries ¶
WithMaxRetries overrides the transient-error retry budget (tests set 0 for speed).
func WithPacer ¶
WithPacer installs the ban-safety pacer. Without one, requests are not paced (tests default to no pacer for speed).
func WithUserAgent ¶
WithUserAgent overrides the default User-Agent (empty keeps the default).
type Pacer ¶
type Pacer struct {
MinDelay time.Duration // lower bound of the inter-request jitter
MaxDelay time.Duration // upper bound of the inter-request jitter
PerRunCap int // max live requests in one process run (0 = unlimited)
DailyCap int // max job-detail fetches charged per calendar day (0 = unlimited)
StatePath string // JSON file persisting the daily counter (config/state dir)
// contains filtered or unexported fields
}
Pacer enforces the ban-safety posture that is ON BY DEFAULT (not an option): one request in flight at a time, a jittered human-paced delay between requests, a per-run request cap, and a persisted per-DAY cap on job-detail fetches. These defaults exist because this CLI drives LinkedIn's unofficial Voyager API against a real personal account — the biggest risk isn't a bug, it's an account restriction from looking automated. See DECISIONS.md.
func DefaultPacer ¶
DefaultPacer returns the shipped ban-safety defaults: a 3–15s jittered delay, no per-run cap, and ~30 job-detail fetches/day. statePath is where the daily counter persists.
func (*Pacer) ChargeDaily ¶
ChargeDaily records one job-detail fetch against today's cap, persisting the counter. It returns an error (and does NOT increment) when the cap is already reached, so `jobs get` refuses to exceed the daily budget rather than silently blowing past it. now is injected for deterministic tests.
func (*Pacer) DailyRemaining ¶
DailyRemaining reports how many job-detail fetches are left today (for doctor/status).
func (*Pacer) Wait ¶
Wait blocks for a jittered delay before a live request and enforces the per-run cap. It is a no-op the FIRST time (no delay before the first request of a run) so a single command stays snappy; the pacing applies between successive requests. Returns an error if the per-run cap is exceeded, so a runaway loop stops rather than pounding LinkedIn.
type SearchFilters ¶
type SearchFilters struct {
Keywords string // free-text role/skill match
GeoID string // resolved location geoId (see ResolveGeo); "" = anywhere
Remote bool // → workplaceType:List(2)
SinceSecs int // → timePostedRange:List(r<secs>); 0 = any time
JobType []string // → jobType:List(F,C,…)
Experience []string // → experience:List(2,3,…)
}
SearchFilters are the user-facing job-search filters. They compile into LinkedIn's Rest.li `query=(...)` blob (see buildSearchQuery). Zero-valued fields are omitted.
type SearchResult ¶
type SearchResult struct {
*voyager.SearchResult
Raw json.RawMessage
}
SearchResult is one parsed page plus the raw envelope (for -o json).
type StringOrSlice ¶
type StringOrSlice []string
StringOrSlice accepts a single string OR an array of strings — LinkedIn's workplaceTypes is an array, but adjacent fields are sometimes a scalar.
func (*StringOrSlice) UnmarshalJSON ¶
func (s *StringOrSlice) UnmarshalJSON(b []byte) error