# Lempile Docs > Guides and API reference for Lempile's APIs: Lempile Direct (a LinkedIn API on Unipile's paths, fields and errors), the Google Search API (SerpApi-compatible), Email Finder and People Search. Every page is plain Markdown at its own address plus `.md`. --- # Lempile Direct > Unipile's LinkedIn API paths, fields and errors, served by Lempile from each account's own browser session, without a browser per call. Lempile Direct is a LinkedIn API that answers Unipile's paths, fields, status codes and error types, at `https://direct.lempile.com/api/v1`, without opening a browser for each call. A client written for Unipile points at Direct by changing two things: its base URL and its API key. Anything Direct doesn't do answers Unipile's own `501 errors/feature_not_implemented`, so such a client can fall back to Unipile for that call. ```text your app -> Direct (X-API-KEY) -> the account's own session and proxy -> LinkedIn's own API ``` ## How It Works - Every LinkedIn account Direct serves is a Lempile account: a real browser on Lempile's browser server, signed in to LinkedIn, with its own proxy. - Direct reads that browser's LinkedIn session (cookies, user agent, client hints) through a signed internal route, keeps it encrypted for up to 6 hours, and reads it again when LinkedIn refuses it. - Each call goes to LinkedIn's own web API through the account's own proxy, with the connection fingerprint of the account's Chrome version. LinkedIn sees the same browser, the same address and the same session it always sees from that account. - Calls are paced per account: one at a time, 1.5 to 3 seconds apart, at most 1,500 a day, and a 15-minute rest when LinkedIn says "too many". - Answers are LinkedIn's data reshaped into Unipile's objects (`UserProfile`, `Chat`, `Message`, `Post`…). The test suite checks the read answers against Unipile's published response schemas. ## 60-Second Quickstart ```bash export DIRECT_API_KEY="your-api-key" # The LinkedIn accounts your key can use. curl -sS "https://direct.lempile.com/api/v1/accounts" \ -H "X-API-KEY: $DIRECT_API_KEY" # Who an account is on LinkedIn. curl -sS "https://direct.lempile.com/api/v1/users/me?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` The [Quick Start](https://lempile.com/docs/direct/quick-start) connects an account, sets up a webhook and tries a first write without sending it. ## What It Covers | Area | Routes | | --- | --- | | Accounts | list, retrieve, resync, delete; hosted auth link to connect or reconnect one | | Webhooks | create, list, delete; `messaging` (`message_received`), `users` (`new_relation`), `account_status` | | Users and profiles | the account's own profile, any profile with its sections, lookup by profile URL | | Relations and invitations | connections, sent invitations, invite, withdraw | | Chats and messages | chats, one chat, messages, one message, attendees; start a chat, send a message, files | | Posts | a member's or a company's posts, one post; create (text, mentions, images, a video), delete | | Comments and reactions | comments and replies, reactions; comment, reply, react, and Direct's own delete routes | | Company search | LinkedIn's classic company search, by keywords | The [Unipile Compatibility](https://lempile.com/docs/direct/unipile-compatibility) page lists every Unipile route, what Direct does with it, and where it differs. ## How It Differs From Unipile - **LinkedIn only.** No email, calendar, WhatsApp or other providers. No Sales Navigator, Recruiter or group conversations. - **Lempile accounts.** An account id is a Lempile account id. Accounts connect through Lempile's own real-browser sign-in (the hosted auth link), and stay in Lempile's browsers. - **Paced per account.** Daily caps on calls and writes, and a rest after LinkedIn's "too many", answered as Unipile's `429` with `retry_after`. See [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits). - **Messages in real time, relations by polling.** Direct keeps LinkedIn's own live connection open for each account, so a message reaches your webhook within seconds, day and night; a read of the inbox now and then catches anything it missed. New relations come from reading them every 30 to 60 minutes. - **Writes are off by default** for each account, until Lempile turns them on for it. - **Extras Unipile doesn't have:** `?dry_run=true` on every write (the exact LinkedIn call, not sent), an `Idempotency-Key` header that never writes twice, and routes to delete a comment and to remove a reaction. ## When To Use It Use Direct when: - your LinkedIn accounts are (or can be) connected in Lempile; - you have a Unipile integration and want the same calls served from the account's own browser session and address; - you want to see a write's exact LinkedIn call before sending it, and retry writes safely. Keep Unipile (or fall back to it on `501`) when you need another provider, Sales Navigator or Recruiter, people, post or job search, group conversations, or webhook events within seconds. ## Production Host ```text https://direct.lempile.com/api/v1 ``` The OpenAPI document is public at `https://direct.lempile.com/api/v1/openapi.json`, and a reference page draws it at `https://direct.lempile.com/docs`. ## Next - [Quick Start](https://lempile.com/docs/direct/quick-start) - [Authentication And Accounts](https://lempile.com/docs/direct/authentication-and-accounts) - [Webhooks](https://lempile.com/docs/direct/webhooks) - [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits) - [Migrating From Unipile](https://lempile.com/docs/direct/migrating-from-unipile) --- # Lempile Direct: Quick Start > From an API key to a connected LinkedIn account, a first read, a first webhook and a first write tried without sending it. This page takes you from an API key to a connected LinkedIn account, a first read, a first webhook and a first write tried without sending it. Every example uses shell variables: replace them with your own values. ## 1. Set Your Key Direct authenticates every API call with the `X-API-KEY` header. See [Authentication And Accounts](https://lempile.com/docs/direct/authentication-and-accounts) for how to get a key. ```bash export DIRECT_API_KEY="your-api-key" export DIRECT="https://direct.lempile.com/api/v1" ``` Check it: ```bash curl -sS "$DIRECT/accounts" -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "AccountList", "items": [], "cursor": null, "total_count": 0, "status_counts": { "OK": 0, "CONNECTING": 0, "CREDENTIALS": 0, "STOPPED": 0, "ERROR": 0 } } ``` The list holds your workspace's accounts only. A missing, wrong or revoked key answers `401` with `"type": "errors/missing_credentials"`. ## 2. Connect A LinkedIn Account Ask for a hosted auth link, then send the person who owns the LinkedIn account to its `url`. They sign in to LinkedIn on Lempile's page, in a real browser (security checks and codes included), and come back to your `success_redirect_url`. ```bash curl -sS -X POST "$DIRECT/hosted/accounts/link" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "create", "api_url": "https://direct.lempile.com", "providers": ["LINKEDIN"], "expiresOn": "2026-11-01T12:00:00.000Z", "name": "user_42", "success_redirect_url": "https://app.example.com/linkedin/connected", "failure_redirect_url": "https://app.example.com/linkedin/failed", "notify_url": "https://app.example.com/hooks/direct-connected?secret=your-own-secret" }' ``` ```json { "object": "HostedAuthUrl", "url": "https://direct.lempile.com/connect/" } ``` When the account works, Direct POSTs your `notify_url`: ```json { "status": "CREATION_SUCCESS", "account_id": "", "name": "user_42" } ``` Keep `account_id`: every other call names it. ```bash export ACCOUNT_ID="" curl -sS "$DIRECT/accounts/$ACCOUNT_ID" -H "X-API-KEY: $DIRECT_API_KEY" ``` The account is ready when `sources[0].status` is `OK`. ## 3. First Read ```bash curl -sS "$DIRECT/users/me?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "AccountOwnerProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAbcdefghijklmnop", "first_name": "Ada", "last_name": "Sender", "public_identifier": "ada-sender", "public_profile_url": "https://www.linkedin.com/in/ada-sender/", "premium": true } ``` (Shortened.) Then the inbox: ```bash curl -sS "$DIRECT/chats?account_id=$ACCOUNT_ID&limit=20" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## 4. First Webhook Register a URL for new messages. Put your own secret in a header: Direct sends it with every delivery, and you check it. ```bash curl -sS -X POST "$DIRECT/webhooks" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "request_url": "https://app.example.com/hooks/linkedin-messages", "source": "messaging", "name": "messages", "headers": [{ "key": "x-webhook-secret", "value": "your-own-secret" }] }' ``` ```json { "object": "WebhookCreated", "webhook_id": "" } ``` Send the account a LinkedIn message from another account. Within seconds, your URL receives a `message_received` event. Answer any `2xx`. See [Webhooks](https://lempile.com/docs/direct/webhooks) for the payload and the timing. ## 5. First Write, Not Sent Every write takes `?dry_run=true`: Direct answers the exact LinkedIn call it would make, and sends nothing. ```bash export CHAT_ID="" curl -sS -X POST "$DIRECT/chats/$CHAT_ID/messages?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "Thanks, Tuesday works for me." }' ``` ```json { "object": "DryRun", "route": "chats.message", "method": "POST", "path": "/voyager/api/voyagerMessagingDashMessengerMessages?action=createMessage", "body": { "message": { "body": { "attributes": [], "text": "Thanks, Tuesday works for me." } } }, "status": 201 } ``` (Shortened.) To send it for real, drop `dry_run` and add an `Idempotency-Key` header, so a retry never sends it twice: ```bash curl -sS -X POST "$DIRECT/chats/$CHAT_ID/messages" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: reply-42" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "Thanks, Tuesday works for me." }' ``` ```json { "object": "MessageSent", "message_id": "", "id": "" } ``` An account whose writes aren't on yet answers `403` with title `Writes off` (`errors/insufficient_permissions`). Dry runs work either way. ## Next - [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits): what each error means and how to retry. - [Chats And Messages](https://lempile.com/docs/direct/chats-and-messages): files, InMail, ids. - [Migrating From Unipile](https://lempile.com/docs/direct/migrating-from-unipile): moving an existing Unipile integration. --- # Lempile Direct: Authentication And Accounts > API keys, account ids, account statuses, resync, removal, writes per account, and connecting accounts with hosted auth. ## API Keys Every API call carries a Direct API key in the `X-API-KEY` header, as with Unipile: ```bash curl -sS "https://direct.lempile.com/api/v1/accounts" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` - Only the header is read (no `api_key` query parameter). - A missing, wrong or revoked key answers `401` with `"type": "errors/missing_credentials"`, on every route, before anything else is checked. - Direct stores only each key's SHA-256 hash, never the key itself, and never writes a key in a log or an answer. - Two routes are public, with no key: `GET /api/v1/openapi.json` (this API's OpenAPI document) and `GET /docs` (its reference page). ### Keys And Workspaces Every key belongs to one Lempile workspace, and sees and acts on **that workspace's accounts and webhooks only**. - **Getting a key.** A Lempile admin creates it for your workspace in Lempile's dashboard (Direct → API keys). It starts with `ldk_` and is shown once, when it's made: store it as a secret. Ask Lempile for one; each app should have its own. A workspace has at most 50 active keys. - **Revoking a key.** A Lempile admin revokes it in the same place. A revoked key answers `401 errors/missing_credentials` at once, on every route, for good. - **Accounts.** `GET /accounts` lists your workspace's accounts only, and its `total_count` and `status_counts` count those only. Any other account id, whether it belongs to another workspace or doesn't exist, answers exactly the same `404 errors/resource_not_found` (title `Account not found`), on every read, write (dry runs included), account, resync and hosted reconnect route, before any LinkedIn call. A key never learns that another workspace's account exists. - **Webhooks** belong to the workspace of the key that created them. `GET /webhooks` lists your workspace's only; another workspace's webhook id answers `404` to `DELETE`. Only your workspace's accounts send to your webhooks: an account id of another workspace named in `account_ids` is kept but never sends, and its name is never answered. - **Hosted auth.** A new account connected through your key's link joins your workspace. A reconnect link can only name one of your workspace's accounts (served, or removed earlier); any other answers `404`. - **Limits are per account**, not per key: two keys using one account share its pacing and daily caps (see [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits)). - **Writes** are still turned on per account by Lempile (see [Writes Per Account](#writes-per-account)). - Lempile's own admin keys see every workspace, and are the only keys Direct's internal routes (`/api/v1/admin/*`, `/api/v1/canary`) answer; any other key gets `403 errors/insufficient_permissions` there. Those routes aren't part of this API. ## Account IDs An account id is a **Lempile account id** (a UUID). It names one LinkedIn account connected in Lempile: its own browser, its own proxy, its own LinkedIn session. ```bash curl -sS "https://direct.lempile.com/api/v1/accounts" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "AccountList", "items": [ { "object": "Account", "type": "LINKEDIN", "id": "00000000-0000-4000-8000-0000000000a1", "name": "Ada Sender", "created_at": "2026-10-28T09:12:44.512Z", "connection_params": { "im": { "id": "ACoAAAbcdefghijklmnop", "publicIdentifier": "ada-sender", "username": "Ada Sender", "premiumId": null, "premiumContractId": null, "premiumFeatures": ["premium"], "organizations": [] } }, "groups": [], "sources": [{ "id": "00000000-0000-4000-8000-0000000000a1_MESSAGING", "status": "OK" }] } ], "cursor": null, "total_count": 1, "status_counts": { "OK": 1, "CONNECTING": 0, "CREDENTIALS": 0, "STOPPED": 0, "ERROR": 0 } } ``` Where the account id goes: | Routes | Where | | --- | --- | | Every read (`GET /users/...`, `/chats/...`, `/posts/...`, `POST /linkedin/search`) | `?account_id=` query parameter, required | | Writes with a body (`POST /users/invite`, `/chats`, `/chats/{chat_id}/messages`, `/posts`, `/posts/{post_id}/comments`, `/posts/reaction`) | `account_id` in the body, or `?account_id=` | | `DELETE` writes (`/users/invite/sent/{id}`, `/posts/{post_id}`, `/posts/{post_id}/comments/{comment_id}`) | `?account_id=` query parameter, required | | `/accounts/{account_id}` routes | the path | Unlike Unipile, **every inbox call names its account**, `GET /chats/{chat_id}` and `POST /chats/{chat_id}/messages` included: a chat id is LinkedIn's own thread id, which doesn't say which account it belongs to. An account id Direct doesn't serve (unknown, removed, or another workspace's) answers `404` with `"type": "errors/resource_not_found"` and title `Account not found`. ## Account Status `sources[0].status` is what Direct has seen, never a guess: | Status | Meaning | What to do | | --- | --- | --- | | `OK` | LinkedIn answered the account's last call (any read, write or check). | Nothing. | | `CONNECTING` | Nothing has answered yet: a new account, before its first check. | Wait; `GET /accounts/{id}` checks it. | | `CREDENTIALS` | LinkedIn refused the account's session (signed out, or a security check), or its browser is signed out. | Reconnect it with a hosted auth link (`type: "reconnect"`). | | `ERROR` | The account's browser or proxy gave no working session, at least twice over 10 minutes with no call succeeding meanwhile. A single hiccup or a slow start is never `ERROR`. | Try a resync in a few minutes; if it stays, contact Lempile. | - `GET /accounts/{account_id}` checks the account first when its status is older than 10 minutes (15 while it's failing): one cheap LinkedIn call through its own session. - For 5 minutes after LinkedIn signs an account out, every read and write for it answers `401` `errors/disconnected_account` at once, without asking LinkedIn again (`GET /accounts/{account_id}` still answers, with `CREDENTIALS`). - A change of status sends the `account_status` webhook (see [Webhooks](https://lempile.com/docs/direct/webhooks)). ## Resync ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/accounts/$ACCOUNT_ID/resync" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` `GET /accounts/{account_id}/sync` (Unipile's form) does the same. Direct drops the session it kept, reads a fresh one from the account's browser, and checks it with LinkedIn: | Answer | Meaning | | --- | --- | | `200 {"object": "AccountResync", "status": "SYNC_DONE"}` | The fresh session works. The `account_status` webhook `SYNC_SUCCESS` is sent. | | `200 {"object": "AccountResync", "status": "SYNC_ERROR"}` | The browser or the proxy didn't give a working session. Try again in a few minutes. | | `401 errors/disconnected_account` | LinkedIn signed the account out. Reconnect it. | ## Removing An Account ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/accounts/$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "AccountDeleted" } ``` - Direct stops serving the account, and it stays off across restarts. Its kept session is dropped, and the `account_status` webhook `DELETED` is sent. - The LinkedIn session, the browser and the Lempile account are untouched: Direct never signs anyone out of LinkedIn. `provider_session_revoke=true` answers `501`. - A removed account can come back through a hosted auth link with `type: "reconnect"` and its id. ## Writes Per Account Writes are **off by default** for each account. Lempile turns them on per account. Until then, a write answers `403`: ```json { "title": "Writes off", "detail": "This account doesn't write through Lempile Direct.", "type": "errors/insufficient_permissions", "status": 403 } ``` Dry runs (`?dry_run=true`) work for every served account, writes on or off. ## Connecting Accounts: Hosted Auth `POST /api/v1/hosted/accounts/link` takes Unipile's body and answers a link to a page Direct serves: | Field | Required | Notes | | --- | --- | --- | | `type` | yes | `create` (a new account) or `reconnect` | | `api_url` | yes | Any non-empty string, as Unipile requires it (Direct doesn't use it). | | `providers` | for `create` | One provider, a category or a list, taking LinkedIn: `"LINKEDIN"`, `["LINKEDIN"]`, `"*"` or `"*:MESSAGING"`. Optional for `reconnect`; when given, it must take LinkedIn too. | | `expiresOn` | yes | ISO 8601 UTC, in the future. The link lasts 24 hours at most, whatever this says. | | `reconnect_account` | for `reconnect` | The account id to sign in again: one of your workspace's (a removed one works too). | | `name` | no | Your reference (1 to 500 characters), sent back to `notify_url`. | | `success_redirect_url`, `failure_redirect_url` | no | https. Where the person lands. | | `notify_url` | no | https. Direct POSTs `{status, account_id, name}` there when the account works. | | `bypass_success_screen` | no | Accepted, no effect: with `success_redirect_url`, Direct redirects at once. | Unipile's other options (`disabled_features`, `disabled_options`, `proxy`, `sync_limit`, `google_scopes`, `microsoft_scopes`, `single_use`) are accepted and ignored. Direct answers `200` with `{"object": "HostedAuthUrl", "url": "https://direct.lempile.com/connect/"}`. What happens next: 1. The person opens the `url`. A new account will join your key's workspace. The page says "Connect your LinkedIn account" (or "Reconnect") with Cancel and Continue. 2. Continue goes to Lempile's own LinkedIn sign-in, in a real browser on Lempile's servers, with LinkedIn's security checks and codes handled there. Direct never sees the password. 3. Back on Direct, the account is served and its session checked (up to 3 tries, 20 seconds apart, for a fresh browser). 4. When it works: Direct POSTs `notify_url` with exactly `{"status": "CREATION_SUCCESS" | "RECONNECTED", "account_id", "name"}` (retried like a webhook), sends the `account_status` webhooks, and redirects (303) to `success_redirect_url`. 5. A cancel, a failed sign-in or a session that doesn't work redirects to `failure_redirect_url`. A reconnect that comes back with a different account fails. Notes: - The link's token holds only a random id and its expiry. The redirect and notify URLs stay on Direct's server, encrypted, and never appear in a URL. - An expired link shows "This link has expired" (`410`). The way back from the sign-in stays open 30 minutes past the link's expiry, for a sign-in that took long. - One link starts at most 5 sign-in attempts. A reconnect keeps Lempile's own limit (3 failed attempts in 6 hours). - `notify_url` gets no extra header: put your own secret in the URL and check it. - Direct never signs anyone in or out of LinkedIn itself: the sign-in is Lempile's, in the account's own browser. --- # Lempile Direct: Users And Profiles > The account's own profile, any profile with its sections, and lookup by profile URL. Every route here is a read: it takes `?account_id=` and answers Unipile's objects. ## The Account's Own Profile ```bash curl -sS "https://direct.lempile.com/api/v1/users/me?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "AccountOwnerProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAbcdefghijklmnop", "entity_urn": "urn:li:fsd_profile:ACoAAAbcdefghijklmnop", "object_urn": "urn:li:member:123", "first_name": "Ada", "last_name": "Sender", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "public_profile_url": "https://www.linkedin.com/in/ada-sender/", "public_identifier": "ada-sender", "email": "ada@example.com", "premium": true, "open_profile": true, "organizations": [], "recruiter": null, "sales_navigator": null, "headline": "Founder", "occupation": "Founder", "location": "Paris, Île-de-France, France" } ``` Three LinkedIn calls the first time, then kept for an hour. `organizations` is always `[]`, `recruiter` and `sales_navigator` always `null`. ## A Profile ```bash curl -sS "https://direct.lempile.com/api/v1/users/ada-sender?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` `{identifier}` can be: - a provider id (`ACoAAAbcdefghijklmnop`), the id every write takes; - a public identifier (`ada-sender`); - `urn:li:fsd_profile:…`; - a profile URL, URL-encoded (`https%3A%2F%2Fwww.linkedin.com%2Fin%2Fada-sender%2F`). ```json { "object": "UserProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAzyxwvutsrqponml", "public_identifier": "bob-buyer", "first_name": "Bob", "last_name": "Buyer", "headline": "Head of Sales", "websites": ["https://example.com"], "is_premium": false, "is_influencer": false, "is_creator": false, "is_open_profile": true, "is_relationship": true, "is_self": false, "public_profile_url": "https://www.linkedin.com/in/bob-buyer/", "member_urn": "123", "location": "Boston, Massachusetts, United States", "profile_picture_url": "https://media.licdn.com/dms/image/x/100.jpg", "profile_picture_url_large": "https://media.licdn.com/dms/image/x/800.jpg", "background_picture_url": "https://media.licdn.com/dms/image/x/800.jpg", "network_distance": "FIRST_DEGREE", "connected_at": 1761000000000, "follower_count": 973, "connections_count": 970, "shared_connections_count": 1, "is_verified": true, "contact_info": { "emails": ["bob@example.com"] } } ``` - Three LinkedIn calls, as Unipile answers a profile: the profile's top card, the profile page's second card (`follower_count`, `connections_count`, `shared_connections_count` when LinkedIn shows mutual connections, `is_verified`) and its contact info (`websites`, `contact_info`: `emails`, `phones`, `adresses`, `socials`, as the member shares them with the account; `birthdate` when shared). - Someone from the account's conversations of the last half hour, read by provider id without sections, is answered from what the conversation showed, with no LinkedIn call: names, headline, picture, link and degree only. - `network_distance` is `FIRST_DEGREE`, `SECOND_DEGREE`, `THIRD_DEGREE` or `OUT_OF_NETWORK`; it's absent for the account itself (`is_self: true`). `connected_at` (ms) is a connection's date. - `invitation` appears when an invitation is between the account and this person: `SENT` by the account, or `RECEIVED` from them; `status` `PENDING`, or `IGNORED` for a received one the account ignored. - Pictures are Unipile's sizes: `profile_picture_url` 100 px, `profile_picture_url_large` the largest, `background_picture_url` 800 × 200. - Keys LinkedIn has no value for are left out (a profile with no location has no `location`). - An unknown profile answers `404` `errors/resource_not_found`. `linkedin_api` (Recruiter, Sales Navigator) answers `501`. ### Sections `linkedin_sections` adds the profile's sections, as Unipile's: ```bash curl -sS "https://direct.lempile.com/api/v1/users/ada-sender?account_id=$ACCOUNT_ID&linkedin_sections=experience,skills_preview" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` | Value | Adds | | --- | --- | | `about` | `summary` | | `experience` | `work_experience` (every position, grouped by company, newest first, with locations), `work_experience_total_count` | | `education` | `education`, `education_total_count` | | `skills` | `skills` (every skill), `skills_total_count` | | `languages` | `languages`, `languages_total_count` | | `certifications` | `certifications`, `certifications_total_count` | | `volunteering_experience` | `volunteering_experience`, `volunteering_experience_total_count` | | `projects` | `projects`, `projects_total_count` | | `recommendations_received`, `recommendations_given` | `recommendations.received`, `recommendations.given` and their totals | | `recruiting_activity` | `recruiting_activity`, always `[]` | | `*` | every section, in full | | `_preview`, `*_preview` | the same section; only `skills_preview` differs from `skills`: the skills the profile shows first, without reading them all | - Repeat the parameter or separate values with commas. Full wins when a section is asked both ways. - Dates are Unipile's `month/day/year`, on the 1st of the month: `"2/1/2024"`. A current position has `"end": null` and `"current": true`. - Sections cost more LinkedIn calls: the full profile (for `about`, `education`, `skills`, `languages`, `certifications`, `volunteering_experience`, `projects`), the positions (`experience`), every skill (`skills` in full, when the profile has more than it shows), and one per kind of recommendation: 1 to 5 more calls. Ask only the sections you use. - An unknown section name answers `400` `errors/invalid_parameters`. - Skills carry no endorsements: `endorsement_count` is always `0`. ```json { "work_experience": [ { "id": "2398765", "position": "Founder", "company": "Example Co", "skills": [], "current": true, "start": "2/1/2024", "end": null, "company_id": "1035", "company_url": "https://www.linkedin.com/company/example-co/", "location": "Paris, Île-de-France, France", "description": "Building it.", "status": "Full-time" } ], "work_experience_total_count": 1 } ``` ## Lookup By Profile URL ```bash curl -sS "https://direct.lempile.com/api/v1/users?account_id=$ACCOUNT_ID&provider_url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fada-sender%2F" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` Answers the `UserProfile` itself, as `GET /users/{identifier}` does. Without `provider_url`, `GET /users` lists everyone the account has a conversation with (as `GET /chat_attendees`, see [Chats And Messages](https://lempile.com/docs/direct/chats-and-messages)). ## A Profile's Posts See [Posts](https://lempile.com/docs/direct/posts): `GET /users/{identifier}/posts`. ## A Member's Comments And Reactions ```bash curl -sS "https://direct.lempile.com/api/v1/users/ACoAAAbcdefghijklmnop/comments?account_id=$ACCOUNT_ID&limit=10" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "CommentList", "items": [ { "object": "Comment", "id": "7391000000000000001", "post_id": "7390000000000000004", "post_urn": "urn:li:activity:7390000000000000004", "author": "Ada Sender", "author_details": { "id": "ACoAAAbcdefghijklmnop", "headline": "Founder", "profile_url": "https://www.linkedin.com/in/ada-sender", "profile_picture_url": null, "network_distance": null }, "date": "2025-10-24T10:00:00.000Z", "text": "Great point", "reaction_counter": 3, "reply_counter": 1 } ], "cursor": null, "paging": { "page_count": 1 } } ``` `GET /users/{identifier}/reactions` lists the posts the member reacted to (`PostReactionList`: `value` is the reaction LinkedIn shows, `post_id` the post). Both read the member's profile activity, 20 posts a LinkedIn call; `{identifier}` is a provider id, a public identifier (one more read) or `me`. --- # Lempile Direct: Relations And Invitations > Connections, sent invitations, sending and withdrawing invitations, and LinkedIn's refusals. ## Relations The account's connections, newest first. ```bash curl -sS "https://direct.lempile.com/api/v1/users/relations?account_id=$ACCOUNT_ID&limit=100" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "UserRelationsList", "items": [ { "object": "UserRelation", "first_name": "Bob", "last_name": "Buyer", "headline": "Head of Sales", "public_identifier": "bob-buyer", "public_profile_url": "https://www.linkedin.com/in/bob-buyer/", "created_at": 1761000000000, "member_id": "ACoAAAzyxwvutsrqponml", "member_urn": "urn:li:fsd_profile:ACoAAAzyxwvutsrqponml", "connection_urn": "urn:li:fsd_connection:ACoAAAzyxwvutsrqponml", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" } ], "cursor": "eyJrIjoicmVsYXRpb25zIiwicyI6MTAwfQ" } ``` - `limit`: 1 to 1,000 (default 100). LinkedIn gives 100 a call, so a page of 1,000 costs 10 calls. - `member_id` is the relation's provider id: what writes take as `provider_id` or `attendees_ids`. - `created_at` is when the connection was made, in milliseconds. - `filter` (search relations by name) answers `501`. - New relations also arrive as the `users` webhook `new_relation` (see [Webhooks](https://lempile.com/docs/direct/webhooks)). ## Followers The account's followers, newest first, as My Network → Followers lists them. ```bash curl -sS "https://direct.lempile.com/api/v1/users/followers?account_id=$ACCOUNT_ID&limit=100" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "UserFollowersList", "items": [ { "object": "UserFollower", "id": "ACoAAAfollower00000000", "urn": "urn:li:fsd_profile:ACoAAAfollower00000000", "name": "Follower 0", "headline": "Founder at Example 0", "profile_url": "https://www.linkedin.com/in/ACoAAAfollower00000000", "profile_picture_url": "https://media.licdn.com/dms/image/f0/100.jpg", "profile_picture_url_large": "https://media.licdn.com/dms/image/f0/800.jpg" } ], "cursor": "" } ``` - One LinkedIn call a page; `limit` 1 to 100 (default 100). LinkedIn may answer a few less than asked; the next page starts after the ones asked, as Unipile's. - `user_id`: the account's own provider id is its own list; another member's answers an empty list (LinkedIn shows a member's follower count, never the list, to another account; Unipile answers the same); a company's answers `501`. - `GET /users/following` answers Unipile's own `501` for LinkedIn (`errors/feature_not_implemented`, "Missing feature"). ## Sent Invitations Invitations the account sent that are still waiting, newest first. ```bash curl -sS "https://direct.lempile.com/api/v1/users/invite/sent?account_id=$ACCOUNT_ID&limit=100" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "InvitationList", "items": [ { "object": "InvitationSent", "id": "7300000000000000001", "invited_user": "Bob Buyer", "invited_user_id": "ACoAAAzyxwvutsrqponml", "invited_user_public_id": "bob-buyer", "invited_user_description": "Founder", "date": "2025-10-24T10:00:00.000Z", "parsed_datetime": "2025-10-24T10:00:00.000Z", "invitation_text": "Hi Bob", "specifics": { "provider": "LINKEDIN", "shared_secret": "" }, "inviter": { "inviter_name": "Ada Sender", "inviter_id": "ACoAAAbcdefghijklmnop", "inviter_public_identifier": "ada-sender", "inviter_description": "Founder" } } ], "cursor": null } ``` `limit`: 1 to 100 (default 10). ## Send An Invitation ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/users/invite" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: invite-bob-1" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "provider_id": "ACoAAAzyxwvutsrqponml", "message": "Hi Bob, good to meet you at the meetup." }' ``` ```json { "object": "UserInvitationSent", "invitation_id": "7300000000000000002" } ``` | Field | | | --- | --- | | `account_id` | The account that invites (or `?account_id=`). | | `provider_id` | The person: provider id, public identifier or profile URL. | | `message` | Optional note, 300 characters at most. | | `user_email` | Optional: the person's email address, when LinkedIn asks for it. | Direct reads the person's profile first (one call), because LinkedIn itself accepts an invitation to someone already connected. Then it sends the invitation (one write). Status `200`, as Unipile. | Refusal | Status | `type` | | --- | --- | --- | | Already a connection | 422 | `errors/already_connected` | | An invitation from the account is already waiting | 422 | `errors/already_invited_recently` | | The account itself, or someone whose invitation to the account is waiting (accept theirs instead) | 422 | `errors/cannot_invite_attendee` | | No profile with this id | 422 | `errors/invalid_recipient` | | LinkedIn's weekly invitation limit, or a withdrawn invitation sent again too soon | 422 | `errors/cannot_resend_yet` | | Too many invitations waiting for an answer | 422 | `errors/limit_exceeded` | | The account's connection limit | 422 | `errors/connection_limit_reached` | | LinkedIn wants the account to confirm its email address | 403 | `errors/action_required` | `invitation_id` is empty when LinkedIn's answer didn't name the new invitation; it then appears in `GET /users/invite/sent`. ## Withdraw An Invitation ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/users/invite/sent/7300000000000000001?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "InvitationCanceled" } ``` - Direct finds the invitation among the account's latest 100 sent (one read), then withdraws it. One not among them answers `404`. - LinkedIn then blocks a new invitation to that person for about 3 weeks (`errors/cannot_resend_yet`). Both writes take `?dry_run=true` and an `Idempotency-Key` (see [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits)). ## Received Invitations ```bash curl -sS "https://direct.lempile.com/api/v1/users/invite/received?account_id=$ACCOUNT_ID&limit=10" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "InvitationList", "items": [ { "object": "InvitationReceived", "id": "7400000000000000000", "invited_user": "Ada Sender", "invited_user_id": "ACoAAAbcdefghijklmnop", "invited_user_public_id": "ada-sender", "invited_user_description": "Founder", "date": "2025-10-24T10:00:00.000Z", "parsed_datetime": "2025-10-24T10:00:00.000Z", "invitation_text": "Hi Ada, let's connect", "inviter": { "inviter_name": "Bob Buyer", "inviter_id": "ACoAAAzyxwvutsrqponml", "inviter_public_identifier": "bob-buyer", "inviter_description": "CTO" }, "specifics": { "provider": "LINKEDIN", "shared_secret": "s3cret0" } } ], "cursor": null } ``` Invitations waiting for the account's answer, newest first: `inviter` is who sent it, `invited_user` the account itself. ## Accept Or Decline One ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/users/invite/received/7400000000000000000" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "LINKEDIN", "account_id": "'"$ACCOUNT_ID"'", "action": "accept", "shared_secret": "s3cret0" }' ``` ```json { "object": "UserInvitationHandled", "status": "ACCEPTED" } ``` - `action`: `accept`, or `decline` (LinkedIn's Ignore: the inviter isn't told). `shared_secret` is the invitation's `specifics.shared_secret`. - Direct finds the invitation among the account's latest 100 received (one read) first: one not among them answers `404`, a `shared_secret` that isn't its own `400`. - LinkedIn's connection limit answers `422 errors/connection_limit_reached`. It takes `?dry_run=true` and an `Idempotency-Key`, as every write. --- # Lempile Direct: Chats And Messages > The LinkedIn inbox: chat and message ids, chats, messages, attendees, starting a chat, sending messages and files. The account's LinkedIn inbox, in Unipile's `Chat`, `Message` and `ChatAttendee` objects. Every call names its account with `account_id`, the writes included. ## Chat And Message IDs - A chat's id is LinkedIn's own conversation id (`2-` and base64), a message's LinkedIn's own message id. - Direct answers them **URL-safe and unpadded** (`+` becomes `-`, `/` becomes `_`, no trailing `=`), so they fit `[A-Za-z0-9_-]` and go in a path as they are: `2-QUFBQXRocmVhZPv_b25lMQ`. - Every route that takes one also takes LinkedIn's own form, URL-encoded (`2-QUFBQXRocmVhZPv%2Fb25lMQ%3D%3D`), and maps it back. - They are not Unipile ids: an id stored from Unipile doesn't resolve here. Read the chats again from Direct. ## List Chats ```bash curl -sS "https://direct.lempile.com/api/v1/chats?account_id=$ACCOUNT_ID&limit=20" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "ChatList", "items": [ { "object": "Chat", "id": "2-QUFBQXRocmVhZPv_b25lMQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "provider_id": "2-QUFBQXRocmVhZPv_b25lMQ", "name": null, "type": 0, "timestamp": "2026-10-29T09:41:07.000Z", "unread_count": 1, "archived": 0, "muted_until": null, "read_only": 0, "folder": ["INBOX", "INBOX_LINKEDIN_CLASSIC"], "pinned": 0, "attendee_provider_id": "ACoAAAzyxwvutsrqponml" } ], "cursor": "" } ``` | Parameter | | | --- | --- | | `limit` | 1 to 250 (default 10). | | `cursor` | The previous page's `cursor`. | | `unread` | `true`: only chats with unread messages. | | `before`, `after` | Only chats last active before or after this time (ISO 8601 UTC, e.g. `2026-10-01T00:00:00.000Z`). | | `account_type` | `LINKEDIN` or nothing; anything else answers `400`. | - Newest activity first, the inbox and the archive together (LinkedIn's spam folder isn't read). - The newest 20 come from LinkedIn's inbox sync in one call, kept for a minute. Further pages, `before`, `after`, `unread` or a `limit` over 20 walk the inbox and the archive by last activity, 25 a call, so a cursor reaches the whole inbox. - `type` is `0` for one person, `1` for a group. `attendee_provider_id` is the other person in a one-to-one chat. - `read_only: 1` when nobody can answer (LinkedIn's sponsored messages: `content_type: "linkedin_offer"` for the ones LinkedIn labels "LinkedIn Offer", `"sponsored"` for the others). `content_type: "inmail"` for an InMail. - `folder` holds the chat's box (`INBOX`, `ARCHIVED`, `OTHER`, `PENDING`, `SPAM`) and `INBOX_LINKEDIN_CLASSIC`. - `pinned` is always `0`; `muted_until` is `-1` when muted. ## One Chat ```bash curl -sS "https://direct.lempile.com/api/v1/chats/$CHAT_ID?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` The `Chat`, with its latest message as `lastMessage`. Found among the newest conversations, else read from LinkedIn by its id; an unknown id answers `404`. ## Messages ```bash curl -sS "https://direct.lempile.com/api/v1/chats/$CHAT_ID/messages?account_id=$ACCOUNT_ID&limit=20" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "MessageList", "items": [ { "object": "Message", "id": "2-_vptc2cwMTI5IQ", "provider_id": "2-_vptc2cwMTI5IQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "chat_id": "2-QUFBQXRocmVhZPv_b25lMQ", "chat_provider_id": "2-QUFBQXRocmVhZPv_b25lMQ", "sender_id": "ACoAAAzyxwvutsrqponml", "sender_attendee_id": "ACoAAAzyxwvutsrqponml", "sender_urn": "urn:li:fsd_profile:ACoAAAzyxwvutsrqponml", "text": "Sounds good", "attachments": [], "timestamp": "2026-10-29T09:41:07.000Z", "is_sender": 0, "reactions": [], "seen": 0, "seen_by": {}, "hidden": 0, "deleted": 0, "edited": 0, "is_event": 0, "delivered": 1, "behavior": null, "original": "", "message_type": "MESSAGE", "attendee_type": "MEMBER", "attendee_distance": 2 } ], "cursor": "" } ``` - Newest first, back to the first message (LinkedIn gives 100 a call). `limit` 1 to 250 (default 20). - `before`, `after` (ISO 8601 UTC) and `sender_id` (a provider id) filter, as on Unipile. - `is_sender` is `1` for the account's own messages. `deleted: 1` (with `deleted_for: "everyone"`) when its sender took it back, `edited: 1` when edited, `is_event: 1` for an event LinkedIn writes in the conversation. - `reactions` holds the account's own reactions only; other people's reactions to a message aren't read. `seen` is always `0` and `seen_by` always `{}`. - `attachments`: images (`type: "img"`, with `size`), files (`type: "file"`, with `file_name`, `file_size`, `mimetype`) and voice notes (`type: "audio"`, `voice_note: true`, `duration` in milliseconds, `mimetype: "audio/mp4"`), each with a `url` (LinkedIn's signed link, which expires). An image LinkedIn hasn't processed yet comes with `unavailable: true` and no `url`. - `subject` on an InMail. `GET /messages/{message_id}?account_id=` answers one `Message` (with `account_type: "LINKEDIN"`), or `404` when LinkedIn doesn't know it. ## Attendees ```bash curl -sS "https://direct.lempile.com/api/v1/chats/$CHAT_ID/attendees?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "ChatAttendeeList", "items": [ { "object": "ChatAttendee", "id": "ACoAAAzyxwvutsrqponml", "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAzyxwvutsrqponml", "name": "Bob Buyer", "is_self": 0, "hidden": 0, "specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:456", "occupation": "Head of Sales", "headline": "Head of Sales", "network_distance": "DISTANCE_2" }, "picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_url": "https://www.linkedin.com/in/ACoAAAzyxwvutsrqponml" } ], "cursor": null } ``` - The account itself is in the list with `is_self: 1` and `network_distance: "SELF"`. - `GET /chat_attendees?account_id=` lists everyone the account has a conversation with, newest conversation first, with a cursor (`limit` 1 to 250, default 100). With `chat_id`, that chat's attendees. - `GET /users?account_id=` without `provider_url` answers the same list. ## Start A Chat ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: first-message-bob" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "attendees_ids": ["ACoAAAzyxwvutsrqponml"], "text": "Hi Bob, quick question about your post." }' ``` ```json { "object": "ChatStarted", "chat_id": "2-QUFBQXRocmVhZPv_b25lMQ", "message_id": "2-_vptc2cwMTMxIQ", "id": "2-QUFBQXRocmVhZPv_b25lMQ" } ``` Status `201`. Direct first reads the profile's "Message" option, as LinkedIn's profile page does (one call): | Case | Answer | | --- | --- | | The account can message the person for free (a connection, an open profile, a reply) | the message is sent | | It can't, and `linkedin[inmail]` isn't `true` | `422 errors/no_connection_with_recipient` | | `linkedin[inmail]=true`, and LinkedIn offers no InMail to this person | `422 errors/not_allowed_inmail` | | `linkedin[inmail]=true`, and the account has no InMail credit for them | `422 errors/insufficient_credits` | | `linkedin[inmail]=true`, and an InMail is offered | an InMail is sent, with `subject` | | The account itself | `422 errors/invalid_recipient` | - `attendees_ids` holds one person: a provider id, a public identifier or a profile URL. Several (a group conversation) answer `501`. - `linkedin[api]` is `classic` or absent; `recruiter` and `sales_navigator` answer `501`, as do `linkedin[topic]`, `linkedin[applicant_id]`, `linkedin[invitation_id]` and `linkedin[group_id]`. - `text` is 8,000 characters at most, and required unless a file or a voice note is sent. - A voice note (`voice_message`, see [Voice Notes](#voice-notes)) starts a chat the same way; never as an InMail (`501`). - InMail is built from LinkedIn's own web code and covered by tests, but hasn't been confirmed with a real InMail send yet. ## Send A Message ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats/$CHAT_ID/messages" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: reply-42" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "Thanks, Tuesday works for me." }' ``` ```json { "object": "MessageSent", "message_id": "2-_vptc2cwMTMwIQ", "id": "2-_vptc2cwMTMwIQ" } ``` Status `201`. `id` and `message_id` are the same id, in the form `GET /chats/{chat_id}/messages` answers it. ## Files Both writes take files in Unipile's multipart form: ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats/$CHAT_ID/messages" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -F "account_id=$ACCOUNT_ID" \ -F "text=The deck we talked about" \ -F "attachments=@deck.pdf;type=application/pdf" ``` - File fields: `attachments`, `attachments[]` or `attachments[0]`, `attachments[1]`…, and `video_message` (sent as a video). - Other fields in Unipile's bracket form: `attendees_ids[0]`, `linkedin[api]`, `linkedin[inmail]`. - Each file is 20 MB at most (`400 errors/content_too_large` above). Videos go as videos; images and every other file go as files, as LinkedIn's web app sends them. - Files go through LinkedIn's own uploader first (two or three calls each), then the message. - `quote_id` (quoting a message) answers `501`. ## Voice Notes ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats/$CHAT_ID/messages" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: voice-42" \ -F "account_id=$ACCOUNT_ID" \ -F "linkedin[api]=classic" \ -F "voice_message=@voice_message.m4a;type=audio/m4a" ``` ```json { "object": "MessageSent", "message_id": "2-_vptc2cwMTMyIQ", "id": "2-_vptc2cwMTMyIQ" } ``` Status `201`, as for any message; `POST /chats` takes `voice_message` too. - A voice note goes alone, as in LinkedIn's apps: with `text` or files it answers `501`. - Any audio works: m4a (AAC, as Looply records it), mp3, ogg, webm/opus (a browser's recorder), wav, aac or flac, 20 MB at most. Direct turns it into LinkedIn's format, AAC in an .m4a file (AAC audio is kept as it is), and measures it. - 60 seconds at most, LinkedIn's limit: longer answers `422 errors/invalid_message`. A file that isn't audio answers `415 errors/unsupported_media_type`. Nothing is sent in either case. - LinkedIn gets three calls: the upload's registration (`mediaUploadType: VOICE_MESSAGE`), the file, then the message with the voice note as its `audio` (its asset, size and length). `?dry_run=true` shows all three, with the note's length and what it came as, and sends nothing. - Read back, it's an attachment of `type: "audio"` with `voice_note: true` and its `duration` in milliseconds. ## Delete A Message ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/messages/$MESSAGE_ID?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "MessageDeleted" } ``` Status `200`. As LinkedIn's "Delete": the message is taken back for everyone, and reads show it with `deleted: 1`. - Only the account's own messages (someone else's answers `403`), and only within 60 minutes of sending it, LinkedIn's limit (`422 errors/unprocessable_entity` after). One already deleted answers `422 errors/action_already_performed`. - `account_id` is required, as on every inbox call. ## React To A Message ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/messages/2-QUFBQXRocmVhZPv_b25lMQ/reaction" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "reaction": "👍" }' ``` ```json { "object": "MessageReactionAdded", "success": true } ``` `DELETE /messages/{message_id}/reaction` with the same body (or `?reaction=`) takes it off: `{"object": "MessageReactionRemoved", "success": true}`. `reaction` is a native emoji. The message is read first (one call): an unknown one answers `404`. ## Edit A Message ```bash curl -sS -X PATCH "https://direct.lempile.com/api/v1/messages/2-QUFBQXRocmVhZPv_b25lMQ" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "Hi Bob, see you at 3pm" }' ``` ```json { "object": "MessagePatched" } ``` Only the account's own messages, within 60 minutes of sending them, as LinkedIn allows (as a deletion): Direct reads the message first, and someone else's answers `403 errors/insufficient_permissions`, an older one `422 errors/unprocessable_entity`. ## Mark A Chat Read, Mute It, Delete It ```bash curl -sS -X PATCH "https://direct.lempile.com/api/v1/chats/2-QUFBQXRocmVhZPv_b25lMQ" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "action": "setReadStatus", "value": true }' ``` ```json { "object": "ChatPatched" } ``` - `action`: `setReadStatus` (`value: false` marks it unread) or `setMuteStatus`. Unipile's other actions are WhatsApp's and Telegram's: `422 errors/not_supported_by_provider`. - `DELETE /chats/{chat_id}?account_id=` deletes the conversation from the account's inbox (`{"object": "ChatDeleted"}`); the other people keep theirs. - The chat is read first (an unknown one answers `404`). Every write here takes `?dry_run=true` and an `Idempotency-Key`. ## Not Done These Unipile messaging routes answer `501 errors/feature_not_implemented`: archiving or pinning a chat (WhatsApp's), a chat's sync, a message's attachment download, forwarding, `GET /messages` and the `/chat_attendees/{attendee_id}` routes. See [Unipile Compatibility](https://lempile.com/docs/direct/unipile-compatibility). --- # Lempile Direct: Posts > A member's or a company's posts, one post, creating a post with mentions, images or a video, and deleting one. ## A Profile's Or A Company's Posts ```bash curl -sS "https://direct.lempile.com/api/v1/users/me/posts?account_id=$ACCOUNT_ID&limit=10" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "PostList", "items": [ { "object": "Post", "provider": "LINKEDIN", "id": "7388000000000000000", "social_id": "urn:li:activity:7388000000000000000", "share_url": "https://www.linkedin.com/posts/ada-sender_hello-activity-7388000000000000000-abcd", "text": "Hello\nworld", "date": "2d", "parsed_datetime": "2025-10-25T23:54:22.402Z", "reaction_counter": 12, "comment_counter": 3, "repost_counter": 1, "impressions_counter": 0, "author": { "public_identifier": "ada-sender", "id": "ACoAAAbcdefghijklmnop", "name": "Ada Sender", "is_company": false, "headline": "Founder", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" }, "permissions": { "can_react": true, "can_share": true, "can_post_comments": true }, "is_repost": false, "attachments": [ { "id": "D1", "type": "img", "url": "https://media.licdn.com/dms/image/x/1280.jpg", "url_expires_at": 1900000000000, "size": { "width": 1280, "height": 1280 }, "mimetype": "image/jpeg", "unavailable": false, "sticker": false } ], "mentions": [] } ], "paging": { "page_count": 1 }, "cursor": "" } ``` - `{identifier}`: `me` for the account's own posts, or any profile identifier (provider id, public identifier, profile URL). - `is_company=true`: `{identifier}` is a company page (its id or its universal name, e.g. `example-co`). - `limit`: 1 to 100 (default 10). LinkedIn gives 20 posts a call. - `social_id` (`urn:li:activity:…`) is what comments and reactions take as `post_id`. - `date` is LinkedIn's relative date (`2d`); `parsed_datetime` is the exact time, read from the post's id. - `impressions_counter` is `0` unless LinkedIn shows views to the account. - `user_reacted` holds the account's own reaction (`LIKE`, `PRAISE`…) when it reacted. - `mentions`: each person or page named in the text, as `{start, length, url}`. - Attachments: images (`type: "img"`) and videos (`type: "video"`, with `duration` in seconds and `file_size`). Their `url` expires at `url_expires_at` (milliseconds). - A plain repost (no words of its own) is the reposted post as LinkedIn shows it: its `id`, `social_id`, `text`, `author` and counts are the original's, `is_repost` is `true`, `reposted_by` is who reposted, `repost_id` the repost's own activity id (what `POST /posts` answered; `DELETE /posts/{repost_id}` undoes it), `repost_parsed_datetime` when it was reposted, and `repost_content` holds the original (`id`, `date`, `parsed_datetime`, `text`, `author`, `attachments`). - A repost with words is the reposter's own post (`is_repost: false`, its words and author), with `repost_content` the post it shares. ## One Post ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn:li:activity:7388000000000000000?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` `{post_id}` can be the post's id (digits), `urn:li:activity:…`, a `urn:li:ugcPost:…` or `urn:li:share:…`, or the post's URL (URL-encoded). One LinkedIn call. An unknown post answers `404`. ## Create A Post ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: post-2026-10-29-1" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "We shipped it today. Thanks {{0}} for the review!", "mentions": [{ "name": "Bob Buyer", "profile_id": "ACoAAAzyxwvutsrqponml" }] }' ``` ```json { "object": "PostCreated", "post_id": "7389000000000000000", "id": "7389000000000000000", "social_id": "urn:li:activity:7389000000000000000", "external_post_id": "urn:li:activity:7389000000000000000", "share_url": "https://www.linkedin.com/feed/update/urn:li:activity:7389000000000000000/" } ``` Status `201`. The post is public, with comments open to everyone. | Field | | | --- | --- | | `account_id` | The account that posts (or `?account_id=`). | | `text` | 3,000 characters at most, mentions' names and an added link included. | | `mentions` | `[{name, profile_id, is_company?}]`, placed in `text` with `{{0}}`, `{{1}}`… A person's `profile_id` is their provider id (`ACo…`); a page's is its id (digits). Every mention must be placed, and every placeholder must have its mention. | | `external_link` | An `http(s)` link, added at the end of the text unless it's already in it. | | `as_organization` | Post as this company page (its id, digits). The account must be an admin of the page. | | `attachments` | Up to 20 images (jpg, png, gif, webp; 36 MB each), or one video (75 KB to 5 GB). Not both. | | `video_thumbnail` | An image for the video. | Files go in Unipile's multipart form (`attachments`, `attachments[]` or `attachments[0]`…): ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: post-2026-10-29-2" \ -F "account_id=$ACCOUNT_ID" \ -F "text=Two pictures from the meetup" \ -F "attachments=@one.jpg;type=image/jpeg" \ -F "attachments=@two.jpg;type=image/jpeg" ``` - Each file goes through LinkedIn's own uploader first; several images are grouped (one more call), then the post is created. - A video post may take up to 30 seconds more: Direct waits for LinkedIn to give the post its activity id. If it never does, `post_id` is the post's `urn:li:ugcPost:…` (or `urn:li:share:…`) and `share_url` is left out; if LinkedIn's answer names neither, Direct answers `504` (unknown outcome: check the account's posts before posting again). - A refusal LinkedIn names in its answer is `422 errors/invalid_post`. - `repost`, `include_job_posting` and `targeted_audience` answer `501`. - Requests reach Direct through Cloudflare, which limits a request body's size (100 MB on most plans) and a request's time (100 seconds). A large video can be refused there (`413`) or cut (`524`) before Direct answers; use an `Idempotency-Key` and see [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits). ## Delete A Post ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/urn:li:activity:7389000000000000000?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` `204` with an empty body, as Unipile. Direct reads the post first: it must be the account's own (or a page's, as an admin); anyone else's answers `403 errors/insufficient_permissions`. The account's own plain repost (its `repost_id`) is undone as LinkedIn's "Delete repost" does: the repost goes, the reposted post stays. A repost with words is the account's own post and is deleted as one. Both writes take `?dry_run=true` and an `Idempotency-Key`. ## Repost ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "repost": "7388000000000000000", "text": "" }' ``` ```json { "object": "PostCreated", "post_id": "7401000000000000001", "id": "7401000000000000001", "share_url": "https://www.linkedin.com/feed/update/urn:li:activity:7401000000000000001/" } ``` - `repost` is the post's id; with no `text` (or `""`) it's LinkedIn's instant repost, as a page with `as_organization`. With `text`, a repost with those words (mentions and `external_link` as for a post; no files: `400`). - Direct reads the post first (one call) for the share LinkedIn reposts. `post_id` is the new post's activity id, never the reposted one's; when LinkedIn names a plain repost only by its repost urn, `post_id` is that urn. --- # Lempile Direct: Comments And Reactions > A post's comments, replies and reactions; commenting, reacting, and Direct's routes to delete a comment or remove a reaction. `{post_id}` takes the post's id (digits), `urn:li:activity:…`, `urn:li:ugcPost:…`, `urn:li:share:…` or its URL (URL-encoded). A comment's id is digits, as `GET /posts/{post_id}/comments` answers it. ## List Comments ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn:li:activity:7388000000000000000/comments?account_id=$ACCOUNT_ID&sort_by=MOST_RECENT" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "CommentList", "items": [ { "object": "Comment", "id": "7390000000000000001", "post_id": "7388000000000000000", "post_urn": "urn:li:activity:7388000000000000000", "author": "Bob Buyer", "author_details": { "id": "ACoAAAzyxwvutsrqponml", "headline": "Founder", "profile_url": "https://www.linkedin.com/in/bob-buyer", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "network_distance": "DISTANCE_2", "is_company": false }, "date": "2025-10-24T10:00:00.000Z", "text": "Great point", "reaction_counter": 4, "reply_counter": 1 } ], "cursor": "", "total_items": 5, "paging": { "start": 0, "page_count": 1, "total_count": 5 } } ``` - `limit`: 1 to 100 (default 10). - `sort_by`: `MOST_RELEVANT` (the default) or `MOST_RECENT` (newest first). - `comment_id`: that comment's replies instead, oldest first as LinkedIn's apps show them (newest first with `MOST_RECENT`; LinkedIn has no relevance order for replies). - `user_reacted` holds the account's own reaction to a comment, when it reacted. A page's comment has `is_company: true` in `author_details`. - As Unipile's: the author's name is `author` (`author_details` has no name or public identifier), and `network_distance` is LinkedIn's own word for the account itself and its 1st and 2nd degree (`SELF`, `DISTANCE_1`, `DISTANCE_2`), `DISTANCE_3` for anyone further, out of network, or a page. - A reply (with `comment_id`) has `thread_id`: its parent comment's id. Its list has `total_items: null` and `paging.total_count: null`, as Unipile's. Direct finds the post's comment thread by itself (most company and recent posts keep their comments under their `ugcPost`, others under their `activity`). ## Comment Or Reply ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts/urn:li:activity:7388000000000000000/comments" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Idempotency-Key: comment-7388-1" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "text": "Agreed, the second point is the one people miss." }' ``` ```json { "object": "CommentSent", "comment_id": "7390000000000000009" } ``` Status `201`. | Field | | | --- | --- | | `account_id` | The account that comments (or `?account_id=`). | | `text` | 1,250 characters at most, mentions' names and an added link included. | | `comment_id` | Reply to this comment. | | `mentions` | As on posts: `[{name, profile_id, is_company?}]`, placed with `{{0}}`… | | `external_link` | An `http(s)` link, added at the end of the text unless it's in it. | | `as_organization` | Comment as this company page (its id, digits). | | `attachments` | One image (jpg, png, gif or webp), multipart. | `comment_id` in the answer is empty when LinkedIn didn't name the new comment. ## Delete A Comment Direct's own route: Unipile has none. ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/urn:li:activity:7388000000000000000/comments/7390000000000000009?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "CommentDeleted" } ``` ## List Reactions ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn:li:activity:7388000000000000000/reactions?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "PostReactionList", "items": [ { "object": "PostReaction", "value": "PRAISE", "post_id": "7388000000000000000", "author": { "id": "ACoAAAzyxwvutsrqponml", "type": "INDIVIDUAL", "name": "Bob Buyer", "headline": "Head of Sales", "profile_url": "https://www.linkedin.com/in/bob-buyer/", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "public_identifier": "bob-buyer", "network_distance": "SECOND_DEGREE" } } ], "cursor": null, "paging": { "cursor": null, "start": 0, "page_count": 1, "total_count": 1 } } ``` - `limit`: 1 to 100 (default 10). `comment_id`: a comment's reactions instead (each item then has `comment_id`). - `value` is LinkedIn's name: `LIKE`, `PRAISE`, `APPRECIATION`, `EMPATHY`, `INTEREST`, `ENTERTAINMENT`. A page's reaction has `type: "COMPANY"`. ## React ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts/reaction" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "post_id": "urn:li:activity:7388000000000000000", "reaction_type": "like" }' ``` ```json { "object": "ReactionAdded" } ``` Status `201`. | `reaction_type` (Unipile's) | LinkedIn's | | --- | --- | | `like` (default) | `LIKE` | | `celebrate` | `PRAISE` | | `support` | `APPRECIATION` | | `love` | `EMPATHY` | | `insightful` | `INTEREST` | | `funny` | `ENTERTAINMENT` | `comment_id` reacts to one of the post's comments; `as_organization` reacts as a company page. ## Remove A Reaction Direct's own route: Unipile has none. The same fields as reacting, in the query or a JSON body: ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/reaction?account_id=$ACCOUNT_ID&post_id=urn%3Ali%3Aactivity%3A7388000000000000000" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "ReactionRemoved" } ``` Every write here takes `?dry_run=true` and an `Idempotency-Key`. --- # Lempile Direct: Company Search > LinkedIn's classic company search, by keywords. LinkedIn's classic company search, by keywords, in Unipile's `LinkedinSearch` shape. ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=$ACCOUNT_ID&limit=10" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "api": "classic", "category": "companies", "keywords": "outbound software" }' ``` ```json { "object": "LinkedinSearch", "items": [ { "type": "COMPANY", "id": "1035", "name": "Example Co", "location": "Cambridge, MA", "profile_url": "https://www.linkedin.com/company/example-co", "industry": "Software Development", "summary": "Grow better", "logo": "https://media.licdn.com/dms/image/logo1035/100.jpg", "followers_count": 1200000 } ], "config": { "params": { "api": "classic", "category": "companies", "keywords": "outbound software" } }, "paging": { "start": 0, "page_count": 1, "total_count": 2 }, "cursor": "" } ``` | Body field | | | --- | --- | | `api` | `classic` (or absent). | | `category` | `companies`. | | `keywords` | Required. | | `limit` | 1 to 100 (default 10). The query's `limit` wins over it. | - `account_id` goes in the query, as on Unipile. - LinkedIn gives 10 results a call; a `limit` of 100 costs 10 calls. - `id` is the company's id: what `as_organization`, mentions and `GET /users/{id}/posts?is_company=true` take. - `logo` is the company's 100 px logo. `summary` is LinkedIn's own search snippet (Unipile's sometimes differs: a matched specialty, the tagline or the description) and `followers_count` LinkedIn's rounded count ("1.2M followers" → 1200000). - `cursor` (in the query) gives the next page. ## Not Done Answer `501 errors/feature_not_implemented`: - any other `category` (people, posts, jobs) or `api` (Sales Navigator, Recruiter); - any filter: `industry`, `location`, `has_job_offers`, `headcount`, `network_distance`; - `GET /linkedin/search/parameters`. --- # Lempile Direct: Search, Company Pages And Raw Data > LinkedIn's classic search for people, posts and jobs, the ids its filters take, company pages and their employees, and the raw route for reads. Everything here reads LinkedIn as the connected account, with Unipile's paths, bodies and answers. The company search itself (`category: companies`) is on [Company Search](https://lempile.com/docs/direct/company-search). ## Search People ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=$ACCOUNT_ID&limit=10" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "api": "classic", "category": "people", "keywords": "founder", "network_distance": [2], "location": ["105015875"], "company": ["1035"] }' ``` ```json { "object": "LinkedinSearch", "items": [ { "object": "SearchResult", "type": "PEOPLE", "id": "ACoAAApeoplesearch01", "public_identifier": "ada-founder", "public_profile_url": "https://www.linkedin.com/in/ada-founder", "profile_url": "https://www.linkedin.com/in/ACoAAApeoplesearch01", "profile_picture_url": "https://media.licdn.com/dms/image/...", "profile_picture_url_large": "https://media.licdn.com/dms/image/...", "member_urn": "urn:li:member:1001", "name": "Ada Founder", "network_distance": "DISTANCE_2", "location": "Paris, Île-de-France, France", "industry": null, "headline": "Founder at Example" } ], "config": { "params": { "api": "classic", "category": "people", "keywords": "founder", "network_distance": [2], "location": ["105015875"], "company": ["1035"] } }, "paging": { "start": 0, "page_count": 10, "total_count": 1240 }, "cursor": "" } ``` - Filters, as Unipile's: `keywords`, `industry`, `location`, `profile_language`, `network_distance` (1, 2, 3), `company` (current), `past_company`, `school`, `service`, `connections_of`, `followers_of`, `open_to` (`proBono`, `boardMember`) and `advanced_keywords` (`first_name`, `last_name`, `title`, `company`, `school`). Ids are LinkedIn's (digits), from `GET /linkedin/search/parameters` below; `connections_of` and `followers_of` take members' provider ids. - `id` is the member's provider id (`ACo…`), what `GET /users/{identifier}`, `POST /users/invite` and `POST /chats` take. - LinkedIn answers 10 people a call; `limit` up to 100 reads up to 10 pages. The `cursor` keeps the filters: send it back alone (`{"cursor": ""}`, or `?cursor=`). - **A company's employees:** `{"category": "people", "company": ["1035"]}`. ## Search Posts ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "api": "classic", "category": "posts", "keywords": "outbound", "sort_by": "date", "date_posted": "past_week" }' ``` Each item is a `SearchResult` of `type: "POST"`: the post's `id` (its activity id, what `GET /posts/{post_id}`, its comments and reactions take), `social_id`, `share_url`, `text`, `date`, `parsed_datetime`, the counters, `author`, `permissions`, `is_repost` (and `reposted_by`). Filters: `sort_by` (`relevance`, `date`), `date_posted` (`past_day`, `past_week`, `past_month`), `content_type`, `posted_by` (`member`, `company`, `me`, `first_connections`, `people_you_follow`), `mentioning` (`member`, `company`), `author` (`industry`, `company`, `keywords`). As on Unipile, 3 posts by default and 49 at most a page. ## Search Jobs ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=$ACCOUNT_ID&limit=25" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "api": "classic", "category": "jobs", "keywords": "sales", "region": "105015875", "presence": ["remote"], "date_posted": 7 }' ``` Each item is a `SearchResult` of `type: "JOB"` (`id`, `title`, `location`, `posted_at`, `url`, `promoted`, `easy_apply`, `company`). Filters: `sort_by`, `date_posted` (days), `region`, `location`, `location_within_area`, `industry`, `seniority`, `function`, `role`, `job_type`, `company`, `presence`, `easy_apply`, `has_verifications`, `under_10_applicants`, `in_your_network`, `fair_chance_employer`. `benefits`, `commitments` and `minimum_salary` answer `501` for now. 25 jobs a LinkedIn call, 50 at most a page. ## By A LinkedIn Search Address Unipile's `{"url": "…"}` body: a LinkedIn search page's address (people, content, companies or jobs), read as the matching body. ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.linkedin.com/search/results/people/?keywords=cto&network=%5B%22S%22%5D" }' ``` Sales Navigator and Recruiter (`api: "sales_navigator"`, `api: "recruiter"`, or their addresses) answer `403` `errors/feature_not_subscribed`: Direct serves LinkedIn's classic API. ## Search Parameters The ids the filters take, from LinkedIn's own typeahead (one call): ```bash curl -sS "https://direct.lempile.com/api/v1/linkedin/search/parameters?account_id=$ACCOUNT_ID&type=LOCATION&keywords=paris" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "LinkedinSearchParametersList", "items": [{ "object": "LinkedinSearchParameter", "id": "105015875", "title": "Paris, Île-de-France, France" }], "paging": { "page_count": 1 } } ``` `type`: `LOCATION`, `REGION`, `PEOPLE`, `CONNECTIONS` (ids are provider ids), `COMPANY`, `SCHOOL`, `INDUSTRY`, `JOB_TITLE` (the jobs search's `role`) or `SKILL`. Other classic types answer `501`; Sales Navigator and Recruiter types answer `403` `errors/feature_not_subscribed`. ## A Company Page ```bash curl -sS "https://direct.lempile.com/api/v1/linkedin/company/example-co?account_id=$ACCOUNT_ID" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ```json { "object": "CompanyProfile", "id": "1035", "name": "Example Co", "description": "We make outbound simple.", "entity_urn": "urn:li:fsd_company:1035", "public_identifier": "example-co", "profile_url": "https://www.linkedin.com/company/example-co/", "tagline": "Grow better", "followers_count": 5400, "is_following": true, "is_employee": false, "hashtags": [{ "title": "#outbound" }], "messaging": { "is_enabled": false }, "claimed": true, "viewer_permissions": { "canReadAdminDashboard": false }, "organization_type": "PRIVATELY_HELD", "locations": [{ "is_headquarter": true, "country": "US", "city": "Boston", "street": ["1 Main St"], "postalCode": "02110", "area": "MA" }], "industry": ["Software Development"], "employee_count": 120, "employee_count_range": { "from": 51, "to": 200 }, "website": "https://example.com", "foundation_date": "2015" } ``` - `{identifier}`: the public identifier, the id, a urn or the page's link. One LinkedIn call. - `viewer_permissions` are all `true` for the page's admins and all `false` otherwise (LinkedIn's answer names admin only). `messaging.is_enabled` is always `false`, and `insights` and `crunchbase_funding` aren't answered. - Its employees: search people with `company: [""]` (above). Its posts: `GET /users/{id}/posts?is_company=true` ([Posts](https://lempile.com/docs/direct/posts)). ## Raw Data (Reads) Unipile's raw route, for a LinkedIn read Direct has no route for yet: a GET of one of LinkedIn's API addresses, with the account's own session, paced and counted like every read. ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "'"$ACCOUNT_ID"'", "request_url": "https://www.linkedin.com/voyager/api/me" }' ``` ```json { "object": "LinkedinRawData", "data": { "data": { "plainId": 1 }, "included": [] } } ``` - `request_url` is on `https://www.linkedin.com/voyager/api/` (or that path); `query_params` are added to it (`encoding: false` adds them as given). `headers` and `body` are ignored. - Any other `method`, or an `action=` address, answers `501`: writes go through Direct's own routes, each checked first and tried with `?dry_run=true`. - Unipile's Sales Navigator and Recruiter routes (`/linkedin/projects`, `/linkedin/contracts`, `/linkedin/user/{user_id}`, `/linkedin/recruiter/sessions/…`) answer `403` `errors/feature_not_subscribed`. --- # Lempile Direct: Webhooks > Registering webhooks, every payload field by field, delivery and retries, authenticating deliveries, and timing. Direct sends Unipile's webhooks for three sources: `messaging` (`message_received`), `users` (`new_relation`) and `account_status`. You register them through the API (Unipile also lets you add them in its dashboard; Direct has only the API). A webhook belongs to the workspace of the key that created it: a key lists and deletes its own workspace's webhooks only, and only that workspace's accounts send to them. ## Register A Webhook ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/webhooks" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "request_url": "https://app.example.com/hooks/linkedin-messages", "source": "messaging", "name": "messages", "headers": [{ "key": "x-webhook-secret", "value": "your-own-secret" }] }' ``` ```json { "object": "WebhookCreated", "webhook_id": "a1B2c3D4e5F6g7H8i9J0kL" } ``` | Field | Required | | | --- | --- | --- | | `request_url` | yes | https. (http only to `127.0.0.1` or `localhost`, for a local test.) | | `source` | yes | `messaging`, `users` or `account_status`. Anything else answers `400`. | | `name` | no | 200 characters at most; sent as `webhook_name` in `messaging` and `users` payloads. | | `headers` | no | Up to 20 `{key, value}`, sent with every delivery. Values are kept encrypted and never shown again. | | `events` | no | Default: every event of the source (see below). | | `account_ids` | no | Only these accounts. Default: every account of your workspace. Another workspace's account named here never sends. | | `data` | no | `[{key, name}]`: send only these payload keys, renamed (`messaging` and `users` only). | | `format` | no | `json` (default) or `form` (`application/x-www-form-urlencoded`, each value a string or JSON-encoded). | | `enabled` | no | Default `true`. | `GET /api/v1/webhooks` lists your workspace's (header values as `********`), `DELETE /api/v1/webhooks/{hook_id}` deletes one (`hook_id` is the `webhook_id` it was created with) (its deliveries still waiting are dropped). There is no update: delete and create again. ### Events | Source | Sent | Accepted, never sent | | --- | --- | --- | | `messaging` | `message_received`, `message_read`, `message_reaction`, `message_edited`, `message_deleted` | `message_delivered` | | `users` | `new_relation` | | | `account_status` | `ok`, `credentials`, `error`, `connecting`, `creation_success`, `reconnected`, `sync_success`, `deleted` | `creation_fail`, `stopped`, `permissions` | The "accepted, never sent" events are taken at registration so a Unipile setup registers unchanged, but Direct never sends them: LinkedIn has no delivery receipt of its own, and the others aren't states a Lempile account goes through. `message_read`, `message_reaction`, `message_edited` and `message_deleted` come only from the live stream (see [Timing](#timing)). ## Authenticating Deliveries Direct doesn't sign payloads. Every delivery carries the `headers` you gave the webhook, exactly; put a secret there and check it on every request, comparing in constant time. That's how Unipile setups usually authenticate (for example a static `x-unipile-signature` header). Each delivery also has `user-agent: Lempile-Direct-Webhooks/1` and `content-type: application/json` (or the form type). ## Payloads ### messaging: message_received A new message in one of the account's chats: received, or sent by the account from anywhere (Direct, LinkedIn's website or apps), as Unipile does. ```json { "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "account_info": { "type": "LINKEDIN", "feature": "classic", "user_id": "ACoAAAbcdefghijklmnop" }, "event": "message_received", "chat_id": "2-QUFBQXRocmVhZPv_b25lMQ", "timestamp": "2026-10-29T09:41:07.000Z", "webhook_name": "messages", "message_id": "2-_vptc2cwMTMyIQ", "message": "Sounds good", "sender": { "attendee_id": "ACoAAAzyxwvutsrqponml", "attendee_name": "Bob Buyer", "attendee_provider_id": "ACoAAAzyxwvutsrqponml", "attendee_profile_url": "https://www.linkedin.com/in/ACoAAAzyxwvutsrqponml", "attendee_public_identifier": null, "attendee_specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:123456789", "network_distance": "DISTANCE_1", "occupation": "Head of Sales at Example", "is_company": false } }, "attendees": [ { "attendee_id": "ACoAAAzyxwvutsrqponml", "attendee_name": "Bob Buyer", "attendee_provider_id": "ACoAAAzyxwvutsrqponml", "attendee_profile_url": "https://www.linkedin.com/in/ACoAAAzyxwvutsrqponml", "attendee_public_identifier": null, "attendee_specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:123456789", "network_distance": "DISTANCE_1", "occupation": "Head of Sales at Example", "is_company": false } } ], "attachments": [], "is_sender": false, "subject": null, "provider_chat_id": "2-QUFBQXRocmVhZPv_b25lMQ", "provider_message_id": "2-_vptc2cwMTMyIQ", "is_event": 0, "chat_pinned": 0, "quoted": null, "reply_to": null, "is_forwarded": null, "is_view_once": null, "chat_content_type": null, "message_type": "MESSAGE", "is_group": false, "folder": ["INBOX", "INBOX_LINKEDIN_CLASSIC"], "event_type": null } ``` Every key and every type is as in Unipile's own deliveries (checked against real ones). The ids are LinkedIn's (see below), as every Direct route answers them. | Key | | | --- | --- | | `account_id`, `account_type` | The account (`LINKEDIN`). | | `account_info.user_id` | The account's own provider id. | | `chat_id`, `provider_chat_id` | The chat's id, as `GET /chats` answers it (both the same; Unipile's `chat_id` is its own id). | | `message_id`, `provider_message_id` | The message's id, as `GET /chats/{chat_id}/messages` answers it (both the same). | | `timestamp` | The message's time. | | `message` | Its text (empty for a file alone). | | `sender` | Who sent it (below). The account itself when it sent it. | | `attendees` | Everyone in the chat but the account, in the same shape. | | `attachments` | `[{attachment_id, attachment_name, attachment_size, attachment_type, attachment_unavailable, attachment_url}]`: `attachment_type` `img`, `file` or `audio` (a voice note); a file has its `attachment_name` and its size in bytes, an image or a voice note `attachment_size: null`. `attachment_url` is LinkedIn's own link (Unipile's is an `att://` reference to its attachments route, which Direct doesn't have). | | `is_sender` | `true` when the account sent it. | | `subject` | An InMail's subject, else `null`. | | `chat_content_type` | `inmail`, `sponsored`, `linkedin_offer` or `null`. | | `is_group` | A group conversation. | | `folder` | The chat's folders. | | `is_event`, `chat_pinned`, `quoted`, `reply_to`, `is_forwarded`, `is_view_once`, `message_type`, `event_type` | Always `0`, `0`, `null`, `null`, `null`, `null`, `"MESSAGE"`, `null`, as Unipile's LinkedIn ones. | A person (`sender`, each of `attendees`, `read_by`, `reaction_sender`): | Key | | | --- | --- | | `attendee_id`, `attendee_provider_id` | Their provider id (Unipile puts its own attendee id in `attendee_id`). | | `attendee_name` | Their name (a page's name for a page). | | `attendee_profile_url` | `https://www.linkedin.com/in/`, as LinkedIn's messaging links people; `null` for a page. | | `attendee_public_identifier` | Always `null`, as Unipile's. | | `attendee_specifics` | `provider` (`LINKEDIN`), `member_urn` (`urn:li:member:`; for a page `urn:li:msg_messagingParticipant:urn:li:fsd_company:`), `network_distance` (`SELF`, `DISTANCE_1`, `DISTANCE_2`, `DISTANCE_3`, `OUT_OF_NETWORK`; members only), `occupation` (their headline; members only), `is_company`. | ### messaging: message_read, message_reaction, message_edited, message_deleted The same payload as `message_received`, about the message concerned, with its own `event`: | `event` | When | Extra keys | | --- | --- | --- | | `message_read` | Someone else in the chat read it (LinkedIn's read receipt; the account reading its own inbox isn't one). | `read_by`: who read it, in the `sender` shape. | | `message_reaction` | Someone added a reaction to it. | `reaction`: the emoji. `reaction_sender`: who, in the `sender` shape. | | `message_edited` | Its text changed after it was sent (`message` is the new text). | | | `message_deleted` | Its sender deleted it (`message` is `null`, `attachments` empty, as Unipile's). | | Each is sent once (a receipt once per message and reader, a reaction once per message, emoji and person). ### users: new_relation A new connection: an invitation accepted, either way. ```json { "event": "new_relation", "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "webhook_name": "relations", "user_full_name": "Bob Buyer", "user_provider_id": "ACoAAAzyxwvutsrqponml", "user_public_identifier": "bob-buyer", "user_profile_url": "https://www.linkedin.com/in/bob-buyer/", "user_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "timestamp": 1793260931000 } ``` `timestamp` is when the connection was made, in milliseconds since 1970 (a number, to the second), as Unipile's. `user_picture_url` may be `null`. ### account_status ```json { "AccountStatus": { "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "message": "CREDENTIALS" } } ``` | `message` | When | | --- | --- | | `OK`, `CREDENTIALS`, `ERROR`, `CONNECTING` | The account's status changed (see [Authentication And Accounts](https://lempile.com/docs/direct/authentication-and-accounts)). | | `CREATION_SUCCESS` | A hosted auth link connected a new account. | | `RECONNECTED` | A hosted auth link reconnected an account. | | `SYNC_SUCCESS` | A resync worked. | | `DELETED` | `DELETE /accounts/{account_id}`. | The event names you filter on are the lowercase forms (`credentials`, `creation_success`…). A `SYNC_SUCCESS` also has `"Product": "classic"` beside `AccountStatus`, as every one of Unipile's does. ### Hosted auth notify_url Not a registered webhook: a hosted auth link's `notify_url` gets exactly `{"status": "CREATION_SUCCESS" | "RECONNECTED", "account_id", "account_type": "LINKEDIN", "name"}` when the account works, delivered and retried like a webhook, with no extra header. ## Delivery - A POST, 30 seconds at most per try. - Any `2xx` is delivered and never sent again. - A `5xx`, `408`, `425`, `429`, a timeout or a refused connection is tried again 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours later (5 more tries). Any other answer (a `4xx`, a redirect) is final. - Deliveries waiting for a retry are kept encrypted on Direct's disk: a restart of Direct sends them. - Each delivery is independent: a slow receiver holds no other. - A delivery that timed out on Direct's side may still have reached you, so you can receive one twice. Make your handler idempotent on `message_id` (or `user_provider_id`, or the status and account). ## Timing **Messages arrive in real time, day and night.** For each account some `messaging` or `users` webhook wants, Direct keeps LinkedIn's own live connection open (the one LinkedIn's messaging page keeps open), through the account's own session and proxy. A new message (received, or sent by the account from anywhere), a read receipt, a reaction, an edit or a deletion reaches your webhook within seconds of LinkedIn telling the account's browser. Behind it, Direct still reads each account's inbox and relations, through the account's own session and pacing, and only for what some webhook wants (no webhook, no read). It catches anything the live connection missed: | What | Awake (07:00 to 23:00, the account's timezone) | At night | | --- | --- | --- | | The inbox, while the live connection is up | every 15 to 25 minutes | every 45 to 75 minutes | | The inbox, while it's down | every 2 to 5 minutes | every 20 to 40 minutes | | Relations (newest 20) | every 30 to 60 minutes, and within seconds of a hint on the live connection (below) | every 2 to 3 hours, and the same | | Account status, when only `account_status` webhooks exist | every 15 to 30 minutes | every 30 to 60 minutes | - **New connections:** LinkedIn's live connection never names a new connection, but someone accepting an invitation is a notification. When it signals an invitation, the notifications or My Network badge going up, or an in-app alert, Direct reads the newest relations within seconds (one call; at most once every 90 seconds and 96 times a day an account): the new connection then reaches your `users` webhook within about a minute. Otherwise the regular read finds it. Unipile's own `new_relation` comes by polling, up to 8 hours after. - Every message is sent once, whichever path saw it first. - When the live connection drops, it reconnects within seconds, and the inbox is read within a minute after any gap of 30 seconds or more, as LinkedIn's own page re-syncs: messages from the gap arrive then, a minute or two late. While the connection stays down, the inbox is read every 2 to 5 minutes again. - A conversation whose last activity moved has its latest messages read: up to 8 conversations a read (the others next read), up to 20 new messages each, oldest first. - Status changes go out as soon as any call (yours, or a read) sees them. - The timezone is the account's own (America/New_York unless Lempile set another). - The first read for an account only remembers where things are: it sends nothing already there. An account whose reading was stopped for more than 6 hours starts again the same way, so messages from that gap aren't sent; read them with `GET /chats` and `GET /chats/{chat_id}/messages`. - The reads cost the account about 190 LinkedIn calls on a quiet day while the live connection is up (about 300 without it): one per read, one per conversation with new activity, one per (re)connection (LinkedIn closes its live connection every 12 to 14 minutes, as it does for its own page: about 100 a day), and for a message the live connection didn't fully describe, one or two to read it. They stop for the day at 450, and pause while the account has used 80% of its daily calls, so your own calls keep most of the budget. - A signed-out account (`CREDENTIALS`) isn't read until it's `OK` again; a rest after LinkedIn's "too many" is waited out. --- # Lempile Direct: Errors And Limits > Every error type Direct answers, pacing and daily caps, retries, dry runs, Idempotency-Key and unknown outcomes. Direct answers errors in Unipile's shape, with Unipile's status codes and error types, so code written for Unipile's errors keeps working. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` Branch on `status` and `type`. `detail` is for people: its words can change, and it never holds an account's data. ## Every Error Type | HTTP | `type` | Meaning | | --- | --- | --- | | `400` | `errors/invalid_parameters` | A parameter or body field is invalid: a value of the wrong type (`limit=abc`), a `limit` out of range, a cursor this API didn't give, an id that isn't LinkedIn's, a text too long, a body that isn't JSON or a form, an `Idempotency-Key` used for another write. | | `400` | `errors/invalid_url` | Hosted auth or webhooks: a URL that isn't https. | | `400` | `errors/content_too_large` | A file over LinkedIn's limit (a message's file over 20 MB, an image over 36 MB, a video under 75 KB or over 5 GB). | | `400` | `errors/missing_parameters` | A required parameter is missing: `account_id` on a read, `api_url` or `reconnect_account` on a hosted auth link. `detail` names it. | | `401` | `errors/missing_credentials` | The `X-API-KEY` header is missing, wrong or revoked. | | `401` | `errors/disconnected_account` | LinkedIn signed the account out, asks it for a security check, or its browser has no LinkedIn session. Reconnect it with a hosted auth link. | | `402` | `errors/subscription_required` | Your workspace has no active trial or subscription, so its API keys answer this on every route. Start the free trial or add a card at [lempile.com/dashboard/subscription](https://lempile.com/dashboard/subscription) and they work again right away. Your accounts stay connected and your webhooks keep coming meanwhile. | | `403` | `errors/insufficient_permissions` | The account doesn't write through Direct (title `Writes off`), the post isn't the account's to delete, LinkedIn says the account may not do this, or a workspace key called one of Direct's internal admin routes. | | `403` | `errors/resource_access_restricted` | LinkedIn refused this read or write for this account. | | `403` | `errors/account_restricted` | LinkedIn says the account is restricted. | | `403` | `errors/action_required` | LinkedIn asks the account to confirm its email address first. | | `403` | `errors/feature_not_subscribed` | A Sales Navigator or Recruiter search, parameter or route: Direct serves LinkedIn's classic API. | | `404` | `errors/resource_not_found` | An account Direct doesn't serve or that isn't your workspace's (title `Account not found`, the same answer for both), or nothing on LinkedIn with this id (a profile, a post, a chat, a message, an invitation, a webhook). | | `409` | `errors/conflict` | A write with this `Idempotency-Key` is still running. | | `415` | `errors/unsupported_media_type` | A voice note that isn't audio Direct can turn into LinkedIn's format. Nothing was sent. | | `422` | `errors/invalid_parameters` | LinkedIn refused the write's content (title `LinkedIn refused it`). | | `422` | `errors/already_connected` | Invitation: already a connection. | | `422` | `errors/already_invited_recently` | Invitation: one is already waiting. | | `422` | `errors/cannot_invite_attendee` | Invitation: the account itself, or someone whose invitation to the account is waiting. | | `422` | `errors/invalid_recipient` | No profile with this id, or the account itself as a chat's recipient. | | `422` | `errors/cannot_resend_yet` | LinkedIn's weekly invitation limit, or a withdrawn invitation sent again within about 3 weeks. The account's other calls go on. | | `422` | `errors/limit_exceeded` | Too many invitations waiting for an answer: withdraw some. | | `422` | `errors/connection_limit_reached` | The account reached LinkedIn's connection limit. | | `422` | `errors/no_connection_with_recipient` | The account can't message this person for free: only an InMail would reach them. | | `422` | `errors/not_allowed_inmail` | LinkedIn offers no InMail to this person from this account. | | `422` | `errors/insufficient_credits` | No InMail credit for this person. | | `422` | `errors/invalid_post` | LinkedIn refused the post. | | `422` | `errors/unprocessable_entity` | LinkedIn refused another write (for example a post's deletion), or a message sent more than 60 minutes ago can't be deleted or edited. | | `422` | `errors/not_supported_by_provider` | `PATCH /chats/{chat_id}` with one of Unipile's WhatsApp or Telegram actions. | | `422` | `errors/invalid_message` | A voice note over 60 seconds. Nothing was sent. | | `422` | `errors/action_already_performed` | The message is already deleted. | | `429` | `errors/too_many_requests` | The account is resting, used up its day, or is busy. See [Pacing](#pacing). Always with `retry_after`. | | `500` | `errors/provider_error` | LinkedIn answered something Direct couldn't read, or an error of its own. Nothing was written. | | `500` | `errors/unexpected_error` | Direct failed. | | `501` | `errors/feature_not_implemented` | Direct doesn't do this (yet): fall back to Unipile, or do without. | | `503` | `errors/network_down` | The account's proxy or Lempile's browser server didn't answer. Nothing was sent. | | `504` | `errors/request_timeout` | A read: LinkedIn didn't answer in time. A write: **unknown outcome** (see below). | ### Parameter Errors A required query parameter that's missing, or one of the wrong type, answers Unipile's `400` too, naming the parameter (never the value you sent): ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ```json { "title": "Invalid parameters", "detail": "limit: Input should be a valid integer, unable to parse string as an integer.", "type": "errors/invalid_parameters", "status": 400 } ``` ### Cloudflare's Answers `direct.lempile.com` is behind Cloudflare. Two of its answers can reach you instead of Direct's: - `524`: the request took more than 100 seconds (a write waiting its turn, a large upload). For a write, it's an unknown outcome. - `413`: the request body is over Cloudflare's limit (100 MB on most plans): a large video. ## Pacing Every LinkedIn call of an account goes through that account's own pace: | Limit | Value | When reached | | --- | --- | --- | | One call at a time | | A call waits its turn up to 60 seconds, then `429` with `retry_after: 60`. | | Gap between calls | 1.5 to 3 seconds | Calls wait; nothing is refused. | | LinkedIn calls in any 24 hours | 1,500 | `429` until the oldest of them is 24 hours old (`retry_after` says when). | | Writes in any 24 hours | 150 | `429` until the oldest write is 24 hours old. | | Rest after LinkedIn's "too many" (`429` or `999`) | 15 minutes | Every call of the account answers `429` with the seconds left. | - The limits are per account, never per key: two apps using one account share its budget. - One request can cost several LinkedIn calls: a profile with sections, a long page, a write's checks and uploads. Each route's page says what it reads. - The webhook reads use the same budget (about 300 calls a quiet day, 450 at most; see [Webhooks](https://lempile.com/docs/direct/webhooks)). - A security check (`errors/disconnected_account`) is never retried. - The counters live in Direct's memory and start again when Direct restarts. ### Retrying | Answer | Retry? | | --- | --- | | `429` | Yes, after `retry_after` seconds (also in the `Retry-After` header). | | `503` | Yes: nothing was sent. | | `500 provider_error` | A read: yes, later. A write: nothing was written; retry once. | | `504` on a read | Yes. | | `504` on a write, `524`, a lost connection | Only with the same `Idempotency-Key` (below), or after checking the write didn't happen. | | `401 disconnected_account` | No: reconnect the account. | | Other `4xx`, `501` | No. | ## Writes ### Dry Runs Every write takes `?dry_run=true`. Direct does everything up to the write itself (it reads what it checks: the profile before an invitation, the "Message" option before a new chat, the post before its deletion) and answers the plan instead of sending it: ```json { "object": "DryRun", "route": "posts.react", "method": "POST", "path": "/voyager/api/voyagerSocialDashReactions?threadUrn=urn%3Ali%3Aactivity%3A7388000000000000000", "body": { "reactionType": "LIKE" }, "status": 201 } ``` - Status `200`. `status` is what the real write would answer. `uploads`, `multiphoto` and `checked` appear when the write has files or checks. - Nothing is sent to a LinkedIn write endpoint, and no file is uploaded. The checks' reads do happen and count in the account's calls. - Dry runs work for every served account, writes on or off, and don't use an `Idempotency-Key`. - A refusal found by the checks (`already_connected`, `no_connection_with_recipient`…) answers as the real write would. ### Idempotency-Key Send an `Idempotency-Key` header (1 to 200 characters, unique per write) on every real write: - The key is claimed (per account) before anything is sent. The same key on the same account never writes twice. - A repeat after the write finished answers the first answer again (same status, same body; an empty `204` stays empty), for 7 days. - A repeat while the first is still running answers `409 errors/conflict`. - A write that surely didn't happen (a refusal, `429`, `503`, `500 provider_error`) frees the key: the same key may try again. A refusal found by the checks before sending never takes the key at all. - A write whose outcome is unknown answers the same `504` to every repeat: it never writes again under that key. - The same key on another route answers `400`. With a key, retrying after a timeout, a `524` or a lost connection is safe: you get the first answer, `409` while it runs, or the write if it never started. ### Unknown Outcome A write is tried again by Direct only when nothing reached LinkedIn (the proxy refused the connection, or LinkedIn refused the session before reading it). When LinkedIn may have received it (a timeout after sending, or an answer that doesn't name what it made), Direct answers: ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` Check before writing again: the chat's latest messages, the account's posts, the post's comments, the sent invitations. ## Other Limits | | | | --- | --- | | Page sizes | Each list's `limit` range is on its page (relations up to 1,000; chats, messages and attendees up to 250; most others up to 100). | | Cursors | Opaque; only the list that gave one takes it back. | | Texts | Message 8,000 characters, post 3,000, comment 1,250, invitation note 300, InMail subject 200. | | Files | Message: 20 MB each. Post: up to 20 images of 36 MB, or one video of 75 KB to 5 GB. Comment: one image. | | Webhooks | 20 headers each; one `request_url` and one source each. | --- # Lempile Direct: Migrating From Unipile > Moving a Unipile integration to Direct: base URL, key, account ids, chat ids, webhooks, hosted auth and fallback. Direct answers Unipile's LinkedIn routes with Unipile's paths, fields, status codes and error types. Moving a Unipile integration is mostly configuration, plus a few points below. ## 1. Base URL And Key | | Unipile | Direct | | --- | --- | --- | | Base URL | `https:///api/v1` | `https://direct.lempile.com/api/v1` | | Key header | `X-API-KEY: ` | `X-API-KEY: ` (one per workspace, made in Lempile's dashboard) | Paths stay the same: `GET /users/me`, `GET /chats`, `POST /chats/{chat_id}/messages`… If your code checks hostnames (for example that a hosted auth link starts with `https://account.unipile.com/`, or that the API host ends in `unipile.com`), accept `direct.lempile.com` too. ## 2. Account IDs Direct's account ids are Lempile account ids. A Unipile account id doesn't exist here. - Connect each LinkedIn account through Direct's hosted auth link (or have Lempile connect it), then store the new id. `GET /accounts` lists them; `connection_params.im.id` is the member's provider id, the same as on Unipile, so you can match accounts by it. - Every account starts with writes off. Ask Lempile to turn writes on per account, and use `?dry_run=true` meanwhile. ## 3. Chat And Message IDs - A chat id is LinkedIn's own conversation id (`2-…`) and a message id LinkedIn's own message id, answered URL-safe (`-` and `_`, no `=`). They are not Unipile's ids: chat and message ids stored from Unipile don't resolve. Read the chats from Direct to map them (match on the other attendee's provider id and the latest message's time). - **Every inbox call names its account**: `GET /chats/{chat_id}?account_id=`, `GET /chats/{chat_id}/messages?account_id=`, `GET /messages/{message_id}?account_id=`, and `account_id` in the body of `POST /chats/{chat_id}/messages`. Unipile finds the account from the chat id; Direct can't. - Profile provider ids (`ACo…`) and posts' `social_id` (`urn:li:activity:…`) are LinkedIn's own, as on Unipile, so stored ones keep working. Comment and invitation ids are LinkedIn's numbers; read them from Direct. ## 4. Webhooks - Register your webhooks again with `POST /webhooks` on Direct: webhooks set in Unipile's dashboard don't move. Same bodies, same payloads. - Messaging events come in real time from LinkedIn's own live connection, with polling as the backstop; `new_relation` from polling, 30 to 60 minutes after the connection. See [Webhooks](https://lempile.com/docs/direct/webhooks). - `message_received`, `message_read`, `message_reaction`, `message_edited`, `message_deleted`, `new_relation` and the account status events are sent. `message_delivered` is accepted but never sent. - Deliveries carry the headers you registered and are not signed: authenticate them with a secret header, as with Unipile. ## 5. Hosted Auth - `POST /hosted/accounts/link` takes Unipile's body and answers Unipile's `HostedAuthUrl` (`200`). The `url` is on `direct.lempile.com`. - `providers` takes Unipile's forms (`"LINKEDIN"`, `["LINKEDIN"]`, `"*"`, `"*:MESSAGING"`) and must take LinkedIn; a `reconnect` link needs none. - A new account joins your key's workspace; a reconnect link can only name your workspace's accounts. - `notify_url` receives exactly `{status, account_id, name}` (`CREATION_SUCCESS` or `RECONNECTED`), as on Unipile. - Unipile's options `disabled_features`, `disabled_options`, `proxy`, `sync_limit`, `google_scopes`, `microsoft_scopes` and `single_use` are ignored. The link lasts 24 hours at most. - Native authentication (`POST /accounts` with credentials or cookies), checkpoints (`POST /accounts/checkpoint`) and `PATCH /accounts/{id}` answer `501`: accounts connect only through Lempile's real-browser sign-in. ## 6. Errors And Fallback - Error bodies are Unipile's `{title, detail, type, status}`; types and statuses follow Unipile's. - Anything Direct doesn't do answers `501 errors/feature_not_implemented`. A client that keeps Unipile for some accounts or features can fall back to Unipile on `501`. For a write, fall back only on `501` (or `403` `Writes off`, or `404` `Account not found`): any other answer may mean the write went out. - `429 errors/too_many_requests` comes with `retry_after` and a `Retry-After` header: Direct's limits are per account (1,500 LinkedIn calls and 150 writes a day). See [Errors And Limits](https://lempile.com/docs/direct/errors-and-limits). - A missing `account_id` answers `400 errors/missing_parameters`, a parameter of the wrong type `400 errors/invalid_parameters`. ## 7. What You Gain - `?dry_run=true` on every write: the exact LinkedIn call, not sent. - `Idempotency-Key` on writes: the same key never writes twice, so retries are safe. - `DELETE /posts/{post_id}/comments/{comment_id}` and `DELETE /posts/reaction`, which Unipile doesn't have. - Every call goes out from the account's own browser session and its own proxy. ## 8. Checklist 1. Get a Direct key for your workspace; set the base URL and the key. 2. Connect the accounts through hosted auth; store the new account ids. 3. Add `account_id` to every inbox call that lacked it. 4. Re-read chats from Direct; don't reuse Unipile's chat and message ids. 5. Register the webhooks on Direct; authenticate them with a header secret. 6. Accept `direct.lempile.com` wherever your code checks Unipile's hostnames. 7. Try every write with `?dry_run=true`, then ask Lempile to turn writes on. 8. Handle `501` (fall back or skip) and `429` (wait `retry_after`). 9. Check the routes you use against [Unipile Compatibility](https://lempile.com/docs/direct/unipile-compatibility). --- # Lempile Direct: Unipile Compatibility > Every Unipile LinkedIn route and what Direct does with it: the same, the same with differences, or 501. Every Unipile route for LinkedIn, and what Direct does with it. - **Same**: Unipile's path, parameters, answer shape and status. The read answers are checked in Direct's tests against Unipile's published response schemas. - **Same, but**: answered, with the differences listed. - **501**: answers `501 errors/feature_not_implemented`, so a client can fall back. Every other path under `/api/v1` (email, calendars, other providers) answers `501` too. ## Accounts And Hosted Auth | Unipile route | Direct | Notes | | --- | --- | --- | | `GET /accounts` | Same, but | The key's workspace's Lempile accounts only. `status_counts` always has `STOPPED: 0`. | | `GET /accounts/{account_id}` | Same, but | Statuses `OK`, `CONNECTING`, `CREDENTIALS`, `ERROR`. `premiumId`, `premiumContractId` are `null`; `organizations`, `groups` are `[]`. | | `DELETE /accounts/{account_id}` | Same, but | Stops serving the account; never signs it out of LinkedIn. `provider_session_revoke=true`: 501. | | `GET /accounts/{account_id}/sync` | Same, but | Answers `{"object": "AccountResync", "status": "SYNC_DONE" \| "SYNC_ERROR"}`, or `401` when signed out. Also `POST /accounts/{account_id}/resync`. | | `POST /hosted/accounts/link` | Same, but | LinkedIn only (`providers` as a string or a list; none needed to reconnect). A new account joins the key's workspace. 24 hours at most. Options `disabled_features`, `disabled_options`, `proxy`, `sync_limit`, scopes, `single_use` ignored. | | `POST /accounts` (native authentication) | 501 | Accounts connect through hosted auth only. | | `POST /accounts/{account_id}` (reconnect) | 501 | Use a hosted auth link with `type: "reconnect"`. | | `POST /accounts/checkpoint`, `POST /accounts/checkpoint/resend` | 501 | Checkpoints are handled in Lempile's sign-in page. | | `POST /accounts/{account_id}/restart`, `PATCH /accounts/{account_id}` | 501 | | ## Webhooks | Unipile route | Direct | Notes | | --- | --- | --- | | `POST /webhooks` | Same, but | Sources `messaging`, `users`, `account_status` only. The webhook is the key's workspace's. | | `GET /webhooks` | Same, but | The key's workspace's webhooks; header values masked. | | `DELETE /webhooks/{webhook_id}` | Same | | | `messaging` events | Same, but | `message_received`, `message_read`, `message_reaction`, `message_edited` and `message_deleted` in real time (LinkedIn's own live connection), with polling as the backstop. `message_delivered` is accepted and never sent. | | `users` events | Same, but | `new_relation` within about a minute when LinkedIn's live connection hints at a new one (the relations are read at once), else within 30 to 60 minutes (polling). Unipile's: up to 8 hours. | | `account_status` events | Same, but | `OK`, `CREDENTIALS`, `ERROR`, `CONNECTING`, `CREATION_SUCCESS`, `RECONNECTED`, `SYNC_SUCCESS`, `DELETED`. Never `CREATION_FAIL`, `STOPPED`, `PERMISSIONS`. | ## Users | Unipile route | Direct | Notes | | --- | --- | --- | | `GET /users/me` | Same | `organizations: []`, `recruiter` and `sales_navigator` `null`. | | `GET /users/{identifier}` | Same, but | Three LinkedIn calls (top card, second card, contact info), as Unipile's fields. Someone from the account's recent conversations, by provider id: names, headline, picture, link and degree only, with no call. `linkedin_sections` all supported; skills carry no endorsements; `recruiting_activity` is `[]`. `linkedin_api` (Recruiter, Sales Navigator): 501. | | `GET /users/relations` | Same, but | `filter`: 501. | | `GET /users/invite/sent` | Same | | | `POST /users/invite` | Same | Checks the relationship first (`already_connected`, `already_invited_recently`, `cannot_invite_attendee`). | | `DELETE /users/invite/sent/{invitation_id}` | Same, but | The invitation must be among the account's latest 100 sent. | | `GET /users/{identifier}/posts` | Same | Also companies with `is_company=true`. | | `GET /users?provider_url=` | Same, but | Answers the `UserProfile` itself. | | `GET /users/invite/received` | Same | | | `POST /users/invite/received/{invitation_id}` | Same, but | The invitation must be among the account's latest 100 received. `decline` is LinkedIn's Ignore. | | `PATCH /users/me` | 501 | | | `GET /users/{identifier}/comments`, `GET /users/{identifier}/reactions` | Same, but | From the member's profile activity. A reaction's `value` is read from LinkedIn's header ("… celebrates this"). | | `GET /users/followers` | Same, but | The account's own followers; another member's `user_id`: an empty list (as Unipile); a company's: 501. | | `GET /users/following` | Same | Unipile answers 501 for LinkedIn; so does Direct. | ## Messaging | Unipile route | Direct | Notes | | --- | --- | --- | | `GET /chats` | Same, but | `account_id` required. Inbox and archive, not spam. `account_type` other than `LINKEDIN`: 400. | | `GET /chats/{chat_id}` | Same, but | `account_id` required. LinkedIn's thread ids, URL-safe. | | `GET /chats/{chat_id}/messages` | Same, but | `account_id` required. Other people's reactions to a message aren't read; `seen` is always `0`. | | `GET /chats/{chat_id}/attendees` | Same, but | `account_id` required. | | `GET /chat_attendees` | Same, but | Everyone the account has a chat with; also takes `chat_id`. | | `GET /messages/{message_id}` | Same, but | `account_id` required. | | `POST /chats` | Same, but | One attendee. InMail with `linkedin[inmail]=true`, sent as LinkedIn's Premium InMail (`premiumInMail`); no credit: `422` `errors/insufficient_credits` (not yet confirmed with a real InMail send). Voice notes (60 s at most, never as an InMail). Groups, `linkedin[api]` other than `classic`, `topic`, `applicant_id`, `invitation_id`, `group_id`: 501. | | `POST /chats/{chat_id}/messages` | Same, but | `account_id` required. Files 20 MB each. `voice_message`: any audio, turned into LinkedIn's AAC .m4a, 60 s at most, sent alone (with text or files: 501). `quote_id`: 501. | | `PATCH /chats/{chat_id}` | Same, but | `setReadStatus`, `setMuteStatus`. WhatsApp's and Telegram's actions: `422 errors/not_supported_by_provider`. | | `DELETE /chats/{chat_id}` | Same | | | `GET /chats/{chat_id}/sync` | 501 | | | `GET /messages` | 501 | | | `GET /messages/{message_id}/attachments/{attachment_id}` | 501 | Use the attachment's `url`. | | `DELETE /messages/{message_id}` | Same, but | `account_id` required. The account's own messages, within 60 minutes of sending them. | | `POST /messages/{message_id}/reaction`, `DELETE /messages/{message_id}/reaction` | Same, but | `account_id` required (body or query). | | `PATCH /messages/{message_id}` | Same, but | `account_id` required. The account's own messages, within 60 minutes (checked first). | | `POST /messages/{message_id}/forward` | 501 | WhatsApp only on Unipile. | | `GET /chat_attendees/{attendee_id}` and its `picture`, `chats`, `messages`, `sync` | 501 | | ## Posts, Comments And Reactions | Unipile route | Direct | Notes | | --- | --- | --- | | `GET /posts/{post_id}` | Same | | | `POST /posts` | Same, but | Text, mentions, link, page, up to 20 images or one video; `repost` (plain, as a page, or with words, no files). `include_job_posting`, `targeted_audience`: 501. | | `DELETE /posts/{post_id}` | Same | `204`, empty. | | `GET /posts/{post_id}/comments` | Same | Replies with `comment_id`. | | `POST /posts/{post_id}/comments` | Same | Replies, mentions, link, page, one image. | | `GET /posts/{post_id}/reactions` | Same | Also a comment's, with `comment_id`. | | `POST /posts/reaction` | Same | Also on a comment, and as a page. | | `DELETE /posts/{post_id}/comments/{comment_id}` | Direct only | Unipile has no such route. | | `DELETE /posts/reaction` | Direct only | Unipile has no such route. | ## LinkedIn Specific | Unipile route | Direct | Notes | | --- | --- | --- | | `POST /linkedin/search` | Same, but | Classic people (every filter), posts (every filter), jobs (`benefits`, `commitments`, `minimum_salary`: 501) and companies (keywords only); by body, `url` or `cursor`. Sales Navigator, Recruiter: `403 errors/feature_not_subscribed`. | | `GET /linkedin/search/parameters` | Same, but | `LOCATION`, `REGION`, `PEOPLE`, `CONNECTIONS`, `COMPANY`, `SCHOOL`, `INDUSTRY`, `JOB_TITLE`, `SKILL`. Other classic types: 501. Sales Navigator and Recruiter types: 403. | | `GET /linkedin/company/{identifier}` | Same, but | `messaging.is_enabled` always `false`; `viewer_permissions` all of an admin's or none; no `insights`, `crunchbase_funding`, `localized_*`. | | `GET /linkedin/inmail_balance` | 501 | | | `POST /linkedin` (raw data) | Same, but | Reads only: GET of `https://www.linkedin.com/voyager/api/…`. Other methods and actions: 501. | | Recruiter hiring projects, contracts, Recruiter sessions, `POST /linkedin/user/{user_id}` | 403 | `errors/feature_not_subscribed`: Sales Navigator and Recruiter only. | | Job postings and applicants, endorsements, target audience | 501 | | ## Direct's Own | Route | | | --- | --- | | `?dry_run=true` on every write | The exact LinkedIn call, not sent. | | `Idempotency-Key` on every write | Never writes twice; 7 days. | | `GET /api/v1/openapi.json`, `GET /docs` | This API's OpenAPI document and reference page, public. | --- # Lempile Direct API reference Base URL: `https://direct.lempile.com`. Authentication: your API key in the `X-API-KEY` header. OpenAPI document: [direct.json](https://lempile.com/docs-assets/openapi/direct.json). ## Accounts - [List accounts](https://lempile.com/docs/direct/reference/list-accounts): `GET /api/v1/accounts` - [Retrieve an account](https://lempile.com/docs/direct/reference/get-account): `GET /api/v1/accounts/{account_id}` - [Delete an account](https://lempile.com/docs/direct/reference/delete-account): `DELETE /api/v1/accounts/{account_id}` - [Resync an account](https://lempile.com/docs/direct/reference/resync-account): `POST /api/v1/accounts/{account_id}/resync` - [Resync an account (Unipile's GET form)](https://lempile.com/docs/direct/reference/sync-account): `GET /api/v1/accounts/{account_id}/sync` ## Hosted auth - [Create a hosted auth link](https://lempile.com/docs/direct/reference/hosted-auth-link): `POST /api/v1/hosted/accounts/link` ## Webhooks - [List webhooks](https://lempile.com/docs/direct/reference/list-webhooks): `GET /api/v1/webhooks` - [Create a webhook](https://lempile.com/docs/direct/reference/create-webhook): `POST /api/v1/webhooks` - [Delete a webhook](https://lempile.com/docs/direct/reference/delete-webhook): `DELETE /api/v1/webhooks/{hook_id}` ## Users and profiles - [Look up a profile by URL, or list chat attendees](https://lempile.com/docs/direct/reference/list-users): `GET /api/v1/users` - [Retrieve the account's own profile](https://lempile.com/docs/direct/reference/get-own-profile): `GET /api/v1/users/me` - [Retrieve a profile](https://lempile.com/docs/direct/reference/get-profile): `GET /api/v1/users/{identifier}` - [List a member's comments](https://lempile.com/docs/direct/reference/list-user-comments): `GET /api/v1/users/{identifier}/comments` - [List a member's reactions](https://lempile.com/docs/direct/reference/list-user-reactions): `GET /api/v1/users/{identifier}/reactions` ## Relations and invitations - [List followers](https://lempile.com/docs/direct/reference/list-followers): `GET /api/v1/users/followers` - [Send an invitation](https://lempile.com/docs/direct/reference/send-invitation): `POST /api/v1/users/invite` - [List sent invitations](https://lempile.com/docs/direct/reference/list-sent-invitations): `GET /api/v1/users/invite/sent` - [Withdraw an invitation](https://lempile.com/docs/direct/reference/withdraw-invitation): `DELETE /api/v1/users/invite/sent/{invitation_id}` - [List relations](https://lempile.com/docs/direct/reference/list-relations): `GET /api/v1/users/relations` - [List received invitations](https://lempile.com/docs/direct/reference/list-received-invitations): `GET /api/v1/users/invite/received` - [Accept or decline a received invitation](https://lempile.com/docs/direct/reference/handle-invitation): `POST /api/v1/users/invite/received/{invitation_id}` ## Chats and messages - [List all attendees](https://lempile.com/docs/direct/reference/list-all-attendees): `GET /api/v1/chat_attendees` - [List chats](https://lempile.com/docs/direct/reference/list-chats): `GET /api/v1/chats` - [Start a chat](https://lempile.com/docs/direct/reference/start-chat): `POST /api/v1/chats` - [Retrieve a chat](https://lempile.com/docs/direct/reference/get-chat): `GET /api/v1/chats/{chat_id}` - [Mark a chat read or unread, mute it](https://lempile.com/docs/direct/reference/patch-chat): `PATCH /api/v1/chats/{chat_id}` - [Delete a chat](https://lempile.com/docs/direct/reference/delete-chat): `DELETE /api/v1/chats/{chat_id}` - [List a chat's attendees](https://lempile.com/docs/direct/reference/list-chat-attendees): `GET /api/v1/chats/{chat_id}/attendees` - [List a chat's messages](https://lempile.com/docs/direct/reference/list-messages): `GET /api/v1/chats/{chat_id}/messages` - [Send a message in a chat](https://lempile.com/docs/direct/reference/send-message): `POST /api/v1/chats/{chat_id}/messages` - [Retrieve a message](https://lempile.com/docs/direct/reference/get-message): `GET /api/v1/messages/{message_id}` - [Edit a message](https://lempile.com/docs/direct/reference/edit-message): `PATCH /api/v1/messages/{message_id}` - [Delete a message](https://lempile.com/docs/direct/reference/delete-message): `DELETE /api/v1/messages/{message_id}` - [React to a message](https://lempile.com/docs/direct/reference/add-message-reaction): `POST /api/v1/messages/{message_id}/reaction` - [Remove a message reaction](https://lempile.com/docs/direct/reference/remove-message-reaction): `DELETE /api/v1/messages/{message_id}/reaction` ## Posts - [Create a post](https://lempile.com/docs/direct/reference/create-post): `POST /api/v1/posts` - [Retrieve a post](https://lempile.com/docs/direct/reference/get-post): `GET /api/v1/posts/{post_id}` - [Delete a post](https://lempile.com/docs/direct/reference/delete-post): `DELETE /api/v1/posts/{post_id}` - [List a profile's or a company's posts](https://lempile.com/docs/direct/reference/list-user-posts): `GET /api/v1/users/{identifier}/posts` ## Comments and reactions - [React to a post or a comment](https://lempile.com/docs/direct/reference/add-reaction): `POST /api/v1/posts/reaction` - [Remove a reaction](https://lempile.com/docs/direct/reference/remove-reaction): `DELETE /api/v1/posts/reaction` - [List a post's comments](https://lempile.com/docs/direct/reference/list-comments): `GET /api/v1/posts/{post_id}/comments` - [Comment on a post](https://lempile.com/docs/direct/reference/comment-post): `POST /api/v1/posts/{post_id}/comments` - [Delete a comment](https://lempile.com/docs/direct/reference/delete-comment): `DELETE /api/v1/posts/{post_id}/comments/{comment_id}` - [List a post's reactions](https://lempile.com/docs/direct/reference/list-reactions): `GET /api/v1/posts/{post_id}/reactions` ## Search and companies - [Search people, companies, posts or jobs](https://lempile.com/docs/direct/reference/search-companies): `POST /api/v1/linkedin/search` - [Find search parameter ids](https://lempile.com/docs/direct/reference/search-parameters): `GET /api/v1/linkedin/search/parameters` - [Retrieve a company page](https://lempile.com/docs/direct/reference/get-company): `GET /api/v1/linkedin/company/{identifier}` - [Raw LinkedIn data (reads)](https://lempile.com/docs/direct/reference/raw-data): `POST /api/v1/linkedin` --- # Lempile Direct: List accounts `GET https://direct.lempile.com/api/v1/accounts` The LinkedIn accounts of this key's workspace, with their status (`total_count` and `status_counts` count those only). An account Direct never checked is checked once (one LinkedIn call). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `limit` (integer): Items per page: 1 to 250 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/accounts" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "AccountList", "items": [ { "object": "Account", "type": "LINKEDIN", "id": "00000000-0000-4000-8000-0000000000a1", "name": "Ada Sender", "created_at": "2026-10-28T09:12:44.512Z", "connection_params": { "im": { "id": "ACoAAAbcdefghijklmnop", "publicIdentifier": "ada-sender", "username": "Ada Sender", "premiumId": null, "premiumContractId": null, "premiumFeatures": [ "premium" ], "organizations": [] } }, "groups": [], "sources": [ { "id": "00000000-0000-4000-8000-0000000000a1_MESSAGING", "status": "OK" } ] } ], "cursor": null, "total_count": 1, "status_counts": { "OK": 1, "CONNECTING": 0, "CREDENTIALS": 0, "STOPPED": 0, "ERROR": 0 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` --- # Lempile Direct: Retrieve an account `GET https://direct.lempile.com/api/v1/accounts/{account_id}` The account and its status. A status older than 10 minutes (15 while failing) is checked first: one `/voyager/api/me` through the account's own session. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `account_id` (string, required): The account id. Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/accounts/00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "Account", "type": "LINKEDIN", "id": "00000000-0000-4000-8000-0000000000a1", "name": "Ada Sender", "created_at": "2026-10-28T09:12:44.512Z", "connection_params": { "im": { "id": "ACoAAAbcdefghijklmnop", "publicIdentifier": "ada-sender", "username": "Ada Sender", "premiumId": null, "premiumContractId": null, "premiumFeatures": [ "premium" ], "organizations": [] } }, "groups": [], "sources": [ { "id": "00000000-0000-4000-8000-0000000000a1_MESSAGING", "status": "OK" } ] } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` --- # Lempile Direct: Delete an account `DELETE https://direct.lempile.com/api/v1/accounts/{account_id}` Direct stops serving the account (it stays off across restarts), drops its kept session and sends the `account_status` webhook `DELETED`. The LinkedIn session and the Lempile account are untouched. Connect it again with a hosted auth link. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `account_id` (string, required): The account id. Example: `00000000-0000-4000-8000-0000000000a1`. ## Query parameters - `provider_session_revoke` (boolean): `true` answers 501: Direct never signs an account out of LinkedIn. Default: `false`. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/accounts/00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "AccountDeleted" } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` --- # Lempile Direct: Resync an account `POST https://direct.lempile.com/api/v1/accounts/{account_id}/resync` Drops the kept session, reads a fresh one from the account's browser and checks it with LinkedIn (one call). `SYNC_DONE` also sends the `account_status` webhook `SYNC_SUCCESS`. A signed-out account answers 401 `errors/disconnected_account`. Same as `GET /accounts/{account_id}/sync`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `account_id` (string, required): The account id. Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/accounts/00000000-0000-4000-8000-0000000000a1/resync" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "AccountResync", "status": "SYNC_DONE" } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` --- # Lempile Direct: Resync an account (Unipile's GET form) `GET https://direct.lempile.com/api/v1/accounts/{account_id}/sync` Drops the kept session, reads a fresh one from the account's browser and checks it with LinkedIn (one call). `SYNC_DONE` also sends the `account_status` webhook `SYNC_SUCCESS`. A signed-out account answers 401 `errors/disconnected_account`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `account_id` (string, required): The account id. Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/accounts/00000000-0000-4000-8000-0000000000a1/sync" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "AccountResync", "status": "SYNC_DONE" } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` --- # Lempile Direct: Create a hosted auth link `POST https://direct.lempile.com/api/v1/hosted/accounts/link` A link to a page where a person connects (or reconnects) their LinkedIn account through Lempile's own real-browser sign-in, checkpoints included. When they're back, Direct serves the account, checks it, POSTs `notify_url`, sends the `account_status` webhooks and redirects to `success_redirect_url`. A new account joins the key's workspace; a reconnect is for one of its accounts only. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Request body `application/json` (required) - `type` (string, required): `create` connects a new LinkedIn account; `reconnect` signs an existing one in again. One of: `create`, `reconnect`. - `api_url` (string, required): Required, as on Unipile (any non-empty string; Direct doesn't use it). - `providers` (one of): Required for `create`: one provider, a category or a list, taking LinkedIn (`"LINKEDIN"`, `["LINKEDIN"]`, `"*"`, `"*:MESSAGING"`). Optional for `reconnect`; when given, it must take LinkedIn too. - Option 1: array of string - Option 2: string - `expiresOn` (string, required): ISO 8601 UTC time (`YYYY-MM-DDTHH:MM:SS.sssZ`), in the future. The link lasts 24 hours at most, whatever this says. - `name` (string): Your own reference (1 to 500 characters), sent back in `notify_url`'s body. - `reconnect_account` (string): The account id to reconnect (one of the key's workspace, served or removed). Required for `reconnect`. - `success_redirect_url` (string): Where the person lands once the account is connected and checked (https). - `failure_redirect_url` (string): Where the person lands after a cancel, a failure, or a session that doesn't work (https). - `notify_url` (string): Direct POSTs `{status, account_id, name}` here once the account works (https; put your own secret in it). - `bypass_success_screen` (boolean): Accepted; no effect (Direct redirects at once when `success_redirect_url` is set). ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/hosted/accounts/link" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "create", "api_url": "https://direct.lempile.com", "providers": [ "LINKEDIN" ], "expiresOn": "2026-11-01T12:00:00.000Z", "name": "user_42", "success_redirect_url": "https://app.example.com/linkedin/connected", "failure_redirect_url": "https://app.example.com/linkedin/failed", "notify_url": "https://app.example.com/hooks/direct-connected?secret=your-own-secret" }' ``` ## Responses ### 200 OK ```json { "object": "HostedAuthUrl", "url": "https://direct.lempile.com/connect/q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6.j7k8l9z0x1c2v3b4n5m6" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` --- # Lempile Direct: List webhooks `GET https://direct.lempile.com/api/v1/webhooks` This key's workspace's webhooks, header values masked. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `limit` (integer): Items per page: 1 to 250 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/webhooks" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "WebhookList", "items": [ { "object": "Webhook", "id": "a1B2c3D4e5F6g7H8i9J0kL", "account_ids": [], "enabled": true, "name": "messages", "request_url": "https://app.example.com/hooks/linkedin-messages", "format": "json", "headers": [ { "key": "x-webhook-secret", "value": "********" } ], "data": [], "events": [ "message_received", "message_read", "message_reaction", "message_edited", "message_deleted", "message_delivered" ], "type": "AUTO", "source": "messaging" } ], "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` --- # Lempile Direct: Create a webhook `POST https://direct.lempile.com/api/v1/webhooks` Registers a URL for one source's events. The webhook belongs to the key's workspace: only that workspace's accounts send to it. The poller starts reading the accounts it covers within a minute. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Request body `application/json` (required) - `request_url` (string, required): Where events are POSTed: https (http only to 127.0.0.1 or localhost). - `source` (string, required): One of: `messaging`, `users`, `account_status`. - `name` (string): 200 characters at most; sent as `webhook_name` in messaging and users payloads. - `format` (string): One of: `json`, `form`. Default: `"json"`. - `headers` (array of WebhookHeader): Up to 20 headers sent with every delivery (your secret goes here). Values are kept encrypted and never shown again. - `key` (string, required) - `value` (string, required) - `events` (array of string): Default: every event of the source. - `account_ids` (array of string): Only these accounts. Default (empty): every account of the key's workspace. Another workspace's account named here never sends. - `data` (array of WebhookDataKey): Send only these keys, renamed (messaging and users only). - `key` (string, required): A key of the payload (see each source's payload). - `name` (string, required): The name it's sent under. - `enabled` (boolean): Default: `true`. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/webhooks" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "request_url": "https://app.example.com/hooks/linkedin-messages", "source": "messaging", "name": "messages", "headers": [ { "key": "x-webhook-secret", "value": "your-own-secret" } ] }' ``` ## Responses ### 201 Created ```json { "object": "WebhookCreated", "webhook_id": "a1B2c3D4e5F6g7H8i9J0kL" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` --- # Lempile Direct: Delete a webhook `DELETE https://direct.lempile.com/api/v1/webhooks/{hook_id}` Deletes one of this key's workspace's webhooks (another workspace's answers 404); its deliveries still pending are dropped. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `hook_id` (string, required): The webhook's id. Example: `a1B2c3D4e5F6g7H8i9J0kL`. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/webhooks/a1B2c3D4e5F6g7H8i9J0kL" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "WebhookDeleted" } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` --- # Lempile Direct: Look up a profile by URL, or list chat attendees `GET https://direct.lempile.com/api/v1/users` With `provider_url`: that profile (`UserProfile`), as `GET /users/{identifier}`. Without: everyone the account has a conversation with, newest conversation first (`ChatAttendeeList`), as `GET /chat_attendees`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `provider_url` (string): A LinkedIn profile URL (`https://www.linkedin.com/in/…`). - `limit` (integer): Items per page: 1 to 250 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "UserProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAbcdefghijklmnop", "public_identifier": "ada-sender", "first_name": "Ada", "last_name": "Sender", "headline": "Founder at Example", "websites": [], "is_premium": false, "is_influencer": false, "is_creator": true, "is_open_profile": true, "is_relationship": false, "is_self": true, "public_profile_url": "https://www.linkedin.com/in/ada-sender/", "member_urn": "123", "location": "Paris, Île-de-France, France", "primary_locale": { "country": "US", "language": "en" }, "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_picture_url_large": "https://media.licdn.com/dms/image/x/800.jpg", "background_picture_url": "https://media.licdn.com/dms/image/x/1584.jpg" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve the account's own profile `GET https://direct.lempile.com/api/v1/users/me` The connected member: three LinkedIn calls the first time, then kept an hour. Its `profile_picture_url` is Direct's permanent copy of the member's own picture (see the field). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/me?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "AccountOwnerProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAbcdefghijklmnop", "entity_urn": "urn:li:fsd_profile:ACoAAAbcdefghijklmnop", "object_urn": "urn:li:member:123", "first_name": "Ada", "last_name": "Sender", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "public_profile_url": "https://www.linkedin.com/in/ada-sender/", "public_identifier": "ada-sender", "email": "ada@example.com", "premium": true, "open_profile": true, "organizations": [], "recruiter": null, "sales_navigator": null, "headline": "Founder", "occupation": "Founder", "location": "Paris, Île-de-France, France" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve a profile `GET https://direct.lempile.com/api/v1/users/{identifier}` A member's profile, as Unipile answers it: its top card, the profile page's second card (followers, connections, mutual connections, verification) and its contact info (websites, emails, phones): three LinkedIn calls. `linkedin_sections` adds the sections. Someone from the account's recent conversations, read by provider id without sections, is answered from what the conversation showed, with no LinkedIn call (names, headline, picture, link, degree). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `identifier` (string, required): The person: provider id (`ACo…`), public identifier, `urn:li:fsd_profile:…`, a profile URL (URL-encoded) or `me`. Example: `ada-sender`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `linkedin_sections` (array of string): Sections to add: `*` (all, in full), `*_preview`, or any of `about`, `experience`, `education`, `skills`, `languages`, `certifications`, `volunteering_experience`, `projects`, `recommendations_received`, `recommendations_given`, `recruiting_activity`, each also `_preview`. Repeat the parameter or separate with commas. `skills_preview` keeps the skills the profile shows; the other `_preview` values equal the full ones. Sections cost 1 to 6 more LinkedIn calls in all. - `linkedin_api` (string): Any value (`recruiter`, `sales_navigator`) answers 501. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/ada-sender?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "UserProfile", "provider": "LINKEDIN", "provider_id": "ACoAAAbcdefghijklmnop", "public_identifier": "ada-sender", "first_name": "Ada", "last_name": "Sender", "headline": "Founder at Example", "websites": [], "is_premium": false, "is_influencer": false, "is_creator": true, "is_open_profile": true, "is_relationship": false, "is_self": true, "public_profile_url": "https://www.linkedin.com/in/ada-sender/", "member_urn": "123", "location": "Paris, Île-de-France, France", "primary_locale": { "country": "US", "language": "en" }, "profile_picture_url": "https://media.licdn.com/dms/image/x/100.jpg", "profile_picture_url_large": "https://media.licdn.com/dms/image/x/800.jpg", "background_picture_url": "https://media.licdn.com/dms/image/x/800.jpg", "follower_count": 973, "connections_count": 970, "is_verified": false } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a member's comments `GET https://direct.lempile.com/api/v1/users/{identifier}/comments` The comments the member wrote, from their profile's activity (20 posts a LinkedIn call), newest first. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `identifier` (string, required): The member's provider id (ACo…), public identifier, or `me`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/{identifier}/comments?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "CommentList", "items": [ { "object": "Comment", "id": "7391000000000000001", "post_id": "7390000000000000004", "post_urn": "urn:li:activity:7390000000000000004", "author": "Ada Sender", "author_details": { "id": "ACoAAAbcdefghijklmnop", "headline": "Founder", "profile_url": "https://www.linkedin.com/in/ada-sender", "profile_picture_url": null, "network_distance": null }, "date": "2025-10-24T10:00:00.000Z", "text": "Great point", "reaction_counter": 3, "reply_counter": 1 } ], "cursor": null, "paging": { "page_count": 1 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a member's reactions `GET https://direct.lempile.com/api/v1/users/{identifier}/reactions` The posts the member reacted to, from their profile's activity (20 a LinkedIn call), newest first, with the reaction LinkedIn shows. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `identifier` (string, required): The member's provider id (ACo…), public identifier, or `me`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/{identifier}/reactions?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "PostReactionList", "items": [ { "object": "PostReaction", "value": "PRAISE", "post_id": "7390000000000000005", "author": { "id": "ACoAAAbcdefghijklmnop", "type": "INDIVIDUAL", "name": "Ada Sender", "headline": null, "profile_url": "https://www.linkedin.com/in/ada-sender/", "profile_picture_url": null } } ], "cursor": null, "paging": { "page_count": 1 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List followers `GET https://direct.lempile.com/api/v1/users/followers` The account's followers, newest first, as My Network → Followers lists them (one LinkedIn call a page; LinkedIn may answer a few less than `limit`, and the next page starts after the ones asked, as Unipile). `user_id` of another member answers an empty list (LinkedIn shows a member's follower count, never the list, to another account; Unipile answers the same); a company's answers 501. `GET /api/v1/users/following` answers Unipile's own 501 for LinkedIn. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `user_id` (string): A member's provider id: the account's own (the default), or another member's (an empty list). - `limit` (integer): Items per page: 1 to 100 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/followers?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "UserFollowersList", "items": [ { "object": "UserFollower", "id": "ACoAAAfollower00000000", "urn": "urn:li:fsd_profile:ACoAAAfollower00000000", "name": "Follower 0", "headline": "Founder at Example 0", "profile_url": "https://www.linkedin.com/in/ACoAAAfollower00000000", "profile_picture_url": "https://media.licdn.com/dms/image/f0/100.jpg", "profile_picture_url_large": "https://media.licdn.com/dms/image/f0/800.jpg" } ], "cursor": "eyJrIjoiZm9sbG93ZXJzIiwicyI6MX0" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Send an invitation `POST https://direct.lempile.com/api/v1/users/invite` Reads the person's top card first: a connection answers 422 `errors/already_connected`, an invitation already waiting `errors/already_invited_recently`, the account itself or someone who invited it `errors/cannot_invite_attendee`, an unknown profile `errors/invalid_recipient`. Then sends the invitation (one write). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `provider_id` (string, required): The person: their provider id (`ACo…`), public identifier or profile URL. - `message` (string): The note, 300 characters at most. - `user_email` (string): The person's email address, when LinkedIn asks for it to connect. `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `provider_id` (string, required): The person: their provider id (`ACo…`), public identifier or profile URL. - `message` (string): The note, 300 characters at most. - `user_email` (string): The person's email address, when LinkedIn asks for it to connect. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/users/invite?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAzyxwvutsrqponml", "message": "Hi Bob, good to meet you at the meetup." }' ``` ## Responses ### 200 Sent, or with `?dry_run=true` the plan. ```json { "object": "UserInvitationSent", "invitation_id": "7300000000000000002" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List sent invitations `GET https://direct.lempile.com/api/v1/users/invite/sent` Invitations the account sent that are still waiting, newest first. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/invite/sent?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "InvitationList", "items": [ { "object": "InvitationSent", "id": "7300000000000000001", "invited_user": "Bob Buyer", "invited_user_id": "ACoAAAzyxwvutsrqponml", "invited_user_public_id": "bob-buyer", "invited_user_description": "Founder", "date": "2025-10-24T10:00:00.000Z", "parsed_datetime": "2025-10-24T10:00:00.000Z", "invitation_text": "Hi Bob", "specifics": { "provider": "LINKEDIN", "shared_secret": "secret-1" }, "inviter": { "inviter_name": "Ada Sender", "inviter_id": "ACoAAAbcdefghijklmnop", "inviter_public_identifier": "ada-sender", "inviter_description": "Founder" } } ], "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Withdraw an invitation `DELETE https://direct.lempile.com/api/v1/users/invite/sent/{invitation_id}` Finds the invitation among the account's latest 100 sent (one read), then withdraws it. LinkedIn then blocks a new invitation to that person for about 3 weeks. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `invitation_id` (string, required): The invitation's id (digits), from `GET /users/invite/sent`. Example: `7300000000000000001`. ## Query parameters - `account_id` (string, required): The account that writes. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/users/invite/sent/7300000000000000001?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 Withdrawn, or with `?dry_run=true` the plan. ```json { "object": "InvitationCanceled" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List relations `GET https://direct.lempile.com/api/v1/users/relations` The account's connections, newest first (100 a LinkedIn call). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 1000 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. - `filter` (string): Answers 501: filtering by name isn't done yet. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/relations?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "UserRelationsList", "items": [ { "object": "UserRelation", "first_name": "First0", "last_name": "Last0", "headline": "Sales", "public_identifier": "person-0", "public_profile_url": "https://www.linkedin.com/in/person-0/", "created_at": 1761000000000, "member_id": "ACoAAAmember0000000000", "member_urn": "urn:li:fsd_profile:ACoAAAmember0000000000", "connection_urn": "urn:li:fsd_connection:ACoAAAmember0000000000", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" }, { "object": "UserRelation", "first_name": "First1", "last_name": "Last1", "headline": "Sales", "public_identifier": "person-1", "public_profile_url": "https://www.linkedin.com/in/person-1/", "created_at": 1761000000001, "member_id": "ACoAAAmember0000000001", "member_urn": "urn:li:fsd_profile:ACoAAAmember0000000001", "connection_urn": "urn:li:fsd_connection:ACoAAAmember0000000001", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" } ], "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List received invitations `GET https://direct.lempile.com/api/v1/users/invite/received` Invitations the account received that wait for its answer, newest first, each with the `shared_secret` accepting or declining it takes. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/invite/received?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "InvitationList", "items": [ { "object": "InvitationReceived", "id": "7400000000000000000", "invited_user": "Ada Sender", "invited_user_id": "ACoAAAbcdefghijklmnop", "invited_user_public_id": "ada-sender", "invited_user_description": "Founder", "date": "2025-10-24T10:00:00.000Z", "parsed_datetime": "2025-10-24T10:00:00.000Z", "invitation_text": "Hi Ada, let's connect", "inviter": { "inviter_name": "Bob Buyer", "inviter_id": "ACoAAAzyxwvutsrqponml", "inviter_public_identifier": "bob-buyer", "inviter_description": "CTO" }, "specifics": { "provider": "LINKEDIN", "shared_secret": "s3cret0" } } ], "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Accept or decline a received invitation `POST https://direct.lempile.com/api/v1/users/invite/received/{invitation_id}` Accepts the invitation, or declines it (LinkedIn's Ignore). Reads the account's latest 100 received invitations first: an unknown id answers 404, a wrong `shared_secret` 400. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `invitation_id` (string, required): The invitation's id (digits). ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `provider` (string) - `account_id` (string, required) - `action` (string, required): `decline` is LinkedIn's Ignore. One of: `accept`, `decline`. - `shared_secret` (string, required): The invitation's `specifics.shared_secret` from `GET /users/invite/received`. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/users/invite/received/{invitation_id}?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "LINKEDIN", "account_id": "00000000-0000-4000-8000-0000000000a1", "action": "accept", "shared_secret": "s3cret0" }' ``` ## Responses ### 200 Handled, or with `?dry_run=true` the plan. ```json { "object": "UserInvitationHandled", "status": "ACCEPTED" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List all attendees `GET https://direct.lempile.com/api/v1/chat_attendees` Everyone the account has a conversation with, newest conversation first. With `chat_id`: that chat's attendees. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 250 (default 100). Default: `100`. - `cursor` (string): A cursor a previous page gave. - `chat_id` (string): Only this chat's attendees. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/chat_attendees?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "ChatAttendeeList", "items": [ { "object": "ChatAttendee", "id": "ACoAAAperson0000xyz", "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAperson0000xyz", "name": "P0 Person", "is_self": 0, "hidden": 0, "specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:930446784", "occupation": "Founder", "headline": "Founder", "network_distance": "DISTANCE_2" }, "picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_url": "https://www.linkedin.com/in/ACoAAAperson0000xyz" }, { "object": "ChatAttendee", "id": "ACoAAAperson0001xyz", "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAperson0001xyz", "name": "P1 Person", "is_self": 0, "hidden": 0, "specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:199989907", "occupation": "Founder", "headline": "Founder", "network_distance": "DISTANCE_2" }, "picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_url": "https://www.linkedin.com/in/ACoAAAperson0001xyz" } ], "cursor": "eyJrIjoiYXR0ZW5kZWVzIiwiYiI6MTc2MTI5OTg4MDAwMCwieCI6WyIyLSsvOTBhSEpsWVdRd01EQXlJUT09Il19" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List chats `GET https://direct.lempile.com/api/v1/chats` The account's conversations, newest activity first: the inbox and the archive (not spam). The newest 20 come from LinkedIn's inbox sync (kept a minute); further pages, `before`, `after`, `unread` or a `limit` over 20 walk the inbox and the archive by last activity (25 a LinkedIn call). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 250 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. - `unread` (boolean): Only chats with unread messages. Default: `false`. - `before` (string): Only chats last active before this time (ISO 8601 UTC). - `after` (string): Only chats last active after this time (ISO 8601 UTC). - `account_type` (string): Anything but `LINKEDIN` answers 400. One of: `LINKEDIN`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/chats?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "ChatList", "items": [ { "object": "Chat", "id": "2--_90aHJlYWQwMDAwIQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "provider_id": "2--_90aHJlYWQwMDAwIQ", "name": null, "type": 0, "timestamp": "2025-10-24T10:00:00.000Z", "unread_count": 1, "archived": 0, "muted_until": null, "read_only": 0, "folder": [ "INBOX", "INBOX_LINKEDIN_CLASSIC" ], "pinned": 0, "attendee_provider_id": "ACoAAAperson0000xyz" }, { "object": "Chat", "id": "2--_90aHJlYWQwMDAxIQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "provider_id": "2--_90aHJlYWQwMDAxIQ", "name": null, "type": 0, "timestamp": "2025-10-24T09:59:00.000Z", "unread_count": 0, "archived": 0, "muted_until": null, "read_only": 0, "folder": [ "INBOX", "INBOX_LINKEDIN_CLASSIC" ], "pinned": 0, "attendee_provider_id": "ACoAAAperson0001xyz" } ], "cursor": "eyJrIjoiY2hhdHMiLCJiIjoxNzYxMjk5ODgwMDAwLCJ4IjpbIjItKy85MGFISmxZV1F3TURBeUlRPT0iXX0" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Start a chat `POST https://direct.lempile.com/api/v1/chats` A first message to one person. Reads the profile's "Message" option first: someone the account can't message for free answers 422 `errors/no_connection_with_recipient` (with `linkedin[inmail]=true`: an InMail, or `errors/not_allowed_inmail` / `errors/insufficient_credits`). A voice note (`voice_message`) starts the chat the same way; never as an InMail (501). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `attendees_ids` (array of string, required): One person: their provider id, public identifier or profile URL. Several (a group) answers 501. In a form: `attendees_ids[0]`. - `text` (string): 8,000 characters at most. Required unless a file or a voice note is sent. - `subject` (string): An InMail's subject (200 characters at most). - `linkedin` (object): In a form: `linkedin[api]`, `linkedin[inmail]`. `topic`, `applicant_id`, `invitation_id` and `group_id` answer 501. - `api` (string): Only `classic`; `recruiter` or `sales_navigator` answers 501. One of: `classic`. - `inmail` (boolean): Send an InMail when the person isn't reachable by a free message. - `attachments` (array of string (binary)): Files, 20 MB each at most (multipart: `attachments`, `attachments[]` or `attachments[0]`…). - `video_message` (string (binary)): A video, sent as a file. - `voice_message` (string (binary)): A voice note (multipart): sent alone (no `text` or files with it), as LinkedIn's apps send one. Any audio (m4a/AAC as Looply records it, mp3, ogg, webm/opus, wav, aac, flac; 20 MB at most) is turned into LinkedIn's AAC .m4a on the server; 60 seconds at most (422 `errors/invalid_message` over it, 415 `errors/unsupported_media_type` when it isn't audio). `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `attendees_ids` (array of string, required): One person: their provider id, public identifier or profile URL. Several (a group) answers 501. In a form: `attendees_ids[0]`. - `text` (string): 8,000 characters at most. Required unless a file or a voice note is sent. - `subject` (string): An InMail's subject (200 characters at most). - `linkedin` (object): In a form: `linkedin[api]`, `linkedin[inmail]`. `topic`, `applicant_id`, `invitation_id` and `group_id` answer 501. - `api` (string): Only `classic`; `recruiter` or `sales_navigator` answers 501. One of: `classic`. - `inmail` (boolean): Send an InMail when the person isn't reachable by a free message. - `attachments` (array of string (binary)): Files, 20 MB each at most (multipart: `attachments`, `attachments[]` or `attachments[0]`…). - `video_message` (string (binary)): A video, sent as a file. - `voice_message` (string (binary)): A voice note (multipart): sent alone (no `text` or files with it), as LinkedIn's apps send one. Any audio (m4a/AAC as Looply records it, mp3, ogg, webm/opus, wav, aac, flac; 20 MB at most) is turned into LinkedIn's AAC .m4a on the server; 60 seconds at most (422 `errors/invalid_message` over it, 415 `errors/unsupported_media_type` when it isn't audio). ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "attendees_ids": [ "ACoAAAzyxwvutsrqponml" ], "text": "Hi Bob, quick question about your post." }' ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 201 Created ```json { "object": "ChatStarted", "chat_id": "2-QUFBQXRocmVhZPv_b25lMQ", "message_id": "2-_vptc2cwMTMxIQ", "id": "2-QUFBQXRocmVhZPv_b25lMQ" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 415 `errors/unsupported_media_type`: a voice note that isn't audio Direct can turn into LinkedIn's format (AAC .m4a). Nothing was sent. ```json { "title": "Unsupported media type", "detail": "This voice note isn't an audio file LinkedIn can take (m4a, mp3, ogg, webm, wav, aac or flac).", "type": "errors/unsupported_media_type", "status": 415 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve a chat `GET https://direct.lempile.com/api/v1/chats/{chat_id}` One conversation with its `lastMessage`: from the newest conversations, else LinkedIn's conversation by its id. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): The chat's id as Direct answers it (LinkedIn's own `2-…` form, URL-encoded, works too). Example: `2--_90aHJlYWQwMDAwIQ`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/chats/2--_90aHJlYWQwMDAwIQ?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "Chat", "id": "2--_90aHJlYWQwMDAwIQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "account_type": "LINKEDIN", "provider_id": "2--_90aHJlYWQwMDAwIQ", "name": null, "type": 0, "timestamp": "2025-10-24T10:00:00.000Z", "unread_count": 1, "archived": 0, "muted_until": null, "read_only": 0, "folder": [ "INBOX", "INBOX_LINKEDIN_CLASSIC" ], "pinned": 0, "attendee_provider_id": "ACoAAAperson0000xyz", "lastMessage": { "object": "Message", "id": "2-_vptc2cwMTI5IQ", "provider_id": "2-_vptc2cwMTI5IQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "chat_id": "2--_90aHJlYWQwMDAwIQ", "chat_provider_id": "2--_90aHJlYWQwMDAwIQ", "sender_id": "ACoAAAbcdefghijklmnop", "sender_attendee_id": "ACoAAAbcdefghijklmnop", "sender_urn": "urn:li:fsd_profile:ACoAAAbcdefghijklmnop", "text": "message 129", "attachments": [], "timestamp": "2025-10-24T09:59:59.000Z", "is_sender": 1, "reactions": [], "seen": 0, "seen_by": {}, "hidden": 0, "deleted": 0, "edited": 0, "is_event": 0, "delivered": 1, "behavior": null, "original": "", "message_type": "MESSAGE", "attendee_type": "MEMBER" } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Mark a chat read or unread, mute it `PATCH https://direct.lempile.com/api/v1/chats/{chat_id}` `setReadStatus` (`value: true` marks it read) or `setMuteStatus`. The chat is read first (unknown answers 404). Unipile's WhatsApp and Telegram actions answer 422 `errors/not_supported_by_provider`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): LinkedIn's thread id (`2-…`, as Direct answers it). ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string) - `action` (string, required): LinkedIn has `setReadStatus` and `setMuteStatus`; the others (WhatsApp's and Telegram's) answer 422 `errors/not_supported_by_provider`. One of: `setReadStatus`, `setMuteStatus`, `setArchiveStatus`, `setPinnedStatus`, `addParticipant`, `removeParticipant`, `setLabel`, `getInviteLink`. - `value` (boolean, required) ## Example request ```bash curl -sS -X PATCH "https://direct.lempile.com/api/v1/chats/{chat_id}?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "action": "setReadStatus", "value": true }' ``` ## Responses ### 200 Done, or with `?dry_run=true` the plan. ```json { "object": "ChatPatched" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Delete a chat `DELETE https://direct.lempile.com/api/v1/chats/{chat_id}` Deletes the conversation from the account's inbox (LinkedIn's Delete conversation; the other people keep theirs). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): LinkedIn's thread id (`2-…`, as Direct answers it). ## Query parameters - `account_id` (string, required): The account that writes. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/chats/{chat_id}?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 Deleted, or with `?dry_run=true` the plan. ```json { "object": "ChatDeleted" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a chat's attendees `GET https://direct.lempile.com/api/v1/chats/{chat_id}/attendees` Everyone in the conversation, the account included (`is_self: 1`). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): The chat's id as Direct answers it (LinkedIn's own `2-…` form, URL-encoded, works too). Example: `2--_90aHJlYWQwMDAwIQ`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/chats/2--_90aHJlYWQwMDAwIQ/attendees?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "ChatAttendeeList", "items": [ { "object": "ChatAttendee", "id": "ACoAAAbcdefghijklmnop", "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAbcdefghijklmnop", "name": "Ada Sender", "is_self": 1, "hidden": 0, "specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:946529289", "occupation": "Founder", "headline": "Founder", "network_distance": "SELF" }, "picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_url": "https://www.linkedin.com/in/ACoAAAbcdefghijklmnop" }, { "object": "ChatAttendee", "id": "ACoAAAperson0005xyz", "account_id": "00000000-0000-4000-8000-0000000000a1", "provider_id": "ACoAAAperson0005xyz", "name": "P5 Person", "is_self": 0, "hidden": 0, "specifics": { "provider": "LINKEDIN", "member_urn": "urn:li:member:618293495", "occupation": "Founder", "headline": "Founder", "network_distance": "DISTANCE_2" }, "picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "profile_url": "https://www.linkedin.com/in/ACoAAAperson0005xyz" } ], "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a chat's messages `GET https://direct.lempile.com/api/v1/chats/{chat_id}/messages` Newest first, back to the first message (100 a LinkedIn call). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): The chat's id as Direct answers it (LinkedIn's own `2-…` form, URL-encoded, works too). Example: `2--_90aHJlYWQwMDAwIQ`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 250 (default 20). Default: `20`. - `cursor` (string): A cursor a previous page gave. - `before` (string): Only messages before this time (ISO 8601 UTC). - `after` (string): Only messages after this time (ISO 8601 UTC). - `sender_id` (string): Only this sender's messages (a provider id). ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/chats/2--_90aHJlYWQwMDAwIQ/messages?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "MessageList", "items": [ { "object": "Message", "id": "2-_vptc2cwMTI5IQ", "provider_id": "2-_vptc2cwMTI5IQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "chat_id": "2--_90aHJlYWQwMDAwIQ", "chat_provider_id": "2--_90aHJlYWQwMDAwIQ", "sender_id": "ACoAAAbcdefghijklmnop", "sender_attendee_id": "ACoAAAbcdefghijklmnop", "sender_urn": "urn:li:fsd_profile:ACoAAAbcdefghijklmnop", "text": "message 129", "attachments": [], "timestamp": "2025-10-24T09:59:59.000Z", "is_sender": 1, "reactions": [], "seen": 0, "seen_by": {}, "hidden": 0, "deleted": 0, "edited": 0, "is_event": 0, "delivered": 1, "behavior": null, "original": "", "message_type": "MESSAGE", "attendee_type": "MEMBER" }, { "object": "Message", "id": "2-_vptc2cwMTI4IQ", "provider_id": "2-_vptc2cwMTI4IQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "chat_id": "2--_90aHJlYWQwMDAwIQ", "chat_provider_id": "2--_90aHJlYWQwMDAwIQ", "sender_id": "ACoAAAperson0000xyz", "sender_attendee_id": "ACoAAAperson0000xyz", "sender_urn": "urn:li:fsd_profile:ACoAAAperson0000xyz", "text": "message 128", "attachments": [], "timestamp": "2025-10-24T09:59:58.000Z", "is_sender": 0, "reactions": [], "seen": 0, "seen_by": {}, "hidden": 0, "deleted": 0, "edited": 0, "is_event": 0, "delivered": 1, "behavior": null, "original": "", "message_type": "MESSAGE", "attendee_type": "MEMBER", "attendee_distance": 2 } ], "cursor": "eyJrIjoibWVzc2FnZXMiLCJiIjoxNzYxMjk5OTk3MDAwLCJ4IjpbIjItX3ZwdGMyY3dNVEkzSVEiXSwiYyI6IjItKy85MGFISmxZV1F3TURBd0lRPT0ifQ" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Send a message in a chat `POST https://direct.lempile.com/api/v1/chats/{chat_id}/messages` A message in an existing conversation: text and files, or a voice note (multipart `voice_message`). A voice note is uploaded to LinkedIn as a `VOICE_MESSAGE` and sent as the message's `audio` (its asset, size and length), with no text. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `chat_id` (string, required): The chat's id as Direct answers it (LinkedIn's own `2-…` form, URL-encoded, works too). Example: `2--_90aHJlYWQwMDAwIQ`. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string): 8,000 characters at most. Required unless a file or a voice note is sent. - `attachments` (array of string (binary)): Files, 20 MB each at most. - `video_message` (string (binary)): A video, sent as a file. - `voice_message` (string (binary)): A voice note (multipart): sent alone (no `text` or files with it), as LinkedIn's apps send one. Any audio (m4a/AAC as Looply records it, mp3, ogg, webm/opus, wav, aac, flac; 20 MB at most) is turned into LinkedIn's AAC .m4a on the server; 60 seconds at most (422 `errors/invalid_message` over it, 415 `errors/unsupported_media_type` when it isn't audio). - `quote_id` (string): Answers 501: quoting a message isn't sent yet. `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string): 8,000 characters at most. Required unless a file or a voice note is sent. - `attachments` (array of string (binary)): Files, 20 MB each at most. - `video_message` (string (binary)): A video, sent as a file. - `voice_message` (string (binary)): A voice note (multipart): sent alone (no `text` or files with it), as LinkedIn's apps send one. Any audio (m4a/AAC as Looply records it, mp3, ogg, webm/opus, wav, aac, flac; 20 MB at most) is turned into LinkedIn's AAC .m4a on the server; 60 seconds at most (422 `errors/invalid_message` over it, 415 `errors/unsupported_media_type` when it isn't audio). - `quote_id` (string): Answers 501: quoting a message isn't sent yet. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/chats/2--_90aHJlYWQwMDAwIQ/messages?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "text": "Thanks, Tuesday works for me." }' ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 201 Created ```json { "object": "MessageSent", "message_id": "2-_vptc2cwMTMwIQ", "id": "2-_vptc2cwMTMwIQ" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 415 `errors/unsupported_media_type`: a voice note that isn't audio Direct can turn into LinkedIn's format (AAC .m4a). Nothing was sent. ```json { "title": "Unsupported media type", "detail": "This voice note isn't an audio file LinkedIn can take (m4a, mp3, ogg, webm, wav, aac or flac).", "type": "errors/unsupported_media_type", "status": 415 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve a message `GET https://direct.lempile.com/api/v1/messages/{message_id}` One message by its id (a LinkedIn call by ids). 404 when LinkedIn doesn't know it. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `message_id` (string, required): The message's id as Direct answers it. Example: `2-_vptc2cwMDAzIQ`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/messages/2-_vptc2cwMDAzIQ?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "Message", "id": "2-_vptc2cwMDAzIQ", "provider_id": "2-_vptc2cwMDAzIQ", "account_id": "00000000-0000-4000-8000-0000000000a1", "chat_id": "2--_90aHJlYWQwMDAwIQ", "chat_provider_id": "2--_90aHJlYWQwMDAwIQ", "sender_id": "ACoAAAbcdefghijklmnop", "sender_attendee_id": "ACoAAAbcdefghijklmnop", "sender_urn": "urn:li:fsd_profile:ACoAAAbcdefghijklmnop", "text": "message 3", "attachments": [], "timestamp": "2025-10-24T09:57:53.000Z", "is_sender": 1, "reactions": [], "seen": 0, "seen_by": {}, "hidden": 0, "deleted": 0, "edited": 0, "is_event": 0, "delivered": 1, "behavior": null, "original": "", "message_type": "MESSAGE", "attendee_type": "MEMBER", "account_type": "LINKEDIN" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Edit a message `PATCH https://direct.lempile.com/api/v1/messages/{message_id}` Changes the text of one of the account's own messages, within 60 minutes of sending it (LinkedIn's limit; checked first: someone else's message answers 403, an older one 422 `errors/unprocessable_entity`). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `message_id` (string, required): LinkedIn's message id (`2-…`, as Direct answers it). ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string) - `text` (string, required) ## Example request ```bash curl -sS -X PATCH "https://direct.lempile.com/api/v1/messages/{message_id}?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "text": "Hi Bob, see you at 3pm" }' ``` ## Responses ### 200 Edited, or with `?dry_run=true` the plan. ```json { "object": "MessagePatched" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Delete a message `DELETE https://direct.lempile.com/api/v1/messages/{message_id}` Unipile's route: takes back one of the account's own messages for everyone, as LinkedIn's "Delete" does (`messengerMessages?action=recall`). Reads the message first: someone else's answers 403, one already deleted 422 `errors/action_already_performed`, and one sent more than 60 minutes ago 422 `errors/unprocessable_entity` (LinkedIn's limit, as Unipile says). Direct needs `account_id`, as on every inbox call. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `message_id` (string, required): The message's id as Direct answers it. Example: `2-_vptc2cwMDAzIQ`. ## Query parameters - `account_id` (string, required): The account that writes. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/messages/2-_vptc2cwMDAzIQ?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 Deleted, or with `?dry_run=true` the plan. ```json { "object": "MessageDeleted" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: React to a message `POST https://direct.lempile.com/api/v1/messages/{message_id}/reaction` Adds the account's emoji reaction to a message (the message is read first: unknown answers 404). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `message_id` (string, required): LinkedIn's message id (`2-…`, as Direct answers it). ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string) - `reaction` (string, required): A native emoji, e.g. 👍. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/messages/{message_id}/reaction?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "reaction": "👍" }' ``` ## Responses ### 200 Added, or with `?dry_run=true` the plan. ```json { "object": "MessageReactionAdded", "success": true } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Remove a message reaction `DELETE https://direct.lempile.com/api/v1/messages/{message_id}/reaction` Takes the account's emoji reaction off a message. `reaction` in a JSON body or the query. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `message_id` (string, required): LinkedIn's message id (`2-…`, as Direct answers it). ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. - `reaction` (string): The emoji, when the body doesn't name it. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` - `account_id` (string) - `reaction` (string, required): A native emoji, e.g. 👍. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/messages/{message_id}/reaction?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "reaction": "👍" }' ``` ## Responses ### 200 Removed, or with `?dry_run=true` the plan. ```json { "object": "MessageReactionRemoved", "success": true } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Create a post `POST https://direct.lempile.com/api/v1/posts` Publishes a post (public, comments open), with mentions, a link, up to 20 images or one video, as the member or as a page. Files are uploaded first (multipart). With `repost`: a repost (LinkedIn's instant repost, or with `text` a repost with those words). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string, required): 3,000 characters at most (mentions' names and an added link count). - `mentions` (array of object): Placed in `text` with `{{0}}`, `{{1}}`… Every mention must be placed. - `name` (string, required): The text shown. - `profile_id` (string, required): A person's provider id (`ACo…`) or a company's id (digits). - `is_company` (boolean) - `external_link` (string): An http(s) link, added at the end of the text unless it's already in it. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. - `attachments` (array of string (binary)): Up to 20 images (jpg, png, gif, webp; 36 MB each) or one video (75 KB to 5 GB). - `video_thumbnail` (string (binary)): An image for the video. - `repost` (string): A post's id to repost. With no `text` (or an empty one): LinkedIn's instant repost (as a page with `as_organization`). With `text`: a repost with those words (mentions and link as for a post; no files). - `include_job_posting` (string): Answers 501. - `targeted_audience` (string): Answers 501. `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string, required): 3,000 characters at most (mentions' names and an added link count). - `mentions` (array of object): Placed in `text` with `{{0}}`, `{{1}}`… Every mention must be placed. - `name` (string, required): The text shown. - `profile_id` (string, required): A person's provider id (`ACo…`) or a company's id (digits). - `is_company` (boolean) - `external_link` (string): An http(s) link, added at the end of the text unless it's already in it. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. - `attachments` (array of string (binary)): Up to 20 images (jpg, png, gif, webp; 36 MB each) or one video (75 KB to 5 GB). - `video_thumbnail` (string (binary)): An image for the video. - `repost` (string): A post's id to repost. With no `text` (or an empty one): LinkedIn's instant repost (as a page with `as_organization`). With `text`: a repost with those words (mentions and link as for a post; no files). - `include_job_posting` (string): Answers 501. - `targeted_audience` (string): Answers 501. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "text": "We shipped it today. Thanks {{0}} for the review!", "mentions": [ { "name": "Bob Buyer", "profile_id": "ACoAAAzyxwvutsrqponml" } ] }' ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 201 Created ```json { "object": "PostCreated", "post_id": "7389000000000000000", "id": "7389000000000000000", "share_url": "https://www.linkedin.com/feed/update/urn:li:activity:7389000000000000000/" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve a post `GET https://direct.lempile.com/api/v1/posts/{post_id}` One post (one LinkedIn call). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "Post", "provider": "LINKEDIN", "id": "7388000000000000000", "social_id": "urn:li:activity:7388000000000000000", "share_url": "https://www.linkedin.com/posts/ada-sender_hello-activity-7388000000000000000-abcd", "text": "Hello\nworld", "date": "2d", "parsed_datetime": "2025-10-25T23:54:22.402Z", "reaction_counter": 12, "comment_counter": 3, "repost_counter": 1, "impressions_counter": 0, "author": { "public_identifier": "ada-sender", "id": "ACoAAAbcdefghijklmnop", "name": "Ada Sender", "is_company": false, "headline": "Founder", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" }, "permissions": { "can_react": true, "can_share": true, "can_post_comments": true }, "is_repost": false, "attachments": [ { "id": "D1", "type": "img", "url": "https://media.licdn.com/dms/image/x/1280.jpg", "url_expires_at": 1900000000000, "size": { "width": 1280, "height": 1280 }, "mimetype": "image/jpeg", "unavailable": false, "sticker": false } ], "mentions": [] } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Delete a post `DELETE https://direct.lempile.com/api/v1/posts/{post_id}` Reads the post first (it must be the account's own, or a page's), then deletes it. The account's own plain repost (its `repost_id`) is undone as LinkedIn's "Delete repost" does; the reposted post stays. Answers 204 with an empty body. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. ## Query parameters - `account_id` (string, required): The account that writes. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 204 Deleted (empty body). ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a profile's or a company's posts `GET https://direct.lempile.com/api/v1/users/{identifier}/posts` A member's posts, newest first (20 a LinkedIn call; `me` for the account's own), or with `is_company=true` a company page's (`identifier` = its id or universal name). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `identifier` (string, required): The person: provider id (`ACo…`), public identifier, `urn:li:fsd_profile:…`, a profile URL (URL-encoded) or `me`. Example: `ada-sender`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. - `is_company` (boolean): `true`: `identifier` is a company. Default: `false`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/users/ada-sender/posts?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "PostList", "items": [ { "object": "Post", "provider": "LINKEDIN", "id": "7388000000000000000", "social_id": "urn:li:activity:7388000000000000000", "share_url": "https://www.linkedin.com/posts/ada-sender_hello-activity-7388000000000000000-abcd", "text": "Hello\nworld", "date": "2d", "parsed_datetime": "2025-10-25T23:54:22.402Z", "reaction_counter": 12, "comment_counter": 3, "repost_counter": 1, "impressions_counter": 0, "author": { "public_identifier": "ada-sender", "id": "ACoAAAbcdefghijklmnop", "name": "Ada Sender", "is_company": false, "headline": "Founder", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg" }, "permissions": { "can_react": true, "can_share": true, "can_post_comments": true }, "is_repost": false, "attachments": [ { "id": "D1", "type": "img", "url": "https://media.licdn.com/dms/image/x/1280.jpg", "url_expires_at": 1900000000000, "size": { "width": 1280, "height": 1280 }, "mimetype": "image/jpeg", "unavailable": false, "sticker": false } ], "mentions": [] } ], "paging": { "page_count": 1 }, "cursor": "eyJrIjoicG9zdHMiLCJzIjoxLCJ0IjoidG9rLTEifQ" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: React to a post or a comment `POST https://direct.lempile.com/api/v1/posts/reaction` Adds the account's reaction (or the page's, with `as_organization`). Reacting again with another type replaces it on LinkedIn. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `post_id` (string, required): The post: its `social_id` (`urn:li:activity:…`, or a `ugcPost`/`share` urn), its id, or its URL. - `comment_id` (string): React to this comment of the post instead (its id, digits). - `reaction_type` (string): One of: `like`, `celebrate`, `support`, `love`, `insightful`, `funny`. Default: `"like"`. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `post_id` (string, required): The post: its `social_id` (`urn:li:activity:…`, or a `ugcPost`/`share` urn), its id, or its URL. - `comment_id` (string): React to this comment of the post instead (its id, digits). - `reaction_type` (string): One of: `like`, `celebrate`, `support`, `love`, `insightful`, `funny`. Default: `"like"`. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts/reaction?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "post_id": "urn:li:activity:7388000000000000000", "reaction_type": "like" }' ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 201 Created ```json { "object": "ReactionAdded" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Remove a reaction `DELETE https://direct.lempile.com/api/v1/posts/reaction` Direct's own route (Unipile has none): takes the account's reaction back. The fields go in the query or in a JSON body. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `post_id` (string): The post. - `comment_id` (string): The comment, for a reaction on a comment. - `as_organization` (string): The page, for a page's reaction. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `post_id` (string, required): The post, as for adding one. - `comment_id` (string): The comment, when the reaction is on a comment. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/reaction?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "string", "post_id": "string", "comment_id": "string", "as_organization": "string" }' ``` ## Responses ### 200 Removed, or with `?dry_run=true` the plan. ```json { "object": "ReactionRemoved" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a post's comments `GET https://direct.lempile.com/api/v1/posts/{post_id}/comments` A post's comments, or with `comment_id` that comment's replies: newest first by default (`MOST_RECENT`, as Unipile), or in LinkedIn's relevance order (`MOST_RELEVANT`; replies oldest first, as LinkedIn's apps read them). ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. - `sort_by` (string): `MOST_RECENT` (the default, as Unipile's): newest first, page after page. `MOST_RELEVANT`: LinkedIn's relevance order (a comment's replies oldest first). One of: `MOST_RELEVANT`, `MOST_RECENT`. Default: `"MOST_RECENT"`. - `comment_id` (string): List this comment's replies instead (its id, digits). ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000/comments?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "CommentList", "items": [ { "object": "Comment", "id": "7390000000000000001", "post_id": "7388000000000000000", "post_urn": "urn:li:activity:7388000000000000000", "author": "Bob Buyer", "author_details": { "id": "ACoAAAzyxwvutsrqponml", "headline": "Founder", "profile_url": "https://www.linkedin.com/in/bob-buyer", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "network_distance": "DISTANCE_2", "is_company": false }, "date": "2025-10-24T10:00:00.000Z", "text": "Great point", "reaction_counter": 4, "reply_counter": 1 } ], "cursor": "eyJrIjoiY29tbWVudHMiLCJzIjoyfQ", "total_items": 5, "paging": { "start": 0, "page_count": 1, "total_count": 5 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Comment on a post `POST https://direct.lempile.com/api/v1/posts/{post_id}/comments` A comment, or with `comment_id` a reply, with mentions, a link and one image, as the member or as a page. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. ## Query parameters - `account_id` (string): The account that writes, when the body doesn't name it. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Request body `application/json` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string, required): 1,250 characters at most. - `comment_id` (string): Reply to this comment (its id, digits). - `mentions` (array of object): Placed in `text` with `{{0}}`, `{{1}}`… Every mention must be placed. - `name` (string, required): The text shown. - `profile_id` (string, required): A person's provider id (`ACo…`) or a company's id (digits). - `is_company` (boolean) - `external_link` (string): An http(s) link, added at the end of the text unless it's already in it. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. - `attachments` (array of string (binary)): One image (jpg, png, gif or webp). `multipart/form-data` (required) - `account_id` (string, required): The account that writes (or `?account_id=`). Required. - `text` (string, required): 1,250 characters at most. - `comment_id` (string): Reply to this comment (its id, digits). - `mentions` (array of object): Placed in `text` with `{{0}}`, `{{1}}`… Every mention must be placed. - `name` (string, required): The text shown. - `profile_id` (string, required): A person's provider id (`ACo…`) or a company's id (digits). - `is_company` (boolean) - `external_link` (string): An http(s) link, added at the end of the text unless it's already in it. - `as_organization` (string): Write as this company page (its id, digits). The account must be an admin of the page. - `attachments` (array of string (binary)): One image (jpg, png, gif or webp). ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000/comments?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "text": "Agreed, the second point is the one people miss." }' ``` ## Responses ### 200 With `?dry_run=true`: the write's plan. Nothing is sent. ```json { "object": "DryRun", "route": "string", "method": "string", "path": "string", "body": {}, "status": 0, "uploads": [ { "method": "string", "path": "string", "body": {}, "then": "string", "content_type": "string", "file": {} } ], "multiphoto": { "method": "string", "path": "string", "body": {} }, "checked": [ "string" ] } ``` ### 201 Created ```json { "object": "CommentSent", "comment_id": "7390000000000000009" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Delete a comment `DELETE https://direct.lempile.com/api/v1/posts/{post_id}/comments/{comment_id}` Direct's own route (Unipile has none): deletes one of the account's comments. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. - `comment_id` (string, required): The comment's id (digits). Example: `7390000000000000009`. ## Query parameters - `account_id` (string, required): The account that writes. - `dry_run` (boolean): `true`: answer the write's plan (`DryRun`, 200) and send nothing to LinkedIn. Default: `false`. ## Headers - `Idempotency-Key` (string): The same key (per account) never writes twice: a repeat answers the first answer for 7 days; a repeat while the first runs answers 409. ## Example request ```bash curl -sS -X DELETE "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000/comments/7390000000000000009?dry_run=true" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 Deleted, or with `?dry_run=true` the plan. ```json { "object": "CommentDeleted" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 409 `errors/conflict`: a write with this Idempotency-Key is still running. ```json { "title": "Write in progress", "detail": "A write with this key is still running.", "type": "errors/conflict", "status": 409 } ``` ### 422 A write LinkedIn refuses, or that the checks before it refuse: `errors/already_connected`, `errors/already_invited_recently`, `errors/cannot_invite_attendee`, `errors/invalid_recipient`, `errors/cannot_resend_yet`, `errors/limit_exceeded`, `errors/connection_limit_reached`, `errors/no_connection_with_recipient`, `errors/not_allowed_inmail`, `errors/insufficient_credits`, `errors/invalid_post`, `errors/unprocessable_entity`, `errors/invalid_parameters`, `errors/invalid_message` (a voice note over 60 seconds), `errors/action_already_performed` (a message already deleted), `errors/not_supported_by_provider`. ```json { "title": "Already connected", "detail": "This person is already a connection of the account.", "type": "errors/already_connected", "status": 422 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: List a post's reactions `GET https://direct.lempile.com/api/v1/posts/{post_id}/reactions` Who reacted to a post, or with `comment_id` to one of its comments. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `post_id` (string, required): The post: its id (digits), `urn:li:activity:…` (or a `ugcPost`/`share` urn) or its URL. Example: `urn:li:activity:7388000000000000000`. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. - `comment_id` (string): List this comment's reactions instead. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/posts/urn%3Ali%3Aactivity%3A7388000000000000000/reactions?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "PostReactionList", "items": [ { "object": "PostReaction", "value": "PRAISE", "post_id": "7388000000000000000", "author": { "id": "ACoAAAzyxwvutsrqponml", "type": "INDIVIDUAL", "name": "Bob Buyer", "headline": "Head of Sales", "profile_url": "https://www.linkedin.com/in/bob-buyer/", "profile_picture_url": "https://media.licdn.com/dms/image/x/400.jpg", "public_identifier": "bob-buyer", "network_distance": "SECOND_DEGREE" } } ], "cursor": null, "paging": { "cursor": null, "start": 0, "page_count": 1, "total_count": 1 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Search people, companies, posts or jobs `POST https://direct.lempile.com/api/v1/linkedin/search` LinkedIn's classic search in Unipile's shape: people (10 a LinkedIn call), companies (keywords only), posts (3 by default) and jobs (25 a call), by Unipile's body, by a LinkedIn search page's `url`, or by `cursor` alone. `api: sales_navigator` or `recruiter` answers 403 `errors/feature_not_subscribed`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. - `cursor` (string): A cursor a previous page gave. ## Request body `application/json` (required) - Option 1: CompanySearchRequest - `api` (string): One of: `classic`. Default: `"classic"`. - `category` (string, required): One of: `companies`. - `keywords` (string, required): Required. - `limit` (integer): The query's `limit` wins over this one. Default: `10`. - Option 2: PeopleSearchRequest - `api` (string): One of: `classic`. Default: `"classic"`. - `category` (string, required) - `keywords` (string) - `industry` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `location` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `company` (array of string): Current company ids: a company's employees. - `past_company` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `school` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `service` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `profile_language` (array of string) - `network_distance` (array of integer) - `connections_of` (array of string): Members' provider ids (or public identifiers: one read each). - `followers_of` (array of string): Members' provider ids (or public identifiers: one read each). - `open_to` (array of string) - `advanced_keywords` (object) - `first_name` (string) - `last_name` (string) - `title` (string) - `company` (string) - `school` (string) - Option 3: PostSearchRequest - `api` (string): One of: `classic`. Default: `"classic"`. - `category` (string, required) - `keywords` (string) - `sort_by` (string): One of: `relevance`, `date`. - `date_posted` (string): One of: `past_day`, `past_week`, `past_month`. - `content_type` (string): One of: `videos`, `images`, `live_videos`, `collaborative_articles`, `documents`, `jobs`. - `posted_by` (object) - `member` (array of string) - `company` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `me` (boolean) - `first_connections` (boolean) - `people_you_follow` (boolean) - `mentioning` (object) - `member` (array of string) - `company` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `author` (object) - `industry` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `company` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `keywords` (string) - Option 4: JobSearchRequest - `api` (string): One of: `classic`. Default: `"classic"`. - `category` (string, required) - `keywords` (string) - `sort_by` (string): One of: `relevance`, `date`. - `date_posted` (number): Days since today. - `region` (string): A location id (the search's own place). - `location` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `location_within_area` (number): Miles. - `industry` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `role` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `company` (array of string): LinkedIn ids (digits): `GET /linkedin/search/parameters` finds them. - `seniority` (array of string) - `function` (array of string) - `job_type` (array of string) - `presence` (array of string) - `easy_apply` (boolean) - `has_verifications` (boolean) - `under_10_applicants` (boolean) - `in_your_network` (boolean) - `fair_chance_employer` (boolean) - Option 5: UrlSearchRequest - `url` (string, required) - Option 6: CursorSearchRequest - `cursor` (string, required) ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin/search?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "api": "classic", "category": "people", "keywords": "founder", "network_distance": [ 2 ], "company": [ "1035" ] }' ``` ## Responses ### 200 OK ```json { "object": "LinkedinSearch", "items": [ { "type": "COMPANY", "id": "1035", "name": "Example Co", "location": "Cambridge, MA", "profile_url": "https://www.linkedin.com/company/example-co/", "industry": "Software Development", "summary": "Grow better", "logo": "https://media.licdn.com/dms/image/logo1035/100.jpg", "followers_count": 1200000 }, { "type": "COMPANY", "id": "2046", "name": "Small Shop", "location": null, "profile_url": "https://www.linkedin.com/company/small-shop/", "industry": "Retail", "summary": null, "followers_count": 845 } ], "config": { "params": { "api": "classic", "category": "companies", "keywords": "x" } }, "paging": { "start": 0, "page_count": 2, "total_count": 2 }, "cursor": null } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Find search parameter ids `GET https://direct.lempile.com/api/v1/linkedin/search/parameters` LinkedIn's own typeahead: the ids its search filters take, for `LOCATION`, `REGION`, `PEOPLE`, `CONNECTIONS`, `COMPANY`, `SCHOOL`, `INDUSTRY`, `JOB_TITLE` and `SKILL` (one LinkedIn call). Other classic types answer 501; Sales Navigator and Recruiter types or services answer 403 `errors/feature_not_subscribed`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. - `type` (string, required): LOCATION, REGION, PEOPLE, CONNECTIONS, COMPANY, SCHOOL, INDUSTRY, JOB_TITLE or SKILL. - `keywords` (string): What to look for (required). - `service` (string): Only CLASSIC is served. One of: `CLASSIC`, `RECRUITER`, `SALES_NAVIGATOR`. Default: `"CLASSIC"`. - `limit` (integer): Items per page: 1 to 100 (default 10). Default: `10`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/linkedin/search/parameters?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "LinkedinSearchParametersList", "items": [ { "object": "LinkedinSearchParameter", "id": "105015875", "title": "Paris, Île-de-France, France" } ], "paging": { "page_count": 1 } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Retrieve a company page `GET https://direct.lempile.com/api/v1/linkedin/company/{identifier}` A company page by its public identifier, id, urn or link (one LinkedIn call). Its employees: `POST /linkedin/search` with `category: people` and `company: [id]`. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Path parameters - `identifier` (string, required): The company's public identifier (`example-co`), id (`1035`), urn or link. ## Query parameters - `account_id` (string, required): The account that reads: a Lempile LinkedIn account id (`GET /accounts` lists them). Example: `00000000-0000-4000-8000-0000000000a1`. ## Example request ```bash curl -sS "https://direct.lempile.com/api/v1/linkedin/company/{identifier}?account_id=00000000-0000-4000-8000-0000000000a1" \ -H "X-API-KEY: $DIRECT_API_KEY" ``` ## Responses ### 200 OK ```json { "object": "CompanyProfile", "id": "1035", "name": "Example Co", "description": "We make outbound simple.", "entity_urn": "urn:li:fsd_company:1035", "public_identifier": "example-co", "profile_url": "https://www.linkedin.com/company/example-co/", "tagline": "Grow better", "followers_count": 5400, "is_following": true, "is_employee": false, "hashtags": [ { "title": "#outbound" } ], "messaging": { "is_enabled": false }, "claimed": true, "viewer_permissions": { "canReadAdminDashboard": false }, "organization_type": "PRIVATELY_HELD", "locations": [ { "is_headquarter": true, "country": "US", "city": "Boston", "street": [ "1 Main St" ], "postalCode": "02110", "area": "MA" } ], "industry": [ "Software Development" ], "employee_count": 120, "employee_count_range": { "from": 51, "to": 200 }, "website": "https://example.com", "foundation_date": "2015" } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Lempile Direct: Raw LinkedIn data (reads) `POST https://direct.lempile.com/api/v1/linkedin` Unipile's raw route, for reads: a GET of one of LinkedIn's API addresses with the account's own session, paced and counted like every read. Writes (any other method, or an `action=`) answer 501: they go through Direct's own routes, each checked and dry-runnable. ## Authentication Send your Lempile Direct API key in the `X-API-KEY` header. ## Request body `application/json` (required) - `account_id` (string, required) - `request_url` (string, required) - `method` (string): One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `"GET"`. - `query_params` (object): Added to the address's query string. - `encoding` (boolean): `false`: query_params are added as given (not URL-encoded). Default: `true`. - `headers` (object): Ignored: the account's own session headers are sent. - `body` (object): Ignored (reads only). ## Example request ```bash curl -sS -X POST "https://direct.lempile.com/api/v1/linkedin" \ -H "X-API-KEY: $DIRECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "00000000-0000-4000-8000-0000000000a1", "request_url": "https://www.linkedin.com/voyager/api/me" }' ``` ## Responses ### 200 OK ```json { "object": "LinkedinRawData", "data": { "data": { "plainId": 1 } } } ``` ### 400 A required parameter missing (`errors/missing_parameters`), or one that isn't valid (`errors/invalid_parameters`, `errors/invalid_url`, `errors/content_too_large`). ```json { "title": "Missing parameters", "detail": "account_id is required.", "type": "errors/missing_parameters", "status": 400 } ``` ### 401 `errors/missing_credentials`: the `X-API-KEY` header is missing, wrong or revoked. `errors/disconnected_account`: LinkedIn signed the account out (or asks it for a security check); reconnect it. ```json { "title": "Missing credentials", "detail": "The X-API-KEY header is missing or wrong.", "type": "errors/missing_credentials", "status": 401 } ``` ### 403 LinkedIn refused it for this account (`errors/resource_access_restricted`, `errors/account_restricted`, `errors/action_required`), or the account doesn't write through Direct (`errors/insufficient_permissions`), or it needs Sales Navigator or Recruiter (`errors/feature_not_subscribed`). ```json { "title": "Access restricted", "detail": "LinkedIn refused this read for this account.", "type": "errors/resource_access_restricted", "status": 403 } ``` ### 404 `errors/resource_not_found`: an unknown or removed account, an account of another workspace (answered exactly the same), or nothing on LinkedIn with this id. ```json { "title": "Account not found", "detail": "No account with this account_id is enabled here.", "type": "errors/resource_not_found", "status": 404 } ``` ### 429 `errors/too_many_requests`: LinkedIn asked the account to slow down (it rests 15 minutes), its daily calls or writes are used up, or it's busy with other calls. Wait `retry_after` seconds. ```json { "title": "Too many requests", "detail": "LinkedIn asked this account to slow down.", "type": "errors/too_many_requests", "status": 429, "retry_after": 900 } ``` ### 500 `errors/provider_error`: LinkedIn answered something Direct couldn't read (nothing was written). `errors/unexpected_error`: Direct failed. ```json { "title": "Provider error", "detail": "LinkedIn answered 500.", "type": "errors/provider_error", "status": 500 } ``` ### 501 `errors/feature_not_implemented`: Direct doesn't do this (yet). A Unipile client can fall back to Unipile. ```json { "title": "Missing feature", "detail": "Lempile Direct reads LinkedIn only (see /api/v1/users and /api/v1/posts).", "type": "errors/feature_not_implemented", "status": 501 } ``` ### 503 `errors/network_down`: the account's proxy or browser server didn't answer. Nothing was sent. ```json { "title": "Network down", "detail": "The account's proxy didn't connect.", "type": "errors/network_down", "status": 503 } ``` ### 504 `errors/request_timeout`: LinkedIn didn't answer in time. On a write: an unknown outcome (it may have been sent); it's never retried and its Idempotency-Key never writes again. ```json { "title": "Unknown outcome", "detail": "LinkedIn may have received this write; it isn't tried again. Check before sending it once more.", "type": "errors/request_timeout", "status": 504 } ``` --- # Google Search API: Overview > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Lempile exposes a SerpApi-compatible Google Search API at `https://api.lempile.com`. Fresh browser-backed searches are queue-first. A new request creates an archived search record, returns `202 Accepted`, and gives you a `search_metadata.json_endpoint` to poll. The runner processes the job when capacity is available. Cached successful searches may return `200 OK` immediately. ```text client -> Lempile API -> queue -> runner lease -> browser -> Google -> parser -> archive ``` ## 60-Second Quickstart Create a search: ```bash curl -sS "https://api.lempile.com/search.json?engine=google&q=site%3Alinkedin.com%2Fposts%2F%20%22AI%20outreach%22&gl=us&hl=en&tbs=qdr%3Am" \ -H "X-API-KEY: your-api-key" ``` Poll the returned `search_metadata.json_endpoint`: ```bash curl -sS "https://api.lempile.com/searches/{search_id}.json" \ -H "X-API-KEY: your-api-key" ``` Stop polling when `search_metadata.status` is `Success` or `Error`. ## Lifecycle | Step | API result | What it means | | --- | --- | --- | | Create | `202`, `status: "Processing"` | The search was accepted and queued. | | Poll | `202`, `status: "Processing"` | The search is still queued or running. | | Success | `200`, `status: "Success"` | Parsed Google results are available. | | Error | `200`, `status: "Error"` | The search finished with a real failure reason. | Queue-specific fields may appear inside `search_metadata`: ```json { "search_metadata": { "id": "search_id", "status": "Processing", "status_detail": "Queued", "queued_at": "2026-06-01T08:00:00Z", "json_endpoint": "https://api.lempile.com/searches/search_id.json" } } ``` `status_detail` can show internal states such as `Queued`, `Running`, `completed`, or a terminal error detail. The public SerpApi-compatible `status` remains `Processing`, `Success`, or `Error`. ## Production Host Use the public host: ```text https://api.lempile.com ``` Do not use staging URLs for production integrations. ## Authentication Pass an API key with `search.write` for create routes and `search.read` for archive routes. Use either: - `X-API-KEY` header - `api_key` query parameter ## Capacity Model Production is conservative by design: one active Google browser job per runner by default. Bursts are queued instead of launching unlimited browser sessions. If a workspace exceeds its queue cap, Lempile returns `429 workspace_search_queue_full`. --- # Google Search API: Create Search > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Use `GET /search.json` for JSON clients. `GET /search` is also supported for SerpApi compatibility. Fresh non-cached searches are queue-first and normally return `202 Accepted`. ## Request ```bash curl -sS "https://api.lempile.com/search.json?engine=google&q=Coffee&location=Austin%2C+Texas%2C+United+States&hl=en&gl=us" \ -H "X-API-KEY: your-api-key" ``` ## Normal Fresh Response ```json { "search_metadata": { "id": "search_id", "status": "Processing", "status_detail": "Queued", "json_endpoint": "https://api.lempile.com/searches/search_id.json", "raw_html_file": "https://api.lempile.com/searches/search_id.html", "pixel_position_endpoint": "https://api.lempile.com/searches/search_id.json_with_pixel_position" }, "search_parameters": { "engine": "google", "q": "Coffee", "google_domain": "google.com", "device": "desktop" } } ``` ## Cached Response If an identical successful search is still cached, Lempile may return `200 OK` immediately with final `organic_results`. ## Queue Full Response If the workspace already has too many pending searches, Lempile returns `429`. ```json { "error": "Workspace Google search queue is full.", "error_code": "workspace_search_queue_full" } ``` ## Important Parameters | Parameter | Purpose | | --- | --- | | `q` | Google query. Supports operators such as `site:`, `inurl:`, and quoted phrases. | | `location` | Human-readable location. Do not combine with `uule`, `lat`, or `lon`. | | `gl` | Country localization, such as `us`, `ca`, or `fr`. | | `hl` | Interface language, such as `en` or `fr`. | | `tbs` | Google filter string, such as `qdr:m`. | | `device` | `desktop`, `mobile`, or `tablet`. Defaults to `desktop`. | | `no_cache` | Set to `true` to force a fresh queued browser run. | `async` is still accepted for compatibility, but fresh searches are queue-first by default whether or not `async=true` is sent. --- # Google Search API: Poll Search > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Use the `search_metadata.json_endpoint` returned by create-search responses. ```bash curl -sS "https://api.lempile.com/searches/{search_id}.json" \ -H "X-API-KEY: your-api-key" ``` ## Processing Response Queued or running searches return `202 Accepted`. ```json { "search_metadata": { "id": "search_id", "status": "Processing", "status_detail": "Running", "started_at": "2026-06-01T08:01:00Z", "lease_expires_at": "2026-06-01T08:03:00Z" } } ``` Keep polling until `search_metadata.status` is not `Processing`. ## Success Response Successful searches return `200 OK`. ```json { "search_metadata": { "id": "search_id", "status": "Success", "processed_at": "2026-06-01T08:02:00Z" }, "organic_results": [] } ``` ## Error Response Failed searches return `200 OK` with `status: "Error"` and a clear `error` message. ```json { "error": "Google returned a block or consent challenge page.", "search_metadata": { "id": "search_id", "status": "Error", "status_detail": "google_search_failed" } } ``` ## Polling Guidance - Poll every 3-5 seconds for normal requests. - Treat `Processing` as normal while the queue or browser runner is working. - Use `status_detail` for dashboard/debug display only; client state machines should rely on `search_metadata.status`. --- # Google Search API: Retrieve Artifacts > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Every search record can expose archive endpoints through `search_metadata`. ## JSON ```text GET /searches/{search_id}.json ``` Returns the current or final SerpApi-shaped JSON payload. ## Raw HTML ```text GET /searches/{search_id}.html ``` Returns the raw Google HTML captured by the browser when the search has completed successfully. If the search is still queued or running, poll the JSON endpoint first. ## Pixel Positions ```text GET /searches/{search_id}.json_with_pixel_position ``` Returns the normal JSON payload plus pixel-position data when available. ## Authentication Archive endpoints require `search.read`. Use either `X-API-KEY` or `api_key`. --- # Google Search API: Errors And Limits > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Lempile uses queue-first behavior so temporary runner pressure does not immediately fail fresh searches. Most new searches return `202 Accepted` and are resolved later through polling. ## Common Errors | HTTP | Code | Meaning | | --- | --- | --- | | `400` | `invalid_search_parameters` | The request has invalid or conflicting search parameters. | | `401` | `unauthorized` | The API key is missing or invalid. | | `402` | `subscription_required` | The workspace has no active trial or subscription. Start the free trial or add a card in the dashboard (Subscription), and its keys work again right away. | | `403` | `forbidden` | The API key does not include the required search scope. | | `429` | `workspace_search_queue_full` | The workspace has too many queued or running Google searches. | | `503` | `service_unavailable` | Platform configuration or authentication failed before queueing was possible. | ## Google Blocks And CAPTCHA If Google returns a block, consent, or CAPTCHA page, Lempile records safe diagnostics in the final JSON response. Diagnostics can include: - CAPTCHA task type - provider status - solver request id - whether a token was applied - whether Google still returned a block page Diagnostics never include raw proxy credentials, cookies, CAPTCHA tokens, API keys, or passwords. ## Queue Limits Production runs conservatively: - One active Google browser job per runner by default. - Burst traffic queues instead of launching unlimited browser sessions. - A single workspace can be rate-limited to protect the shared runner fleet. When higher throughput is needed, add more runner capacity rather than increasing browser concurrency on one machine. --- # Google Search API API reference > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Base URL: `https://api.lempile.com`. Authentication: your API key in the `X-API-KEY` header. OpenAPI document: [google-search.json](https://lempile.com/docs-assets/openapi/google-search.json). ## SerpApi Google Search - [Search Google](https://lempile.com/docs/google-search/reference/search): `GET /search` - [Search Google as JSON](https://lempile.com/docs/google-search/reference/search-json): `GET /search.json` - [Retrieve archived Google search JSON](https://lempile.com/docs/google-search/reference/retrieve-json): `GET /searches/{searchId}.json` - [Retrieve archived Google search HTML](https://lempile.com/docs/google-search/reference/retrieve-html): `GET /searches/{searchId}.html` - [Retrieve archived Google search JSON with pixel positions](https://lempile.com/docs/google-search/reference/retrieve-json-with-pixel-position): `GET /searches/{searchId}.json_with_pixel_position` --- # Google Search API: Search Google > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `GET https://api.lempile.com/search` Create a SerpApi-compatible Google Search through the Lempile browser-backed search pipeline. Fresh non-cached searches are queue-first and normally return HTTP 202 with a `search_metadata.id` plus archive endpoints. Poll `search_metadata.json_endpoint` until `search_metadata.status` becomes `Success` or `Error`. Cached successful searches may return HTTP 200 immediately. Authentication is accepted as either the `api_key` query parameter or the `X-API-KEY` header. This route requires the `search.write` scope. ## Authentication Send your Google Search API API key in the `X-API-KEY` header. ## Query parameters - `engine` (string): Search engine to use. Defaults to `google`; other engines return a SerpApi-style error. One of: `google`. Default: `"google"`. - `q` (string, required): Google search query. Supports normal Google operators such as `site:`, `inurl:`, and quoted phrases. For LinkedIn discovery, use queries such as `site:linkedin.com/posts/ "business inspiration"`. - `location` (string): Human-readable origin location. Cannot be combined with `uule`, `lat`, or `lon`. - `uule` (string): Google encoded location token. Cannot be combined with `location`, `lat`, `lon`, or `radius`. - `lat` (number): GPS latitude. Must be paired with `lon` and cannot be combined with `location` or `uule`. - `lon` (number): GPS longitude. Must be paired with `lat` and cannot be combined with `location` or `uule`. - `radius` (integer): Distance in meters used to bias local results. Desktop accepts 1..199; mobile and tablet accept 1..1000. - `google_domain` (string): Google domain to use. Defaults to `google.com`. Default: `"google.com"`. - `hl` (string): Google interface language, such as `en` or `fr`. - `gl` (string): Country code used for Google search localization, such as `us`, `uk`, or `fr`. - `cr` (string): Country restriction, for example `countryFR|countryDE`. - `lr` (string): Language restriction, for example `lang_fr|lang_de`. - `tbs` (string): Advanced Google filter string, such as `qdr:m` for the last month. - `safe` (string): Adult content filter. Common values are `active` and `off`. One of: `active`, `off`. - `nfpr` (integer): Set to `1` to exclude auto-corrected query results when possible. One of: `0`, `1`. - `filter` (integer): Set to `0` to disable similar-results and omitted-results filtering; defaults to Google behavior. One of: `0`, `1`. - `tbm` (string): Google vertical such as `isch`, `lcl`, `vid`, `nws`, `shop`, or `pts`. Organic web search with `tbm` omitted is the current parity milestone; vertical module parity is best-effort. - `start` (integer): Result offset for pagination. `0` is page 1, `10` is page 2, and so on. Default: `0`. - `device` (string): Browser profile type used for the run. One of: `desktop`, `mobile`, `tablet`. Default: `"desktop"`. - `no_cache` (boolean): Set to `true` to bypass the 1-hour normalized-parameter cache and force a fresh queued browser-backed Google run. Cached successful searches may return immediately. Default: `false`. - `async` (boolean): Accepted for SerpApi compatibility. Lempile is queue-first by default, so fresh non-cached searches return HTTP 202 whether or not `async=true` is provided. Default: `false`. - `zero_trace` (boolean): Accepted for SerpApi compatibility, but Lempile still archives requests for dashboard inspection and search retrieval. Default: `false`. - `ludocid` (string): Google place CID used for local or knowledge graph result variants. - `lsig` (string): Google local result signature when Google requires it for a specific view. - `kgmid` (string): Google Knowledge Graph machine id. - `si` (string): Google cached search parameter token. - `ibp` (string): Google layout or expansion parameter for selected modules. - `uds` (string): Google filter token captured from a previous result page. - `api_key` (string): API key with `search.write` for search creation routes or `search.read` for archive routes. `X-API-KEY` may be used instead. - `output` (string): Requested output format. `json` returns SerpApi-shaped JSON. `html` returns raw Google HTML only when a successful cached run is available; fresh browser runs still return HTTP 202 with polling metadata. One of: `json`, `html`. Default: `"json"`. ## Example request ```bash curl -sS "https://api.lempile.com/search" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 A cached successful search was returned immediately, or `output=html` returned cached raw Google HTML. ```json { "error": "string", "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" }, "search_information": { "query_displayed": "string", "total_results": 0, "time_taken_displayed": 0, "organic_results_state": "string", "page_number": 0 }, "organic_results": [ { "position": 0, "title": "string", "link": "https://example.com", "displayed_link": "string", "snippet": "string", "snippet_highlighted_words": [ "string" ], "source": "string" } ], "related_searches": [ {} ], "pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "serpapi_pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "error_diagnostics": {} } ``` ### 202 Fresh search accepted and queued. Poll `search_metadata.json_endpoint` until the public status changes from `Processing` to `Success` or `Error`. ```json { "search_metadata": { "id": "63c57c0222e0470397fda526", "status": "Processing", "status_detail": "Queued", "runner_status_detail": "queued", "created_at": "2026-06-01 07:35:00 UTC", "queued_at": "2026-06-01 07:35:00 UTC", "json_endpoint": "https://api.lempile.com/searches/63c57c0222e0470397fda526.json", "raw_html_file": "https://api.lempile.com/searches/63c57c0222e0470397fda526.html", "pixel_position_endpoint": "https://api.lempile.com/searches/63c57c0222e0470397fda526.json_with_pixel_position" }, "search_parameters": { "engine": "google", "q": "Coffee", "google_domain": "google.com", "device": "desktop" } } ``` ### 400 Invalid search parameters. ```json { "error": "string", "error_code": "string" } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "error_code": "string" } ``` ### 403 The API key does not include the required `search.write` scope. ```json { "error": "string", "error_code": "string" } ``` ### 429 Workspace has reached the pending Google search queue cap. ```json { "error": "Workspace Google search queue is full. Please wait for existing searches to finish before adding more.", "error_code": "workspace_search_queue_full" } ``` ### 503 Platform, configuration, or storage dependency is temporarily unavailable. New searches normally queue when the runner is at capacity or briefly unavailable. ```json { "error": "string", "error_code": "string" } ``` --- # Google Search API: Search Google as JSON > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `GET https://api.lempile.com/search.json` JSON-suffixed alias for `GET /search`. Fresh non-cached searches are queue-first and return HTTP 202 with polling endpoints. Poll `search_metadata.json_endpoint` until `search_metadata.status` becomes `Success` or `Error`. ## Authentication Send your Google Search API API key in the `X-API-KEY` header. ## Query parameters - `engine` (string): Search engine to use. Defaults to `google`; other engines return a SerpApi-style error. One of: `google`. Default: `"google"`. - `q` (string, required): Google search query. Supports normal Google operators such as `site:`, `inurl:`, and quoted phrases. For LinkedIn discovery, use queries such as `site:linkedin.com/posts/ "business inspiration"`. - `location` (string): Human-readable origin location. Cannot be combined with `uule`, `lat`, or `lon`. - `google_domain` (string): Google domain to use. Defaults to `google.com`. Default: `"google.com"`. - `hl` (string): Google interface language, such as `en` or `fr`. - `gl` (string): Country code used for Google search localization, such as `us`, `uk`, or `fr`. - `tbs` (string): Advanced Google filter string, such as `qdr:m` for the last month. - `start` (integer): Result offset for pagination. `0` is page 1, `10` is page 2, and so on. Default: `0`. - `device` (string): Browser profile type used for the run. One of: `desktop`, `mobile`, `tablet`. Default: `"desktop"`. - `no_cache` (boolean): Set to `true` to bypass the 1-hour normalized-parameter cache and force a fresh queued browser-backed Google run. Cached successful searches may return immediately. Default: `false`. - `api_key` (string): API key with `search.write` for search creation routes or `search.read` for archive routes. `X-API-KEY` may be used instead. ## Example request ```bash curl -sS "https://api.lempile.com/search.json" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 A cached successful search was returned immediately. ```json { "error": "string", "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" }, "search_information": { "query_displayed": "string", "total_results": 0, "time_taken_displayed": 0, "organic_results_state": "string", "page_number": 0 }, "organic_results": [ { "position": 0, "title": "string", "link": "https://example.com", "displayed_link": "string", "snippet": "string", "snippet_highlighted_words": [ "string" ], "source": "string" } ], "related_searches": [ {} ], "pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "serpapi_pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "error_diagnostics": {} } ``` ### 202 Fresh search accepted and queued. ```json { "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" } } ``` ### 400 Invalid search parameters. ```json { "error": "string", "error_code": "string" } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "error_code": "string" } ``` ### 403 The API key does not include the required `search.write` scope. ```json { "error": "string", "error_code": "string" } ``` ### 429 Workspace has reached the pending Google search queue cap. ```json { "error": "Workspace Google search queue is full. Please wait for existing searches to finish before adding more.", "error_code": "workspace_search_queue_full" } ``` ### 503 Platform, configuration, or storage dependency is temporarily unavailable. New searches normally queue when the runner is at capacity or briefly unavailable. ```json { "error": "string", "error_code": "string" } ``` --- # Google Search API: Retrieve archived Google search JSON > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `GET https://api.lempile.com/searches/{searchId}.json` Poll or retrieve the JSON archive for a Google search. Pending internal `Queued`, `Running`, and legacy `Processing` rows return HTTP 202 with public status `Processing`. Final rows return HTTP 200 with public status `Success` or `Error`. This route requires the `search.read` scope. ## Authentication Send your Google Search API API key in the `X-API-KEY` header. ## Path parameters - `searchId` (string, required): Search archive id returned in `search_metadata.id`. ## Query parameters - `api_key` (string): API key with `search.write` for search creation routes or `search.read` for archive routes. `X-API-KEY` may be used instead. ## Example request ```bash curl -sS "https://api.lempile.com/searches/{searchId}.json" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 Final archived search JSON. ```json { "search_metadata": { "id": "63c57c0222e0470397fda526", "status": "Success", "status_detail": "completed", "created_at": "2026-06-01 07:35:00 UTC", "processed_at": "2026-06-01 07:35:22 UTC", "total_time_taken": 18.42, "json_endpoint": "https://api.lempile.com/searches/63c57c0222e0470397fda526.json", "raw_html_file": "https://api.lempile.com/searches/63c57c0222e0470397fda526.html", "pixel_position_endpoint": "https://api.lempile.com/searches/63c57c0222e0470397fda526.json_with_pixel_position" }, "search_parameters": { "engine": "google", "q": "Coffee", "google_domain": "google.com", "device": "desktop" }, "organic_results": [] } ``` ### 202 Search is queued or running. Continue polling. ```json { "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" } } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "error_code": "string" } ``` ### 403 The API key does not include the required `search.read` scope. ```json { "error": "string", "error_code": "string" } ``` ### 404 Search archive id was not found for the current workspace. ```json { "error": "Search not found." } ``` ### 503 Platform, configuration, or storage dependency is temporarily unavailable. New searches normally queue when the runner is at capacity or briefly unavailable. ```json { "error": "string", "error_code": "string" } ``` --- # Google Search API: Retrieve archived Google search HTML > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `GET https://api.lempile.com/searches/{searchId}.html` Retrieve raw Google HTML for a successful browser-backed search. Pending rows return HTTP 202 with the same processing JSON used by archive polling. This route requires the `search.read` scope. ## Authentication Send your Google Search API API key in the `X-API-KEY` header. ## Path parameters - `searchId` (string, required): Search archive id returned in `search_metadata.id`. ## Query parameters - `api_key` (string): API key with `search.write` for search creation routes or `search.read` for archive routes. `X-API-KEY` may be used instead. ## Example request ```bash curl -sS "https://api.lempile.com/searches/{searchId}.html" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 Raw Google Search HTML captured from the browser run. ```text string ``` ### 202 Search is queued or running. Continue polling the JSON endpoint. ```json { "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" } } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "error_code": "string" } ``` ### 403 The API key does not include the required `search.read` scope. ```json { "error": "string", "error_code": "string" } ``` ### 404 Search archive id was not found for the current workspace. ```json { "error": "Search not found." } ``` --- # Google Search API: Retrieve archived Google search JSON with pixel positions > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `GET https://api.lempile.com/searches/{searchId}.json_with_pixel_position` Retrieve final or pending search JSON plus `pixel_positions` when available. Pending rows return HTTP 202 and should be polled until the public status is no longer `Processing`. This route requires the `search.read` scope. ## Authentication Send your Google Search API API key in the `X-API-KEY` header. ## Path parameters - `searchId` (string, required): Search archive id returned in `search_metadata.id`. ## Query parameters - `api_key` (string): API key with `search.write` for search creation routes or `search.read` for archive routes. `X-API-KEY` may be used instead. ## Example request ```bash curl -sS "https://api.lempile.com/searches/{searchId}.json_with_pixel_position" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 Archived search JSON with pixel position data. ```json { "error": "string", "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" }, "search_information": { "query_displayed": "string", "total_results": 0, "time_taken_displayed": 0, "organic_results_state": "string", "page_number": 0 }, "organic_results": [ { "position": 0, "title": "string", "link": "https://example.com", "displayed_link": "string", "snippet": "string", "snippet_highlighted_words": [ "string" ], "source": "string" } ], "related_searches": [ {} ], "pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "serpapi_pagination": { "current": 0, "previous": "https://example.com", "next": "https://example.com", "other_pages": {} }, "error_diagnostics": {}, "pixel_positions": {} } ``` ### 202 Search is queued or running. Continue polling. ```json { "search_metadata": { "id": "string", "status": "Processing", "status_detail": "string", "runner_status_detail": "string", "json_endpoint": "https://example.com", "pixel_position_endpoint": "https://example.com", "raw_html_file": "https://example.com", "created_at": "string", "queued_at": "string", "started_at": "string", "lease_expires_at": "string", "processed_at": "string", "google_url": "string", "total_time_taken": 0, "captcha_autosolve": {} }, "search_parameters": { "engine": "google", "q": "string", "location_requested": "string", "location_used": "string", "google_domain": "string", "hl": "string", "gl": "string", "tbs": "string", "start": 0, "device": "desktop" } } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "error_code": "string" } ``` ### 403 The API key does not include the required `search.read` scope. ```json { "error": "string", "error_code": "string" } ``` ### 404 Search archive id was not found for the current workspace. ```json { "error": "Search not found." } ``` --- # Email Finder: Overview Queue-first B2B email discovery and lightweight identity resolution from LinkedIn profile URLs or public identifiers. Email Finder uses Lempile-owned resolver logic; it does not call Apify or an external contact database. When a workspace has a Ready connected LinkedIn account, Lempile may use that same-workspace account to resolve the visible profile identity before company domain and email verification. ## Quick Start Create a key in your console: **Email Finder → Create key** ([lempile.com/dashboard/email-finder](https://lempile.com/dashboard/email-finder)). It only works for the Email Finder and is shown once. Create a job: ```bash curl -X POST "https://api.lempile.com/email-finder" \ -H "X-API-KEY: $LEMPILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedin_url_or_ids": [ "https://www.linkedin.com/in/nick-hampson1", "swaroop-mehetar" ] }' ``` The API returns `202` with a job id and polling endpoints. Poll the job until `status` becomes `Completed` or `Error`, then fetch items or CSV. You can also pass safe identity hints. This is the preferred shape when you already know the person or company because it lets Lempile resolve from company website evidence without waiting on Google Search evidence. ```json { "records": [ { "linkedin_url_or_id": "https://www.linkedin.com/in/mike-novak-34b8733", "first_name": "Mike", "last_name": "Novak", "company": "Louder Than Digital", "company_domain": "louderthandigital.com" } ] } ``` ## Behavior - Email Finder returns business/work emails only by default. - Gmail, Yahoo, Outlook, iCloud, disposable, and similar personal domains are suppressed by default. - The resolver checks cache, input hints, same-workspace LinkedIn profile identity when available, public LinkedIn profile previews, company websites, company email patterns, and ranked candidate generation. - Verification is internal-owned by default: syntax, personal/disposable/role suppression, DNS/MX, known provider/domain behavior, SMTP-safe catch-all/candidate checks, and cached results. - External verifier fallback is disabled by default. Suppressed, impossible, MX-missing, and internally rejected candidates are never sent to an external provider. - Results include safe identity fields such as name, title, headline, company, company URL, domain, and location when evidence supports them. - Google Search evidence is a bounded fallback only when the Google Search cookie/proxy pool is ready. If it is not ready, items finish deterministically with a safe `Not Found` reason instead of creating stuck Search rows. - Connected LinkedIn account usage is controlled: same workspace only, leased, low concurrency, and used for profile identity resolution only. - Provider payloads, API keys, LinkedIn cookies, proxy credentials, CAPTCHA tokens, and passwords are never returned in diagnostics. ## Pricing - You pay only for emails the company's mail server confirmed: items with status `Found`. `Uncertain`, `Not Found`, `Suppressed` and `Error` are free. - Each billing period includes 1,000 found emails. After that, €10 per 1,000 (£10, $11, C$15.40, A$17.60 or NZ$18.70 per 1,000), on the same monthly invoice. - During the 7-day trial, the Email Finder stops at the 1,000 included: new jobs answer `402 email_finder_trial_limit` until the subscription starts. - `GET /email-finder/usage` returns this period's found emails, the billable ones and the estimated amount. ## Item Statuses - `Found`: an exact work email was observed publicly or a generated candidate was confirmed by the internal SMTP-safe verifier. - `Uncertain`: the domain is catch-all, protected, greylisted, unknown/timeout, or a generated candidate has strong pattern/MX evidence without deliverability proof. - `Not Found`: no safe work email was found, identity/company/domain was not resolved, or public evidence was unavailable. - `Suppressed`: an email was found but suppressed by policy, usually personal or disposable. - `Error`: the item failed after retries or the input was invalid. `Uncertain` items include a safe `confidence_bucket` and `uncertain_bucket`. `confidence_bucket: "likely"` means Lempile has strong identity, domain, and pattern evidence, but the receiving mail server did not allow strict mailbox proof. Typical `uncertain_bucket` values are `catch_all`, `protected`, `greylisted`, `timeout`, `temporary_failure`, `unknown`, and `low`. Greylisted, timeout, and temporary-failure candidates may receive background SMTP-safe retries with `retry_after` and `retry_strategy`. A job can be `Completed` while some items still wait for a retry: `counts.retrying` says how many, and `next_retry_at` when the next one runs. Read the job again after `next_retry_at` until `counts.retrying` is 0. A retried item can become `Found`, and is billed only then. ## Endpoints - `POST /email-finder` - `GET /email-finder/jobs/{jobId}` - `GET /email-finder/jobs/{jobId}/items` - `GET /email-finder/jobs/{jobId}.csv` - `GET /email-finder/usage` Scopes: - Create jobs: `email_finder.write` - Read jobs/results: `email_finder.read` --- # Email Finder API reference Base URL: `https://api.lempile.com`. Authentication: your API key in the `X-API-KEY` header. OpenAPI document: [email-finder.json](https://lempile.com/docs-assets/openapi/email-finder.json). ## Email Finder - [Create email finder job](https://lempile.com/docs/email-finder/reference/create-job): `POST /email-finder` - [Get email finder usage](https://lempile.com/docs/email-finder/reference/get-usage): `GET /email-finder/usage` - [Get email finder job](https://lempile.com/docs/email-finder/reference/get-job): `GET /email-finder/jobs/{jobId}` - [List email finder items](https://lempile.com/docs/email-finder/reference/get-items): `GET /email-finder/jobs/{jobId}/items` - [Export email finder CSV](https://lempile.com/docs/email-finder/reference/get-csv): `GET /email-finder/jobs/{jobId}.csv` --- # Email Finder: Create email finder job `POST https://api.lempile.com/email-finder` Creates a queue-first email finder job and returns `202`. Poll the job endpoint until it reaches `Completed` or `Error`. Inputs can be plain LinkedIn URLs/ids or safe identity records with optional name, company, and company-domain hints. Email Finder checks cache, same-workspace LinkedIn profile identity when a Ready account is available, public LinkedIn profile previews, company-site evidence, public emails, verified domain patterns, generated candidates, and internal syntax/domain/MX/SMTP-safe verification. Google Search evidence is optional last-resort evidence only when the Google evidence pool is ready. ## Authentication Send your Email Finder API key in the `X-API-KEY` header. ## Request body `application/json` (required) - `linkedin_url_or_ids` (array of EmailFinderInput): LinkedIn profile URLs, public identifiers, or safe identity records. Plain strings remain supported for compatibility. - Option 1: string - Option 2: EmailFinderInputRecord - `linkedin_url_or_id` (string): LinkedIn profile URL or public identifier. - `linkedin_url` (string): Accepted alias for `linkedin_url_or_id`. - `linkedin_id` (string): Accepted alias for `linkedin_url_or_id`. - `first_name` (string) - `last_name` (string) - `full_name` (string) - `company` (string) - `company_domain` (string): Company website domain, for example `example.com`. - `company_url` (string): Optional company website or company page URL evidence. - `records` (array of EmailFinderInputRecord): Alternative input field for safe identity records. Use this when you already know names, company, or company domain. - `linkedin_url_or_id` (string): LinkedIn profile URL or public identifier. - `linkedin_url` (string): Accepted alias for `linkedin_url_or_id`. - `linkedin_id` (string): Accepted alias for `linkedin_url_or_id`. - `first_name` (string) - `last_name` (string) - `full_name` (string) - `company` (string) - `company_domain` (string): Company website domain, for example `example.com`. - `company_url` (string): Optional company website or company page URL evidence. - `options` (object) - `business_email_only` (boolean): Email Finder defaults to business emails only. Default: `true`. - `profile_resolution` (string): Use controlled same-workspace Ready LinkedIn accounts for visible profile identity resolution, or public/cache/domain evidence only. One of: `workspace_linkedin_accounts`, `public_only`. Default: `"workspace_linkedin_accounts"`. - `verification_mode` (string): `smtp_only` is the default internal-owned mode: syntax/domain/MX/SMTP-safe verification only. `mx_only` disables SMTP recipient checks and uses domain/MX evidence only. One of: `smtp_only`, `mx_only`. Default: `"smtp_only"`. - `provider_max_candidates_per_item` (integer): Reserved for future external verifier fallback. Current public jobs are internal-only and this value is clamped to `0`. Default: `0`. ## Example request ```bash curl -sS -X POST "https://api.lempile.com/email-finder" \ -H "X-API-KEY: $LEMPILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedin_url_or_ids": [ "string" ], "records": [ { "linkedin_url_or_id": "string", "linkedin_url": "string", "linkedin_id": "string", "first_name": "string", "last_name": "string", "full_name": "string", "company": "string", "company_domain": "string", "company_url": "string" } ], "options": { "business_email_only": true, "profile_resolution": "workspace_linkedin_accounts", "verification_mode": "smtp_only", "provider_max_candidates_per_item": 0 } }' ``` ## Responses ### 202 Email finder job accepted. ```json { "object": "email_finder_job", "id": "string", "status": "Queued", "status_detail": "string", "counts": { "input": 0, "completed": 0, "found": 0, "not_found": 0, "uncertain": 0, "error": 0, "retrying": 0 }, "next_retry_at": "string", "endpoints": { "jobEndpoint": "https://example.com", "itemsEndpoint": "https://example.com", "csvEndpoint": "https://example.com" } } ``` ### 400 Invalid input. ```json { "error": "string", "message": "string" } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "message": "string" } ``` ### 402 No active trial or subscription (`subscription_required`), or a trial that used its 1,000 included found emails (`email_finder_trial_limit`). ```json { "error": "string", "message": "string" } ``` ### 403 API key lacks `email_finder.write`. ```json { "error": "string", "message": "string" } ``` ### 429 Workspace email finder queue is full. ```json { "error": "string", "message": "string" } ``` --- # Email Finder: Get email finder usage `GET https://api.lempile.com/email-finder/usage` Found emails this billing period and what they cost. Only emails the company's mail server confirmed (status `Found`) count; `Uncertain` and `Not Found` are free. Each period includes 1,000 found emails, then 10 EUR (or GBP) per 1,000. A trial stops at the 1,000 included: new jobs answer 402 `email_finder_trial_limit` until the subscription starts. ## Authentication Send your Email Finder API key in the `X-API-KEY` header. ## Example request ```bash curl -sS "https://api.lempile.com/email-finder/usage" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 This period's usage. ```json { "object": "email_finder_usage", "period_start": "2026-10-29T09:00:00Z", "period_end": "2026-10-29T09:00:00Z", "found_emails": 0, "included_found_emails": 0, "billable_found_emails": 0, "price_per_1000": { "amount": 0, "currency": "string" }, "estimated_amount": { "amount": 0, "currency": "string" }, "billing": "string", "note": "string" } ``` ### 403 API key lacks `email_finder.read`. ```json { "error": "string", "message": "string" } ``` --- # Email Finder: Get email finder job `GET https://api.lempile.com/email-finder/jobs/{jobId}` ## Authentication Send your Email Finder API key in the `X-API-KEY` header. ## Path parameters - `jobId` (string, required) ## Example request ```bash curl -sS "https://api.lempile.com/email-finder/jobs/{jobId}" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 Completed or failed job. ```json { "object": "email_finder_job", "id": "string", "status": "Queued", "status_detail": "string", "counts": { "input": 0, "completed": 0, "found": 0, "not_found": 0, "uncertain": 0, "error": 0, "retrying": 0 }, "next_retry_at": "string", "endpoints": { "jobEndpoint": "https://example.com", "itemsEndpoint": "https://example.com", "csvEndpoint": "https://example.com" } } ``` ### 202 Job is still queued or running. ```json { "object": "email_finder_job", "id": "string", "status": "Queued", "status_detail": "string", "counts": { "input": 0, "completed": 0, "found": 0, "not_found": 0, "uncertain": 0, "error": 0, "retrying": 0 }, "next_retry_at": "string", "endpoints": { "jobEndpoint": "https://example.com", "itemsEndpoint": "https://example.com", "csvEndpoint": "https://example.com" } } ``` --- # Email Finder: List email finder items `GET https://api.lempile.com/email-finder/jobs/{jobId}/items` ## Authentication Send your Email Finder API key in the `X-API-KEY` header. ## Path parameters - `jobId` (string, required) ## Example request ```bash curl -sS "https://api.lempile.com/email-finder/jobs/{jobId}/items" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 Item results for the job. ```json { "object": "email_finder_item_list", "job": { "object": "email_finder_job", "id": "string", "status": "Queued", "status_detail": "string", "counts": { "input": 0, "completed": 0, "found": 0, "not_found": 0, "uncertain": 0, "error": 0, "retrying": 0 }, "next_retry_at": "string", "endpoints": { "jobEndpoint": "https://example.com", "itemsEndpoint": "https://example.com", "csvEndpoint": "https://example.com" } }, "items": [ { "object": "email_finder_item", "id": "string", "input": "string", "linkedin_id": "string", "linkedin_url": "https://example.com", "status": "Queued", "first_name": "string", "last_name": "string", "full_name": "string", "title": "string", "headline": "string", "location": "string", "country": "string", "company": "string", "company_domain": "string", "company_url": "https://example.com", "company_source": "string", "email": "string", "confidence": 0, "verification_status": "string", "verification_provider": "string", "verification_result": "string", "verification_sub_status": "string", "identity_source": "string", "identity_confidence": 0, "domain_source": "string", "pattern_source": "string", "candidate_pattern": "string", "pattern_confidence": 0, "confidence_reason": "string", "confidence_bucket": "likely", "confidence_score": 0, "uncertain_bucket": "likely", "likely_reason": "string", "retry_after": "string", "retry_strategy": "string", "unknown_reason": "string", "smtp_infra_reason": "string", "smtp_classification": "string", "domain_behavior": "string", "source_provider": "string", "diagnostics": {} } ] } ``` --- # Email Finder: Export email finder CSV `GET https://api.lempile.com/email-finder/jobs/{jobId}.csv` ## Authentication Send your Email Finder API key in the `X-API-KEY` header. ## Path parameters - `jobId` (string, required) ## Example request ```bash curl -sS "https://api.lempile.com/email-finder/jobs/{jobId}.csv" \ -H "X-API-KEY: $LEMPILE_API_KEY" ``` ## Responses ### 200 CSV export. ```text string ``` --- # People Search: Overview > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact People Search provides gated, Apollo-style lead discovery over approved Lempile people snapshots. It returns only safe search metadata plus contact availability. Raw email and phone values are handled by the separate People Reveal API. ## Quick Start ```bash curl -sS -X POST "https://api.lempile.com/people/search" \ -H "X-API-KEY: $LEMPILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "person_countries": ["Canada"], "job_titles": ["sales"], "company_employee_ranges": [{ "min": 101, "max": 200 }], "has_email": true, "facets": ["person_country", "seniority", "company_size"], "page": 1, "per_page": 25 }' ``` The route requires `people.read`. Contact reveal requires `people.reveal` on the separate `/people/reveal` route. ## Response ```json { "object": "people_search_result", "total": 0, "page": 1, "per_page": 25, "has_more": false, "data": [], "facets": { "person_country": {}, "seniority": {}, "company_size": {} } } ``` Each result contains person role/location fields, company metadata, LinkedIn URL when available, and contact availability/status. Search does not return raw emails or raw phone numbers. ## Filters - `person_countries`: exact country names. - `person_locations`: text match over city, state, country, and location label. - `job_titles`: text match over title, normalized title, and function. - `seniorities`: exact seniority labels. - `company_domains`: normalized company domains. - `company_employee_ranges`: objects with `min` and/or `max`. - `company_industries`: text match over company industry. - `email_statuses`: exact email status labels. - `has_email`: boolean. - `page`: `1` to `500`. - `per_page`: `1` to `100`. Supported facets are `person_country`, `seniority`, `company_size`, `company_industry`, `email_status`, and `has_email`. ## Gated Data Policy Default API calls only search active snapshots whose provenance has been approved. The Apollo_BF archive is not exposed by this API. Platform admins can request synthetic QA sample records with `"sample": true`; non-admin keys receive `403` for sample mode. Real imports are controlled through the internal People Import Gate. A platform admin must register source provenance, confirm lawful use, approve the source, run a capped local-file import, and activate the resulting snapshot before it is visible to this API. See people-imports.md. --- # People Search API reference > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact Base URL: `https://api.lempile.com`. Authentication: your API key in the `X-API-KEY` header. OpenAPI document: [people.json](https://lempile.com/docs-assets/openapi/people.json). ## People Search - [Search people](https://lempile.com/docs/people/reference/search): `POST /people/search` ## People Reveal - [Reveal people contact details](https://lempile.com/docs/people/reference/reveal): `POST /people/reveal` --- # People Search: Search people > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `POST https://api.lempile.com/people/search` Searches active, provenance-approved people snapshots. Platform admins may set `sample: true` to search synthetic admin sample records. This route requires `people.read`. ## Authentication Send your People Search API key in the `X-API-KEY` header. ## Request body `application/json` (required) - `person_countries` (array of string) - `person_locations` (array of string) - `job_titles` (array of string) - `seniorities` (array of string) - `company_domains` (array of string) - `company_employee_ranges` (array of EmployeeRange) - `min` (integer) - `max` (integer) - `company_industries` (array of string) - `email_statuses` (array of string) - `has_email` (boolean) - `facets` (array of string) - `page` (integer): Default: `1`. - `per_page` (integer): Default: `25`. - `sample` (boolean): Platform-admin-only synthetic sample mode. ## Example request ```bash curl -sS -X POST "https://api.lempile.com/people/search" \ -H "X-API-KEY: $LEMPILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "person_countries": [ "string" ], "person_locations": [ "string" ], "job_titles": [ "string" ], "seniorities": [ "string" ], "company_domains": [ "string" ], "company_employee_ranges": [ { "min": 0, "max": 0 } ], "company_industries": [ "string" ], "email_statuses": [ "string" ], "has_email": false, "facets": [ "person_country" ], "page": 1, "per_page": 25, "sample": false }' ``` ## Responses ### 200 People search result. ```json { "object": "people_search_result", "total": 0, "page": 0, "per_page": 0, "has_more": false, "data": [ { "object": "person_search_record", "id": "string", "person_name": "string", "first_name": "string", "last_name": "string", "title": "string", "title_normalized": "string", "function_primary": "string", "seniority": "string", "linkedin_url": "string", "location": {}, "company": {}, "contact": { "email_available": false, "email_status": "string", "email_confidence": 0, "phone_available": false, "phone_status": "string" }, "source": {} } ], "facets": {} } ``` ### 400 Invalid request body, filters, facets, or pagination. ```json { "error": "string", "message": "string" } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "message": "string" } ``` ### 403 API key lacks `people.read`, or sample mode was requested by a non-admin key. ```json { "error": "string", "message": "string" } ``` --- # People Search: Reveal people contact details > **Early access.** Tell us what you're building and we'll set you up: https://lempile.com/contact `POST https://api.lempile.com/people/reveal` Reveals raw contact details for an approved people record. This route requires `people.reveal`, reveal-approved source provenance, workspace credits, rate-limit clearance, and writes an audit event. Repeated reveals of the same contact by the same workspace are not charged again. ## Authentication Send your People Search API key in the `X-API-KEY` header. ## Request body `application/json` (required) - Option 1: any - Option 2: any ## Example request ```bash curl -sS -X POST "https://api.lempile.com/people/reveal" \ -H "X-API-KEY: $LEMPILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Responses ### 200 People contact reveal result. ```json { "object": "people_contact", "record_id": "string", "source_record_id": "string", "charged": false, "remaining_credits": 0, "contact": { "email": "string", "email_status": "string", "email_confidence": 0, "phone": "string", "phone_status": "string" } } ``` ### 400 Invalid reveal request. ```json { "error": "string", "message": "string" } ``` ### 401 Missing or invalid API key. ```json { "error": "string", "message": "string" } ``` ### 402 Workspace has no People reveal credits available. ```json { "error": "string", "message": "string" } ``` ### 403 API key lacks `people.reveal`, or the source is not approved for reveal. ```json { "error": "string", "message": "string" } ``` ### 404 Contact record or requested contact field is not available. ```json { "error": "string", "message": "string" } ``` ### 429 People reveal rate limit exceeded. ```json { "error": "string", "message": "string" } ``` --- # Changelog > What changed in Lempile's APIs and in these docs, newest first. ## 2026-10-30 ### Lempile Direct: reposts read as Unipile's, and undone - A plain repost now reads as Unipile's `Post` on every read (a profile's posts, one post, the posts search): the reposted post (its `id`, `social_id`, `author`, text and counts) with `is_repost: true`, `reposted_by`, `repost_id` (the repost's own id), `repost_parsed_datetime` and `repost_content`. A repost with words is the reposter's own post, with `repost_content` the post it shares. - `DELETE /posts/{repost_id}` undoes the account's own plain repost, as LinkedIn's "Delete repost" does; the reposted post stays. ### Lempile Direct: Unipile's fields, side by side - `GET /users/{identifier}` answers what Unipile answers: `follower_count`, `connections_count`, `shared_connections_count`, `is_verified`, `connected_at`, `websites` and `contact_info` with every profile (three LinkedIn calls), pictures at Unipile's sizes, and an invitation's real direction and state (`RECEIVED` / `IGNORED`). - `GET /users/followers`: the account's followers, newest first. `GET /users/following` answers Unipile's own `501`. - Chats: `content_type: "linkedin_offer"` for the ads LinkedIn labels "LinkedIn Offer". - Company search: each company's `logo`, and no `object`, as Unipile's. - Comments: `author_details` and `network_distance` as Unipile's; a reply's `thread_id` is its parent comment. - `POST /chats` with `linkedin[inmail]=true` sends LinkedIn's Premium InMail as the web does. ### Lempile Direct: search, company pages, received invitations and message actions - `POST /linkedin/search` searches people (every Unipile filter: a company's employees with `company`), posts and jobs, besides companies, by Unipile's body, a LinkedIn search address (`url`) or `cursor` alone. `GET /linkedin/search/parameters` finds the ids the filters take. Sales Navigator and Recruiter answer `403 errors/feature_not_subscribed`. - `GET /linkedin/company/{identifier}`: a company page. `GET /users/{identifier}/comments` and `/reactions`: a member's comments and reactions. - `GET /users/invite/received` and `POST /users/invite/received/{invitation_id}` (accept or decline). - Messages: `POST` and `DELETE /messages/{message_id}/reaction`, `PATCH /messages/{message_id}` (an edit, within 60 minutes). Chats: `PATCH /chats/{chat_id}` (`setReadStatus`, `setMuteStatus`) and `DELETE /chats/{chat_id}`. - `POST /posts` takes `repost` (a plain repost, as a page, or with your words). `POST /linkedin` (Unipile's raw route) reads LinkedIn's API with the account's session; writes go through Direct's own routes. ### Lempile Direct: voice notes - `POST /chats/{chat_id}/messages` and `POST /chats` send LinkedIn voice notes (`voice_message`), as Unipile does. Any audio works (m4a, mp3, ogg, webm, wav, aac, flac): Direct turns it into LinkedIn's AAC .m4a. 60 seconds at most (`422 errors/invalid_message`); a file that isn't audio answers `415 errors/unsupported_media_type`. - Read back, a voice note is an attachment of `type: "audio"` with `voice_note: true` and its `duration`. - `DELETE /messages/{message_id}` deletes one of the account's own messages within 60 minutes of sending it, as Unipile does. ### Ask AI - **Ask AI** in the header, or ⌘I: ask a question in your own words and get an answer written from these docs, with links to the pages it comes from. Follow-up questions keep the conversation. - In search, the last row asks Ask AI your search. - Answers are written by AI, so check the linked pages before you build on them. Questions and answers aren't kept. ### The docs for AI tools - **Copy page** on every page: the page as Markdown, ready for an AI chat, or opened straight in ChatGPT or Claude. - Every page is plain Markdown at its own address plus `.md`, for example `https://lempile.com/docs/direct/quick-start.md`. - `https://lempile.com/llms.txt` lists every page; `https://lempile.com/llms-full.txt` has them all in one file. - An MCP server at `https://lempile.com/docs/mcp`: Cursor, VS Code, Claude Code or any MCP client can search these docs and read any page. Connect in one click from any page's menu. ## 2026-10-29 ### Docs on lempile.com/docs Every guide and API reference now lives at `https://lempile.com/docs`, built from the same Markdown and OpenAPI files as before. New: search across guides and endpoints, code samples in cURL, JavaScript and Python, and **Try it** on every endpoint, with your own API key. On Lempile Direct, writes are tried with `?dry_run=true` first. ### Lempile Direct - 13 guides and the full API reference: accounts and hosted auth, webhooks, profiles, relations and invitations, chats and messages, posts, comments and reactions, company search. - Keys belong to a workspace: a Direct key sees only its own workspace's accounts and webhooks. Another workspace's account answers like an unknown one. - A required parameter that's missing or of the wrong type answers Unipile's `400` (`errors/missing_parameters`, `errors/invalid_parameters`). - `GET /users/followers` and `GET /users/following` answer `501 errors/feature_not_implemented`. ## 2026-06-12 ### People Search `POST /people/search` documented: filters by country, location, job title, seniority, company domain, size and industry, with facets and contact availability. ## 2026-06-11 ### Email Finder Candidates are verified by Lempile's own verifier; the external verifier fallback is off by default. ## 2026-06-04 ### Email Finder `POST /email-finder` creates a job from LinkedIn profile URLs or ids; poll it, then read its items or download them as CSV. ## 2026-06-01 ### Google Search API New searches are queue-first: they answer `202 Accepted` with a `json_endpoint` to poll until `status` is `Success` or `Error`. Cached searches may still answer `200` at once.