# Rynora API LinkedIn people, organizations, jobs and contact details, as JSON - and change tracking over time. Base URL: https://api.rynora.dev ## What this API offers - **Live API - Person** (9 endpoints) - fetch a person from LinkedIn now, or a list of them in one batch: profile, employment history, endorsements, interests, similar people - **Live API - Organization** (10 endpoints) - fetch a company now, or a list of them in one batch: profile, headcount, similar and affiliated pages, its posts and job openings - **Live API - Activity** (5 endpoints) - posts, reactions and replies - **Live API - Opportunity** (5 endpoints) - job postings, the hiring team behind one, and jobs a person has posted - **Live API - Discover** (3 endpoints) - search LinkedIn itself for people, companies and jobs not yet collected - **Live API - Contact** (5 endpoints) - work email and phone: find from a name and domain, or look up who an address or number belongs to - **Watchlist** (9 endpoints) - track people and companies over time: poll them on a schedule and get a structured diff of what changed - job moves, headline edits, new posts, headcount - read by API or emailed to subscribers. This is the only family that answers what changed rather than what is true now ## Authentication Every endpoint except the two unsubscribe links needs an API key: X-API-Key: lsk_... Accounts are created by invitation. Accept yours at https://rynora.dev/app, sign in, and create a key from the Keys panel. A key is shown once and stored only as a hash, so it cannot be recovered later. The same panel shows your credit balance and spending history. Without an invitation, ask for one - there is no self-service sign-up. ## Response envelope Every response under /api uses the same wrapper. Read `success` first, then `data`. On failure `data` is null and `message` says what went wrong. { "success": true, "statusCode": 200, "message": "OK", "errors": null, "data": { ... } } Note: `statusCode` in the body is the meaningful one. A well-formed request that simply found nothing returns HTTP 200 with `success: false` and a statusCode of 404 in the body, because the request itself was fine. The Watchlist endpoints are the exception and return bare JSON, not this wrapper. ## Credits Every call costs credits, returned on each response: X-Credits-Charged: 1 X-Credits-Remaining: 899 1 credit almost everything 2 credits /discover/* and subscribing an address to the watchlist 0 credits any DELETE; submitting, polling or listing batch jobs (each subject a job resolves is charged 1 as it lands) 10 credits each watchlist entity, charged when it is actually polled A new account starts with 100 free credits. Running out returns HTTP 402. A not-found is free. A lookup that runs and finds nobody (a person, company, skill, institution or job that is not in the graph - `success: false` with a 10xx/12xx code) is refunded, and the response carries `X-Credits-Refunded: true` alongside `X-Credits-Charged: 0`. So a backfill pays only for the subjects that resolve. A malformed request (a bad id, a missing filter) is not a not-found and is not refunded, and every call is metered against the rate limit whether or not its credit is returned. ## First call and every call after Every endpoint goes to LinkedIn the first time it is asked about a subject, and answers from what that fetch stored every time after. So the first `enrich` of a person takes minutes; the `employment-history`, `endorsements` and `interests` of the same person are then immediate, and so is the next `enrich` with `refresh=false`. first call seconds to minutes: a browser walks the pages afterwards milliseconds: the stored copy, until it is a month old Where you have a list rather than a name, `batch/enrich` takes the whole list at once and works through it behind the request. ## Rate limits 60 requests per minute with a key, 10 without. Exceeding it returns HTTP 429 with a `Retry-After` header. ## As an MCP server The same API is available as an MCP server at https://api.rynora.dev/mcp/ over Streamable HTTP. Connect with your API key as a bearer token; each tool is one of the endpoints below, billed identically. claude mcp add --transport http rynora https://api.rynora.dev/mcp/ \ --header "Authorization: Bearer lsk_..." ## Live API - Person One person, fetched from LinkedIn. `enrich` does the fetching; the other five are views over what it returned, so call `enrich` first and the rest are free. `batch/enrich` is the same fetch over a list of up to 1000, worked through behind the request. ### POST /api/v2/person/batch/enrich Submit a batch person-enrich job Cost: 0 credits Enrich a list of people in the background. The batch form of `enrich`: accepts entityIds and handles, returns at once with a job token, and resolves them one at a time behind the request. Poll the GET on this path to stream results as they land. One credit per profile that resolves, charged as the worker gets to it; a not-found is free; submitting and polling are free. JSON body: ids (array, optional) - Stable identifiers: an entityId (ACoAA...) or object id for a person, a numeric organization id or object id for an organization. handles (array, optional) - Public handles: the slug after /in/ for a person, after /company/ for an organization. Example: curl -X POST 'https://api.rynora.dev/api/v2/person/batch/enrich' \ -H 'X-API-Key: $RYNORA_KEY' \ -H 'Content-Type: application/json' \ -d '{"handles": ["jane-doe-example", "john-doe-example"]}' ### GET /api/v2/person/batch/enrich Track a batch job and stream its results Cost: 0 credits Job progress plus the next page of resolved profiles. Free to poll: each profile was charged as it resolved, so read as often as the rate limit allows. `credits_charged` reports the running total. Required parameters: job_id - The token returned by the POST Optional parameters: after - How many results you have already taken. Start at 0, then pass back the next_cursor from your previous poll to receive only what is new. Example: curl 'https://api.rynora.dev/api/v2/person/batch/enrich?job_id=job_the-token-from-the-POST' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/person/batch/jobs List your batch jobs Cost: 0 credits Every person batch job this key's account has submitted, newest first, with its token - for when the 202 was not kept. Free. Jobs are deleted six hours after submission, so this is a recent-history view, not an archive. Optional parameters: status - Only jobs in this state. Omit for all. Example: curl 'https://api.rynora.dev/api/v2/person/batch/jobs' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/person/employment-history Get full employment history Cost: 1 credit Every position with its dates, plus the tenure arithmetic a caller would otherwise have to redo from prose date strings. Required parameters: entityId - The person's opaque LinkedIn token (ACoAA...). Get one from /person/resolve-handle. Preferred over a handle because it survives the person renaming their profile. Example: curl 'https://api.rynora.dev/api/v2/person/employment-history?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "entityId": "string", "handle": "string", "id": "string", "linkedin_url": "string", "positions": [ { "company": "string", "company_url": "string", "description": "string", "duration": "string", "end_date": "string", "end_year": "string|null", "is_current": "boolean", "location": "string", "start_date": "string", "start_year": "integer", "title": "string" } ], "summary": { "current_positions": "integer", "earliest_year": "integer", "employer_count": "integer", "position_count": "integer" } } ### GET /api/v2/person/endorsements Get recommendations / endorsements Cost: 1 credit Recommendations written for this person, with their authors. Required parameters: entityId - The person's opaque LinkedIn token (ACoAA...). Get one from /person/resolve-handle. Preferred over a handle because it survives the person renaming their profile. Example: curl 'https://api.rynora.dev/api/v2/person/endorsements?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "entityId": "string", "handle": "string", "id": "string", "linkedin_url": "string", "recommendations": [ "" ] } ### GET /api/v2/person/enrich Enrich a person (full profile) Cost: 1 credit The complete profile for one person, fetched live. Required - provide exactly one of these: entityId - Opaque ACoAA... token handle - Public profile slug Optional parameters: refresh - Fetch from LinkedIn now. Set false to accept the stored copy, which answers in milliseconds instead of minutes. Example: curl 'https://api.rynora.dev/api/v2/person/enrich?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "certifications": [ "" ], "courses": [ "" ], "education": [ { "degree": "string", "description": "string|null", "end_date": "string", "field_of_study": "string|null", "institution": { "id": "string|null", "linkedin_url": "string|null", "name": "string" }, "start_date": "string" } ], "entityId": "string", "handle": "string", "headline": "string", "honors": [ "" ], "id": "string", "languages": [ "string" ], "last_updated": "string", "linkedin_url": "string", "location": "string|null", "name": "string", "open_to_work": "boolean", "positions": [ { "description": "string", "duration": "string|null", "end_date": "string", "is_current": "boolean", "location": "string|null", "organization": { "id": "string|null", "linkedin_url": "string|null", "name": "string", "slug": "string|null" }, "start_date": "string", "title": "string" } ], "profile_picture_url": "string", "projects": [ "" ], "publications": [ "" ], "recommendation_count": "integer", "skills": [ "string" ], "summary": "string", "volunteering": [ "" ] } ### GET /api/v2/person/interests Get a person's interests Cost: 1 credit What the person follows, grouped by the kind of thing it is. Grouped rather than returned flat because the categories answer different questions - the companies somebody follows and the newsletters they read are not the same signal. Required parameters: entityId - The person's opaque LinkedIn token (ACoAA...). Get one from /person/resolve-handle. Preferred over a handle because it survives the person renaming their profile. Example: curl 'https://api.rynora.dev/api/v2/person/interests?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "categories": [ "string" ], "count": "integer", "entityId": "string", "handle": "string", "id": "string", "interests": { "companies": [ { "linkedin_url": "string|null", "name": "string" } ] }, "linkedin_url": "string" } ### GET /api/v2/person/lookalikes Get similar people Cost: 1 credit Profiles LinkedIn shows alongside this one. Required parameters: entityId - The person's opaque LinkedIn token (ACoAA...). Get one from /person/resolve-handle. Preferred over a handle because it survives the person renaming their profile. Example: curl 'https://api.rynora.dev/api/v2/person/lookalikes?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "entityId": "string", "handle": "string", "id": "string", "linkedin_url": "string", "results": [ { "degree": "string", "headline": "string", "linkedin_url": "string", "name": "string" } ] } ### GET /api/v2/person/resolve-handle Resolve a handle to an entityId Cost: 1 credit Turn a public handle into the stable entityId. The entityId is worth the round trip because it is the one identifier that survives the person renaming their profile. Required parameters: handle - The slug from a profile URL Example: curl 'https://api.rynora.dev/api/v2/person/resolve-handle?handle=jane-doe-example' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "entityId": "string", "handle": "string", "id": "string", "linkedin_url": "string" } ## Live API - Organization One company, fetched from LinkedIn. Same shape as the Person family: `enrich` fetches, the rest read, and `batch/enrich` takes a list. ### GET /api/v2/organization/activities Get an organization's activity (posts) Cost: 1 credit Paging is free after the first fetch: the whole feed is stored and paged locally, as it is for a person. Required parameters: id - The organization's numeric LinkedIn page id, e.g. 1000000002. Get one from /organization/resolve-slug. This is the same id a person's positions carry, so it can be passed straight across. Optional parameters: start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. count - How many results to return in this page. Example: curl 'https://api.rynora.dev/api/v2/organization/activities?id=1000000002' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "hasMore": "boolean", "id": "string", "linkedin_url": "string", "name": "string", "nextCursor": "string", "objectId": "string", "posts": [ { "article_url": "string|null", "comments_count": "integer", "image_urls": [ "" ], "linkedin_url": "string", "posted_date": "string", "reactions_count": "integer", "reposts_count": "integer", "text": "string", "urn": "string", "video_url": "string|null" } ], "slug": "string", "start": "integer", "total": "integer" } ### GET /api/v2/organization/affiliated Get affiliated pages Cost: 1 credit Subsidiaries, regional pages and showcase pages. Most companies have none, so an empty list is the ordinary answer - the two kinds are reported separately because LinkedIn treats them as different things. Required parameters: id - The organization's numeric LinkedIn page id, e.g. 1000000002. Get one from /organization/resolve-slug. This is the same id a person's positions carry, so it can be passed straight across. Example: curl 'https://api.rynora.dev/api/v2/organization/affiliated?id=1000000002' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "affiliated_companies": [ "" ], "count": "integer", "id": "string", "linkedin_url": "string", "name": "string", "objectId": "string", "showcase_pages": [ "" ], "slug": "string" } ### POST /api/v2/organization/batch/enrich Submit a batch organization-enrich job Cost: 0 credits Enrich a list of organizations in the background. The batch form of `enrich`: accepts numeric ids and slugs, returns at once with a job token, and resolves them one at a time behind the request. One credit per organization that resolves; a not-found is free; submitting and polling are free. JSON body: ids (array, optional) - Stable identifiers: an entityId (ACoAA...) or object id for a person, a numeric organization id or object id for an organization. handles (array, optional) - Public handles: the slug after /in/ for a person, after /company/ for an organization. Example: curl -X POST 'https://api.rynora.dev/api/v2/organization/batch/enrich' \ -H 'X-API-Key: $RYNORA_KEY' \ -H 'Content-Type: application/json' \ -d '{"handles": ["example-corp", "example-inc"]}' ### GET /api/v2/organization/batch/enrich Track a batch job and stream its results Cost: 0 credits Job progress plus the next page of resolved organizations. A token from the person batch endpoint returns 404 here: it is a different job, and answering it in this shape would be worse than not answering. Required parameters: job_id - The token returned by the POST Optional parameters: after - How many results you have already taken. Start at 0, then pass back the next_cursor from your previous poll to receive only what is new. Example: curl 'https://api.rynora.dev/api/v2/organization/batch/enrich?job_id=job_the-token-from-the-POST' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/organization/batch/jobs List your batch jobs Cost: 0 credits Every organization batch job this key's account has submitted, newest first, with its token - for when the 202 was not kept. Free. Jobs are deleted six hours after submission, so this is a recent-history view, not an archive. Optional parameters: status - Only jobs in this state. Omit for all. Example: curl 'https://api.rynora.dev/api/v2/organization/batch/jobs' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/organization/enrich Enrich an organization (full profile) Cost: 1 credit The complete company record for a numeric id. Required parameters: id - The numeric organization id Example: curl 'https://api.rynora.dev/api/v2/organization/enrich?id=1000000002' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "affiliated_companies": [ "" ], "company_size": "string", "cover_image_url": "string|null", "crunchbase_url": "string|null", "description": "string", "follower_count": "string|null", "founded": "string", "headcount": "integer", "headquarters": { "city": "string", "country": "string|null", "country_code": "string|null", "line": "string", "region": "string" }, "id": "string", "industries": [ "string" ], "last_updated": "string", "linkedin_url": "string", "logo_url": "string", "name": "string", "objectId": "string", "phone": "string|null", "showcase_pages": [ "" ], "slug": "string", "source": "string", "specialities": [ "" ], "tagline": "string|null", "type": "string|null", "website": "string" } ### GET /api/v2/organization/headcount Get organization headcount data Cost: 1 credit Employee count and the size bucket LinkedIn publishes. The per-function distribution the contract mentions is not here: LinkedIn puts it behind Sales Navigator's insights, not on the public page, so reporting it would mean inventing it. Required parameters: id - The organization's numeric LinkedIn page id, e.g. 1000000002. Get one from /organization/resolve-slug. This is the same id a person's positions carry, so it can be passed straight across. Example: curl 'https://api.rynora.dev/api/v2/organization/headcount?id=1000000002' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "distribution": "string|null", "distribution_note": "string", "employee_count": "integer", "id": "string", "last_updated": "string", "linkedin_url": "string", "name": "string", "objectId": "string", "size_range": "string", "slug": "string" } ### GET /api/v2/organization/opportunities Get an organization's opportunities (jobs) Cost: 1 credit Open postings across one or more organizations. Required parameters: organizationEntityIds - Comma-separated ids Optional parameters: start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. count - How many results to return in this page. refresh - Example: curl 'https://api.rynora.dev/api/v2/organization/opportunities?organizationEntityIds=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/organization/resolve-slug Resolve a slug to an organization id Cost: 1 credit Turn a public slug into the numeric id the other endpoints want. Costs a fetch the first time and nothing afterwards. Required parameters: slug - The segment after /company/ Example: curl 'https://api.rynora.dev/api/v2/organization/resolve-slug?slug=example-corp' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "id": "string", "linkedin_url": "string", "name": "string", "objectId": "string", "slug": "string" } ### GET /api/v2/organization/similar Get similar organizations Cost: 1 credit Companies LinkedIn shows beside this one - its peer rail. Required parameters: id - The organization's numeric LinkedIn page id, e.g. 1000000002. Get one from /organization/resolve-slug. This is the same id a person's positions carry, so it can be passed straight across. Example: curl 'https://api.rynora.dev/api/v2/organization/similar?id=1000000002' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "id": "string", "linkedin_url": "string", "name": "string", "objectId": "string", "results": [ { "followers": "string", "linkedin_url": "string", "name": "string" } ], "slug": "string" } ## Live API - Activity Posts, reactions and replies. ### GET /api/v2/activity/details Get details of a single activity Cost: 1 credit Full content and engagement for one post. Required parameters: entityId - The activity urn Optional parameters: refresh - Example: curl 'https://api.rynora.dev/api/v2/activity/details?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/activity/feed Get a person's activity feed Cost: 1 credit Recent posts and shares by one person. Required parameters: entityId - The person's ACoAA... token Optional parameters: cursor - Opaque pagination token. Omit for the first page, then pass the next_cursor from the previous response. Treat it as a black box: it encodes a position, not a page number. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. count - How many results to return in this page. Example: curl 'https://api.rynora.dev/api/v2/activity/feed?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "entityId": "string", "hasMore": "boolean", "nextCursor": "string", "posts": [ { "article_url": "string|null", "comments_count": "integer", "image_urls": [ "" ], "linkedin_url": "string", "posted_date": "string", "reactions_count": "integer", "reposts_count": "string|null", "text": "string", "urn": "string", "video_url": "string|null" } ], "start": "integer", "total": "integer" } ### GET /api/v2/activity/reactions Get reactions on an activity Cost: 1 credit The people behind a post's reaction count. `start` moves in tens because that is how LinkedIn pages the reactions modal; a value in between would silently land on a different page than the caller asked for, so it is rejected instead. Required parameters: entityId - The activity urn Optional parameters: start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. refresh - Example: curl 'https://api.rynora.dev/api/v2/activity/reactions?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/activity/replies Get replies (comments) on an activity Cost: 1 credit The conversation under a post. LinkedIn renders comments in its own relevance order and hides the rest behind "Load more comments", so `date_posted` reverses what was rendered rather than re-sorting by a timestamp we do not reliably have - the cards carry "2w", not a date. Required parameters: entityId - The activity urn Optional parameters: sortBy - Result order. relevance is LinkedIn's own ranking; date_posted puts the newest first. count - How many results to return in this page. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. refresh - Example: curl 'https://api.rynora.dev/api/v2/activity/replies?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/activity/replies/by-author Get replies written by a person Cost: 1 credit Comments this person wrote, across LinkedIn - not the comments on one of their posts. Required parameters: entityId - The person's ACoAA... token Optional parameters: cursor - Opaque pagination token. Omit for the first page, then pass the next_cursor from the previous response. Treat it as a black box: it encodes a position, not a page number. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. count - How many results to return in this page. refresh - Example: curl 'https://api.rynora.dev/api/v2/activity/replies/by-author?entityId=ACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "count": "integer", "entityId": "string", "hasMore": "boolean", "nextCursor": "string", "replies": [ { "author_headline": "string", "author_name": "string", "author_url": "string", "post_author": "string|null", "post_urn": "string", "posted": "string", "reactions_count": "string|null", "text": "string", "urn": "string|null" } ], "source": "string", "start": "integer", "total": "integer" } ## Live API - Opportunity Job postings, the teams hiring for them, and who has posted them. ### GET /api/v2/opportunity/by-person Get opportunities posted by a person Cost: 1 credit Postings where this person is on the hiring team. A reverse lookup over jobs already fetched, because LinkedIn publishes no "jobs posted by X" page. So this answers "which postings we hold name this person", which grows with the corpus rather than describing everything they have ever posted - the same shape as the employees endpoint. Required parameters: personEntityId - The poster's ACoAA... token Optional parameters: count - How many results to return in this page. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. Example: curl 'https://api.rynora.dev/api/v2/opportunity/by-person?personEntityId=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/opportunity/details Get opportunity (job) details Cost: 1 credit The full posting: title, description, company, location, employment type and apply metadata. Required parameters: opportunityEntityId - The numeric job id Example: curl 'https://api.rynora.dev/api/v2/opportunity/details?opportunityEntityId=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/opportunity/hiring-team Get the hiring team for an opportunity Cost: 1 credit The recruiters and hiring managers attached to a posting. Required parameters: opportunityEntityId - The numeric job id, the digits in linkedin.com/jobs/view/. Get one from /discover/opportunities or /organization/opportunities. Example: curl 'https://api.rynora.dev/api/v2/opportunity/hiring-team?opportunityEntityId=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/opportunity/related-views Get "people also viewed" opportunities Cost: 1 credit Jobs that people who viewed this posting also viewed. Reads the same rail as `similar`, because LinkedIn merged the two. The `signal` field says so rather than letting a caller believe they have a behavioural signal distinct from content similarity. Required parameters: opportunityEntityId - The numeric job id, the digits in linkedin.com/jobs/view/. Get one from /discover/opportunities or /organization/opportunities. Example: curl 'https://api.rynora.dev/api/v2/opportunity/related-views?opportunityEntityId=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/opportunity/similar Get similar opportunities Cost: 1 credit Postings LinkedIn lists alongside this one. Required parameters: opportunityEntityId - The numeric job id, the digits in linkedin.com/jobs/view/. Get one from /discover/opportunities or /organization/opportunities. Example: curl 'https://api.rynora.dev/api/v2/opportunity/similar?opportunityEntityId=value' \ -H 'X-API-Key: $RYNORA_KEY' ## Live API - Discover Search LinkedIn for people, organizations and jobs you do not have yet. The slowest family and the only one that pages through results. ### GET /api/v2/discover/opportunities Discover opportunities (jobs) Cost: 2 credits Search jobs by keyword and filters. Everything else pages from one fetch. Optional parameters: keyword - Free-text search term. sortBy - Result order. relevance is LinkedIn's own ranking; date_posted puts the newest first. datePosted - Restrict to postings from the last 24h, 1week or 1month. experience - Comma-separated seniority levels: internship, entry_level, associate, mid_senior, director, executive. jobTypes - Comma-separated employment types: full_time, part_time, contract, temporary, internship, volunteer, other. workplaceTypes - Comma-separated: onsite, remote, hybrid. companies - Comma-separated organization ids. Get one from /organization/resolve-slug. industries - Comma-separated industry names. Partial match against the industry line on the company page. locations - Comma-separated LinkedIn geo ids. Only the first is used: LinkedIn's job search takes a single geo, and sending several returns results for none of them. functions - Comma-separated LinkedIn job-function codes. titles - Comma-separated LinkedIn title ids. easyApply - true returns only postings that can be applied to without leaving LinkedIn. verifiedJob - true returns only postings LinkedIn has verified. count - How many results to return in this page. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. refresh - Example: curl 'https://api.rynora.dev/api/v2/discover/opportunities' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/discover/organizations Discover organizations Cost: 2 credits Search companies by keyword, size, industry and headquarters. Required parameters: keyword - Free-text search term. Optional parameters: headcountRange - Comma-separated size buckets: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+. industry - Comma-separated industries. Partial match. geoEntityId - Comma-separated LinkedIn geo ids for the headquarters. count - How many results to return in this page. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. refresh - Example: curl 'https://api.rynora.dev/api/v2/discover/organizations?keyword=software%20engineer' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/discover/people Discover people Cost: 2 credits Search people by keyword and structured filters. Optional parameters: keyword - Free-text search term. firstName - Comma-separated first names. Combine with lastName to identify one person. lastName - Comma-separated last names. title - Job title across a person's positions. Partial and typo-tolerant. Combine with organization_slugs and current_only=true to find who holds a role at a company today. currentCompany - Organization id(s) pastCompany - Comma-separated organization ids for former employers: alumni and ex-employee sourcing. school - Comma-separated school ids. industry - Comma-separated industries. Partial match. geoEntityId - Comma-separated LinkedIn geo ids for the headquarters. profileLanguage - Comma-separated language codes, e.g. en. The language a profile is written in. count - How many results to return in this page. start - Zero-based offset into the result set. An alternative to cursor; both describe the same position. Example: curl 'https://api.rynora.dev/api/v2/discover/people' \ -H 'X-API-Key: $RYNORA_KEY' ## Live API - Contact Work email and phone lookup, forwards and in reverse. ### GET /api/v2/contact/email Find a work email from a name and company domain Cost: 1 credit Work out the most likely work email for someone, and say how sure it is. Company mail follows a house pattern, so this builds the addresses that pattern could produce for the name given (`first.last@`, `flast@`, `first@`, and so on) and asks the domain's mail server which of them it will accept. The conversation stops one command short of sending: no message is delivered, and the person is not contacted. **The employer comes from you, and `employment` says whether anything supports it.** This endpoint builds an address at the domain you name; it cannot know on its own that the person works there. So `confidence_score` answers "is this the right shape for an address at this domain", and `employment.status` answers "is this person even at this domain": * `confirmed` - an address is on file for them there. * `contradicted` - they are on file at a different employer. The score is capped hard, because a well-shaped address for the wrong company looks exactly like a good answer otherwise. * `unknown` - nothing on file either way. The score is capped below the high band: the address is what somebody at that domain would have, and it assumes they are there. **Read `status` before using the address.** * `valid` - the mail server accepted this address and rejected one that cannot exist, so it is discriminating between real and fake recipients. This is the only status that means the mailbox was confirmed. * `catch_all` - the domain accepted an address made of random hex, so it accepts everything. The address returned is the most common pattern and is unconfirmed. * `unverifiable` - the question could not be asked, or the answer means nothing. Outbound port 25 blocked, greylisting, or a provider such as Microsoft 365 that answers 250 for every recipient. `reason` says which. * `invalid` - every candidate was rejected, or the domain has no MX record at all and cannot receive mail. `confidence` compresses that to `high`, `low`, `guess` or `none` for callers who want one field rather than four. Anything found there comes back under `stored`, and becomes the primary answer only when the probe could not produce a verified one. Required parameters: first_name - The person's given name, such as Jane. Accents are folded to ASCII and honorifics (Dr., Prof.) are stripped, so pass it as written on their profile. last_name - The person's family name, such as Doe. Hyphenated names are kept whole (muller-schmidt), and post-nominals (PhD, Jr.) are stripped. domain - The employer's mail domain, such as example.com. This is the domain their email is on, which is often not the domain their website is on - check the MX record if unsure. Do not pass a LinkedIn slug or a full URL. Optional parameters: verify - true asks the receiving mail server whether each candidate exists, which is what separates a verified address from a guess. false skips the network entirely and returns the most common pattern, which is fast and free but unconfirmed. Example: curl 'https://api.rynora.dev/api/v2/contact/email?first_name=Jane&last_name=Doe&domain=example.com' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "candidates": [ { "accepted": "string|null", "code": "string|null", "email": "string", "pattern": "string" } ], "confidence": "string", "confidence_factors": [ "string" ], "confidence_score": "integer", "domain": "string", "email": "string", "employment": { "employers_on_file": [ "" ], "note": "string", "status": "string" }, "first_name": "string", "full_name": "string", "house_pattern": { "agreement": "number", "confirmed": "boolean", "pattern": "string", "sample": "integer" }, "last_name": "string", "mx": [ "string" ], "pattern": "string", "probing_enabled": "boolean", "reason": "string", "source": "string", "status": "string", "stored": [ "" ] } ### POST /api/v2/contact/known Teach the finder an address you know is real Cost: 1 credit Record an address you know to be correct, and teach the domain's pattern. This is the only thing that fixes a domain nothing else can reach. Two cases defeat every other method: * **Nothing is held at the domain.** The finder falls back to how the whole population writes addresses, and the population is not this company. A domain using `first.last` gets guessed as `flast`, because `flast` is what 46% of addresses look like overall. * **The domain is catch-all.** Its mail server accepts every recipient, including addresses made of random hex, so probing cannot tell a real mailbox from an invented one. Verification returns `catch_all` and nothing is learned. One confirmed address settles both. The effect is not limited to this person. Recording one address teaches the pattern for **everybody** at that domain, so the next lookup there is scored on company evidence rather than a population average. JSON body: first_name (string, required) - The person's given name, exactly as it corresponds to the address. last_name (string, required) - The person's family name, exactly as it corresponds to the address. email (string, required) - The address you know is real, such as Jane.Doe@example.com. Case is not significant. title (string, optional) - Their job title, if you have it. Stored but not used for matching. organization (string, optional) - The employer's name, if you have it. Stored for display on lookups. Example: curl -X POST 'https://api.rynora.dev/api/v2/contact/known' \ -H 'X-API-Key: $RYNORA_KEY' \ -H 'Content-Type: application/json' \ -d '{"first_name": "Jane", "last_name": "Doe", "email": "jane.doe@example.com"}' ### GET /api/v2/contact/phone Find a phone number from a name and company domain Cost: 1 credit Phone numbers held for a person. **Unlike the email endpoint, this cannot guess.** An email address is derivable - companies use a house pattern, so `first.last@domain` is a reasonable inference from a name. A phone number has no such structure: there is no function from a name to a number, so this returns what is on file or nothing at all. A `null` here means "not held", never "does not exist". Numbers come back in E.164 (`+15555550100`) regardless of how they were written at the source, so they are directly dialable and comparable. A contact can hold more than one, in which case all are returned in the order they were recorded. When several people share a name, pass `domain` to disambiguate. Without it every match is returned and `matches` tells you how many there were - treat a result with more than one match as ambiguous rather than picking the first. Required parameters: full_name - The person's full name, as it appears on their profile. Matched case-insensitively against stored contacts after whitespace is collapsed. Optional parameters: domain - The employer's domain, used to pick the right person when several share a name. Matched against both the contact's email domain and their employer's website host. Optional: omit it to search on name alone and read the organization on each match. Example: curl 'https://api.rynora.dev/api/v2/contact/phone?full_name=value' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/contact/reverse/email Find the person behind an email address Cost: 1 credit Who an email address belongs to. A store lookup, not a live one: it answers from contacts already held and never reaches out to verify. An address we do not hold returns `found: false` - which means it is not in this store, and says nothing about whether the address is real or who owns it. `is_free_provider` marks gmail, yahoo and the like. A personal address tells you far less about somebody's employer than a work address does, and a caller routing on the organization should check this before trusting it. Required parameters: email - The address to look up. Case and surrounding whitespace are ignored; it is normalised before matching, so Jane.Doe@Example.com and jane.doe@example.com are the same query. Example: curl 'https://api.rynora.dev/api/v2/contact/reverse/email?email=jane.doe@example.com' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "domain": "string", "email": "string", "found": "boolean", "is_free_provider": "boolean", "person": "string|null" } ### GET /api/v2/contact/reverse/phone Find the person behind a phone number Cost: 1 credit Who a phone number belongs to. A store lookup over the numbers held against contacts. Both sides are normalised to E.164 first, so the format the caller happens to use does not decide whether there is a match - which is the usual reason a reverse lookup wrongly reports nothing. One number can return more than one person: shared switchboards and company main lines are recorded against everyone who listed them. All matches are returned rather than an arbitrary first, and `matches` says how many there were. Required parameters: phone - The number to look up, in any format. (555) 555-0100, 555-555-0100 and +15555550100 are the same query: it is converted to E.164 before matching. A ten-digit number with no country code is read as North American. Example: curl 'https://api.rynora.dev/api/v2/contact/reverse/phone?phone=(555)%20555-0100' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): { "found": "boolean", "matches": "integer", "people": [ "" ], "phone": "string", "shared_line": "boolean" } ## Watchlist Track people and companies over time and get the differences, by API or by email. ### POST /api/v2/watchlist Watch Add Cost: 1 credit Start watching an entity. Idempotent - re-posting the same URL reactivates it and updates the interval rather than creating a duplicate. The first poll only captures a baseline; changes are reported from the second onwards. JSON body: url (string, required) - Profile URL, company URL, or company slug kind (string, optional) - person or company; inferred if omitted interval_days (number, optional) - Default 1.0. note (string, optional) - track_activity (boolean, optional) - Also record new posts. Off by default: posts appear weekly where job moves appear every few years, so tracking everyone would bury the signal. Default false. Example: curl -X POST 'https://api.rynora.dev/api/v2/watchlist' \ -H 'X-API-Key: $RYNORA_KEY' \ -H 'Content-Type: application/json' \ -d '{"url": "https://www.linkedin.com/in/jane-doe-example/"}' ### GET /api/v2/watchlist Watch List Cost: 1 credit Everything you are watching, oldest first. Scoped to the key that calls it. An operator key, which has no owner, sees every entry regardless of who added it. Optional parameters: include_inactive - Include entities removed from the watchlist. Their change history is kept, so this shows what was once watched. Example: curl 'https://api.rynora.dev/api/v2/watchlist' \ -H 'X-API-Key: $RYNORA_KEY' ### DELETE /api/v2/watchlist Watch Remove Cost: 0 credits Stop watching one entity, or empty the watchlist. Deactivates rather than deletes: the recorded changes are the valuable part and dropping the entries would orphan them. Re-adding any URL later reactivates it with its history intact, so this is reversible. Pass `url` to stop one, or `clear=true` to stop all. Passing neither is an error rather than a no-op, and passing both is an error rather than a guess at which was meant. Scoped to your key: `clear=true` empties your own watchlist, not anybody else's. An operator key, which has no owner, is the exception and clears everything. Required - provide exactly one of these: url - The LinkedIn profile or company URL of the entity to stop watching. Omit it and pass clear=true to empty the whole watchlist instead. Optional parameters: clear - true stops watching everything you are watching. Required explicitly: a DELETE that emptied the watchlist merely because a url was left off would turn a mistyped request into a wiped list. Recorded changes are kept either way. Example: curl -X DELETE 'https://api.rynora.dev/api/v2/watchlist?url=https://www.linkedin.com/in/jane-doe-example/' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/watchlist/changes Watch Changes Cost: 1 credit Recorded changes, newest first. Required - provide exactly one of these: url - History for one entity Optional parameters: jobs_only - Only moves between employers posts_only - Only newly published posts contacts_only - Only where an email or phone became visible source - profile or activity limit - How many results to return in this page. Example: curl 'https://api.rynora.dev/api/v2/watchlist/changes?url=https://www.linkedin.com/in/jane-doe-example/' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): [ { "changes": [ { "after": { "linkedin_url": "string", "posted_date": "string", "text": "string", "urn": "string" }, "before": "string|null", "field": "string", "kind": "string", "summary": "string" } ], "count": "integer", "detected_at": "string", "entity_id": "string", "has_job_change": "boolean", "has_new_contact": "boolean", "has_new_posts": "boolean", "kind": "string", "linkedin_url": "string", "source": "string" } ] ### GET /api/v2/watchlist/confirm Confirm a subscription Cost: 1 credit. No API key required. Start receiving watchlist changes at this address. Public, for the same reason unsubscribing is: the person confirming owns the inbox, not necessarily an API key. Required parameters: token - The token from a confirmation link. Example: curl 'https://api.rynora.dev/api/v2/watchlist/confirm?token=the-token-from-your-email' ### POST /api/v2/watchlist/subscribers Email watchlist changes to an address Cost: 2 credits Send an address the changes detected on your watchlist. Every poll that finds something emails the people listening to that entity. The mail names what moved - a headline edit, a new position, a published post - and links back to the full change log. **An address receives nothing until it is confirmed.** Anyone can be typed into this endpoint, so a mistyped address would otherwise be mailed on the next poll - somebody who never asked, about people they have never heard of. Your own account address is confirmed on sight. Any other address starts pending and is sent nothing at all, not even a confirmation, unless you pass `send_confirmation=true`; otherwise the link comes back to you to pass on however you already talk to that person. Idempotent on the address: subscribing twice updates the subscription rather than sending everything twice. Each email carries an unsubscribe link that works without signing in. A notification nobody can stop is a complaint waiting to happen, and in most jurisdictions a legal problem. Costs 2 credits. The polling it causes is billed separately, per entity, when a poll actually runs. JSON body: email (string, required) - Where to send change notifications. Does not need an account on this service: the person who wants to hear that somebody changed jobs is often not the person holding the API key. urls (array, optional) - Limit notifications to these entities. Omit for everything on your watchlist. Narrowing matters more than it looks - somebody tracking twelve people does not want the whole team's watchlist in their inbox. send_confirmation (boolean, optional) - true emails the address a confirmation link. false, the default, sends them nothing and returns the link to you instead, so a mistyped address never receives mail from this service at all. Your own account address needs neither: it is confirmed on sight. Default false. Example: curl -X POST 'https://api.rynora.dev/api/v2/watchlist/subscribers' \ -H 'X-API-Key: $RYNORA_KEY' \ -H 'Content-Type: application/json' \ -d '{"email": "jane.doe@example.com"}' ### GET /api/v2/watchlist/subscribers Who is being emailed Cost: 2 credits Addresses receiving your watchlist's changes. Example: curl 'https://api.rynora.dev/api/v2/watchlist/subscribers' \ -H 'X-API-Key: $RYNORA_KEY' Returns (in `data`): [ "" ] ### DELETE /api/v2/watchlist/subscribers Stop emailing an address Cost: 2 credits Stop notifications to one address. Costs nothing, like any other stop. Required parameters: email - The address to stop emailing. Example: curl -X DELETE 'https://api.rynora.dev/api/v2/watchlist/subscribers?email=jane.doe@example.com' \ -H 'X-API-Key: $RYNORA_KEY' ### GET /api/v2/watchlist/unsubscribe Unsubscribe from an emailed link Cost: 1 credit. No API key required. Stop notifications using the link in the email. On a router without the API key requirement, because the person who wants the mail to stop is often not the person who holds a key. Required parameters: token - The token from the unsubscribe link in a notification email. Example: curl 'https://api.rynora.dev/api/v2/watchlist/unsubscribe?token=the-token-from-your-email'