What HeyLead keeps, for how long, and who sees it.
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.
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 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.
| 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, 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 [email protected]: the privacy policy 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 [email protected]. The same address is published in /.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.
- 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.
Related: Privacy · Terms · Authentication · Pricing · Self-hosting