Skip to content
Docs

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:

Shell
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).
  • Writes are still turned on per account by Lempile (see 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.

Shell
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).

Resync#

Shell
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#

Shell
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/<token>"}.

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.

Was this page helpful?