# What HeyLead keeps, for how long, and who sees it.

> HeyLead security, in plain terms: what is stored in your workspace, what is never stored (your LinkedIn password, a visitor's identity), how a client signs in with OAuth 2.1, the retention windows, export and delete, the services that process data, and where to report a vulnerability.

This is the Markdown twin of https://heylead.dev/security (the page, in text). Index for agents: https://heylead.dev/llms.txt

Last updated 24 September 2026

This page says what the code does today, with the file that does it. Nothing on it is a certification. HeyLead is not affiliated with LinkedIn. It reaches LinkedIn through Unipile, which is also not affiliated.

HeyLead is an AI agent for LinkedIn outreach: it finds the right people, writes to them in the voice of your own LinkedIn posts, follows up, and handles replies. It runs from Claude Code, Cursor, any MCP client or a web dashboard. Everything below applies to the hosted service at heylead.dev. A self-hosted install keeps its data on your machine; see [Self-hosting](https://heylead.dev/docs/self-hosted).

## What is stored

The workspace is the boundary. Every row below belongs to one workspace, in a Postgres database on Google Cloud SQL.

- **Campaigns, the people in them, and the messages.** The campaigns you create, the profiles the search returned, the invitations and messages sent as you, and the replies read back. They stay until you delete the workspace.
- **Your sign-in.** The name, email address and picture Google returns when you sign in. HeyLead has no password of its own.
- **Your LinkedIn seat, as an id.** HeyLead keeps the account id Unipile assigned to your seat ( `organizations.account_id`). It talks to Unipile with one server-side API key that is never sent to a client ( `app/config.py`). The LinkedIn session itself lives at Unipile, not in HeyLead's database.
- **Tokens for the services you connect.** A Slack install is always stored encrypted with Fernet (AES-128-CBC with HMAC-SHA256); without the key, Connect Slack is off ( `app/services/slack_installs.py`). A Google Calendar or X grant is encrypted the same way whenever the sealing key is set, and a backfill re-seals rows written before it ( `app/services/token_crypto.py`, `app/services/token_backfill.py`). The keys are held apart from the database. None of these is requested at sign-in; each is a separate connect in Settings.
- **A count of arrivals at heylead.dev.** The path of the page, the host name of the site that sent you, and the time. That is the whole row ( `site_hits`).

## What is never stored

**Your LinkedIn password.** You connect LinkedIn on a page Unipile hosts. HeyLead mints the link, receives an account id back, and checks that id with Unipile before it binds the seat ( `app/services/hosted_auth_links.py`). No table has a password column.

**A website visitor's identity.** The arrival counter ( `public/arrivals.js`) sends the page path and the referrer's host name. No cookie, no local storage, no IP address, no browser fingerprint, no identifier. Moving between heylead.dev pages sends nothing. The [privacy policy](https://heylead.dev/privacy) describes the same counter.

## How a client signs in

An MCP client connects to `https://heylead.dev/mcp` with OAuth 2.1. It registers itself ( `/oauth/register`, so no pre-shared id), opens the browser, you sign in with Google and choose a workspace on the consent page, and the client keeps a token. PKCE with S256 is required. The full flow is in [Authentication](https://heylead.dev/docs/auth).

| What | Value | Where the code says so |
| --- | --- | --- |
| Scopes | `outreach:read`, `outreach:write` | `app/mcp/oauth_metadata.py` |
| Access token | 1 hour | `app/services/user_tokens.py` |
| Refresh token | 90 days | `app/services/oauth_store.py` |
| Authorization code | 60 seconds | `app/services/oauth_store.py` |
| Revoke | `POST /oauth/revoke` | `app/routers/oauth.py` |

A read-only connection ( `outreach:read`) can show campaigns, contacts, replies and results but never send. `outreach:write` is what lets a client write and send as you, and launch or stop a campaign. An access token is bound to the MCP endpoint: it dies after an hour, is refused the moment it is revoked, and is not accepted by the dashboard's own API. A revoked refresh token stops the client at its next refresh.

## Retention

Your campaigns, contacts and messages stay until you delete the workspace. The operational records around them expire on their own:

| Record | Kept for | Where the code says so |
| --- | --- | --- |
| Finished job rows (the scheduler's queue) | 7 days | `app/services/global_retention.py` |
| Action counters (per-day send tallies) | 2 days | `app/services/global_retention.py` |
| Event log | 30 days | `app/services/global_retention.py` |
| API request log | 30 days | `app/routers/scheduler.py` |
| Aggregated, depersonalised message insights | 90 days | `app/services/insight_store.py` |

The three fleet-wide sweeps run every 6 hours and stamp the database when all three finish. A watchdog on a separate service reads that stamp and raises an alert when it is more than 24 hours old, so a sweep that stops is noticed rather than assumed.

## Export and delete

**Export.** `contacts(action='export')` returns your contacts as a table, CSV or JSON; `analytics(action='export')` does the same for a campaign's results. Both are tools in the [tool reference](https://heylead.dev/docs/tools), so any connected client can run them.

**Delete the workspace.** Settings → Workspace → Danger zone. You type the workspace name back, and the dashboard calls `DELETE /orgs/{org_id}`. Only the owner can do it, and not to their last workspace ( `app/services/org_store.py`).

What that call removes today: the workspace, its members and its open invitations, and it releases the LinkedIn seat from the workspace. It does not disconnect LinkedIn at Unipile; the seat's session stays there until you disconnect it. For the rest of your data and for your account, write to [hello@heylead.dev](mailto:hello@heylead.dev): the [privacy policy](https://heylead.dev/privacy) commits to removing personal data within 30 days of an account-deletion request.

## Who processes data on our behalf

Only services the api code calls. The first four touch every workspace; the rest run only when you connect or turn on that feature.

| Service | Purpose |
| --- | --- |
| Unipile | The LinkedIn connection. Every profile read, invitation, message and reply passes through it ( `app/services/unipile.py`). |
| Google Cloud | Hosting: Cloud Run for the api, Cloud SQL (Postgres) for the database, Secret Manager for the keys. |
| Google Gemini | Writes messages and comments, classifies replies, generates your ICP, embeds your website for retrieval ( `app/services/llm.py`, `app/services/rag/embeddings.py`). |
| OpenAI | Fallback for the same calls when Gemini is rate-limited or down ( `app/config.py`). |
| Google sign-in and Calendar | Sign-in for every account. Calendar only if you connect it, to create the meeting event when a prospect agrees to a time ( `app/services/google_calendar.py`). |
| Stripe | Pro billing, only if you upgrade ( `app/services/billing.py`). |
| Resend | Transactional email to your own address: welcome, account connected, first lead accepted, weekly digest ( `app/services/transactional_email.py`, `app/services/welcome_email.py`). |
| Serper | Web and news search: the web signals for your watchlist, and a news lookup about a company when a client asks for one ( `app/services/hosted_signals.py`, `app/services/llm.py`). |
| Firecrawl | Crawls the website you give for your ICP when a crawl key is set; otherwise HeyLead fetches the pages itself ( `app/services/rag/crawler.py`). |
| Slack | Only if you connect it: the items that need you land in your Slack ( `app/services/slack_installs.py`). |
| Telegram, a webhook | Only if you add one as a notification channel in Settings ( `app/services/notifications.py`). |
| IPinfo | Only if you install the visitor snippet on your own website: turns a visitor's IP into a company name ( `app/services/ip_resolver.py`). |
| X | Only if you connect X, to publish posts there ( `app/services/x_service.py`). |
| Hume AI | Text-to-speech for voice memos. Off by default: a campaign is text only unless you change it ( `app/routers/campaigns.py`). |

The website itself is served by Vercel with Cloudflare in front; like any web server they see your IP address and browser details when a page loads. Plain `http://` requests are redirected to `https://`. The privacy policy names the same providers.

## Reporting a vulnerability

Write to [security@heylead.dev](mailto:security@heylead.dev). The same address is published in [/.well-known/security.txt](https://heylead.dev/.well-known/security.txt). Say what you found and how to reproduce it; you will get a reply from a person.

## What HeyLead does not claim

- No SOC 2, ISO 27001 or GDPR certification. HeyLead has not been audited by a third party.
- No promise about your LinkedIn account. HeyLead sends at a human pace: at most 20 invitations a day and 100 a week on a free LinkedIn account, Monday to Friday 08:00 to 22:00 in your time zone. LinkedIn's own rules are in the [Terms](https://heylead.dev/terms).
- No disclosure to prospects that an AI wrote the message. Since 2 August 2026 the EU AI Act asks for one unless a person reviews each message; by default a person reviews opening messages and follow-ups, and invitations, InMail and automatic replies go out unreviewed.

HeyLead is not affiliated with LinkedIn. It reaches LinkedIn through Unipile, which is also not affiliated.

[Start free, no card](https://heylead.dev/dashboard/login)

Related: [Privacy](https://heylead.dev/privacy) · [Terms](https://heylead.dev/terms) · [Authentication](https://heylead.dev/docs/auth) · [Pricing](https://heylead.dev/pricing) · [Self-hosting](https://heylead.dev/docs/self-hosted)
