# Chrome extension (/docs/extension)



LinkFetch's extension-backed endpoints don't run on a fake LinkedIn
account on our infrastructure. They run **inside the end user's own
signed-in LinkedIn tab** through a Chrome extension called LinkFetch.
The extension captures the data the user has already requested, sends
it to our API, and from then on the row hits the cache like any other.

This is the only way to get LinkedIn data with a defensible compliance
posture in 2026 — see [Compliance](/docs/compliance) for the legal
reasoning.

## When you need the extension [#when-you-need-the-extension]

| Surface           | Needs extension?                                   |
| ----------------- | -------------------------------------------------- |
| `/v1/profiles/*`  | Yes (cache-first, miss → capture)                  |
| `/v1/companies/*` | Yes (cache-first, miss → capture)                  |
| `/v1/posts/*`     | Yes                                                |
| `/v1/groups/*`    | Yes                                                |
| `/v1/search/*`    | Yes                                                |
| `/v1/outbound/*`  | Yes — every action runs from the user's session    |
| `/v1/jobs/*`      | **No** — our scraped dataset, no signed-in session |
| `/v1/locations/*` | No                                                 |

## Install [#install]

[Add LinkFetch to Chrome](https://chromewebstore.google.com/category/extensions)
and pin the icon for visibility. The extension manifest requests:

* `linkedin.com` content-script access (so it can capture and act)
* `storage` (so your API key persists for the message bridge)
* `tabs` (so it can route capture requests to a LinkedIn tab)

It does **not** request `cookies` or `webRequest` host permissions —
your LinkedIn session cookie never leaves the browser.

## How a cache-miss capture works [#how-a-cache-miss-capture-works]

1. Your code calls `GET /v1/profiles?url=…`.
2. LinkFetch returns `422 extension_required` with a `capture_hint`.
3. The page (or your in-app integration) postMessages the hint to the
   extension's content script.
4. The extension fetches the underlying voyager endpoint **inside the
   user's signed-in LinkedIn tab**, normalises it, and POSTs it to
   `/v1/profiles/ingest` with your API key.
5. Your code re-runs the original GET — it now hits the cache and
   returns the typed row with full provenance.

The [/playground](/playground) does this automatically when an endpoint
declares a `capture` flow — Run cascades: GET → 422 → capture → GET
→ 200, all in one click.

## How outbound actions work [#how-outbound-actions-work]

For `POST /v1/outbound/*`, the API itself doesn't issue the action —
it returns `422 extension_required` and the actual call (connection
request, DM, reaction, comment, follow, save) is dispatched by the
extension on the user's session. The API still:

* Validates the target (e.g. blocks DMs to non-1st-degree connections)
* Debits credits when the action succeeds
* Logs the result with provenance for audit

That separation is intentional — LinkFetch never sends actions on
behalf of a user without a live extension round-trip from their own
device.

## Local MCP bridge [#local-mcp-bridge]

If you're driving LinkFetch from a desktop agent (Claude Desktop,
Cursor) and want extension-backed endpoints to work end-to-end, the
extension exposes a local MCP server on `127.0.0.1:7878`. Wire it like:

```json
{
  "mcpServers": {
    "linkfetch-local": {
      "url": "http://127.0.0.1:7878",
      "transport": "http"
    }
  }
}
```

This lets the agent ask the extension to capture from the user's
session without any web UI in the loop. See [MCP](/docs/mcp).

## Diagnostics [#diagnostics]

The extension injects two attributes on `<html>` when it boots:

* `data-linkfetch-extension="1"` — install detection
* `data-linkfetch-build="<iso>-<commit>"` — build identifier

The /playground reads both to render an "Extension connected" pill
with the build SHA. If you're embedding LinkFetch in your own app, the
same pattern works — read the attribute, fall back to an install card.

## Rate limits, the extension's own [#rate-limits-the-extensions-own]

The extension throttles outbound actions locally so a hot loop in your
code can't burn through a user's daily LinkedIn cap. When LinkedIn
serves a security challenge, the extension automatically pauses
outbound for **24 hours** and surfaces a banner — your API call gets
`429 outbound_throttled` with `next_available_at` until the pause
clears.

## Privacy [#privacy]

* The user's LinkedIn session cookie never leaves their browser.
* Captures are normalised to the same typed shape as our scraped data
  before they hit our servers.
* Suppression and DSR removal apply to extension-captured rows the
  same way they apply to scraper rows. See
  [Compliance](/docs/compliance).
