# Errors
Source: https://docs.callprep.app/api-reference/errors
Complete reference of all error codes returned by the CallPrep API.
All errors return a JSON body with an `error` field containing a machine-readable code.
```json theme={null}
{
"error": "credits_exhausted",
"used": 50,
"limit": 50
}
```
## HTTP status codes
| Status | Meaning |
| ------ | ------------------------------------- |
| `202` | Request accepted (POST /research) |
| `200` | Success (GET /research-status) |
| `400` | Bad request — invalid parameters |
| `401` | Unauthorized — missing or invalid key |
| `404` | Not found — research ID doesn't exist |
| `429` | Too many requests — credits exhausted |
| `500` | Internal server error |
## Error codes
### Authentication errors (401)
| Code | Description |
| ----------------- | ----------------------------------------------------- |
| `missing_api_key` | No `Authorization` header provided |
| `invalid_key` | Key not found, revoked, or belongs to another account |
### Request errors (400)
| Code | Description |
| ------------------------ | ---------------------------------------------- |
| `missing_email` | `email` field is required |
| `invalid_email_format` | Email address is not valid |
| `missing_key_id` | API key ID not found in request |
| `api_key_has_no_product` | The key has no product context — regenerate it |
### Credit errors (429)
| Code | Description |
| --------------------- | -------------------------------- |
| `credits_exhausted` | Monthly credits are fully used |
| `trial_limit_reached` | Free plan 3-credit limit reached |
Response body includes:
```json theme={null}
{
"error": "credits_exhausted",
"used": 50,
"limit": 50
}
```
### Not found (404)
| Code | Description |
| ----------- | ------------------------------------------------------------------ |
| `not_found` | The `research_id` does not exist or belongs to a different account |
### Research job errors
These appear in the `error` field when `status` is `failed`:
| Code | Description |
| ---------------- | --------------------------------------------------------- |
| `internal_error` | An unexpected error occurred in the pipeline |
| `timeout` | The job exceeded maximum processing time (auto-retryable) |
## Handling errors
```javascript theme={null}
const res = await fetch(`${BASE_URL}/research`, { ... });
const data = await res.json();
if (!res.ok) {
switch (data.error) {
case 'credits_exhausted':
console.error(`Out of credits: ${data.used}/${data.limit}`);
// Notify user to upgrade
break;
case 'invalid_key':
console.error('Invalid API key — check your credentials');
break;
case 'invalid_email_format':
console.error('Invalid email address provided');
break;
default:
console.error('API error:', data.error);
}
return;
}
```
## Retrying failed jobs
If a research job returns `status: failed`, you can resubmit the same request.
A new credit will be consumed. If failures persist for the same email,
please contact [hello@callprep.app](mailto:hello@callprep.app).
Jobs that fail due to a timeout (infrastructure issue) are eligible for a
credit refund. Contact support with the `research_id` to request a refund.
# GET /research-status/{research_id}
Source: https://docs.callprep.app/api-reference/get-research-status
Poll for research results using the research_id returned by POST /research.
## Overview
Poll this endpoint until `status` is `completed` or `failed`.
Recommended polling interval: every **5 seconds**.
This endpoint does **not** consume credits — poll as many times as needed.
**Base URL:** `https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1`
***
## Request
```http theme={null}
GET /research-status/{research_id}
Authorization: Bearer cp_live_...
```
### Path parameters
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ---------------------------------------------- |
| `research_id` | string | ✅ | The `research_id` returned by `POST /research` |
### Example
```bash theme={null}
curl \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research-status/res_1a7b6e9c35a9b848 \
-H "Authorization: Bearer cp_live_..."
```
***
## Response — processing
**HTTP 200**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "processing",
"estimated_seconds": 15
}
```
Continue polling until `status` changes to `completed` or `failed`.
***
## Response — completed
**HTTP 200**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "completed",
"completed_at": "2026-05-03T08:27:08.004+00:00",
"data": {
"prospect": {
"id": 42,
"name": "Sarah Mitchell",
"title": "VP of Sales",
"company": "Acme Corp",
"email": "sarah.mitchell@acmecorp.com",
"linkedin_url": "https://www.linkedin.com/in/sarah-mitchell/",
"photo": "https://media.licdn.com/dms/image/...",
"last_role_start": "2024-03-01",
"summary": "Sarah Mitchell is VP of Sales at Acme Corp, leading a 40-person enterprise sales team. Her LinkedIn activity shows a focus on outbound pipeline efficiency and CRM adoption, suggesting an interest in tools that reduce manual research overhead for her reps.",
"opening_talk": [
"Your post about reducing sales cycle length through better pre-call prep resonated — that's exactly the problem CallPrep solves for teams like yours.",
"You mentioned your reps spend 45 minutes on average researching before each call — we can cut that to under 60 seconds.",
"Your comment on LinkedIn ROI for outbound caught my attention — CallPrep pulls live LinkedIn activity into every research brief automatically."
],
"posts": [
{
"text": "We cut our average sales cycle by 18% last quarter. The change? Requiring reps to complete a pre-call brief before every discovery call...",
"posted": "2026-04-21 09:15:00",
"post_url": "https://www.linkedin.com/feed/update/urn:li:activity:...",
"reshared": false,
"summary": "Reduced sales cycle 18% by mandating pre-call research briefs before every discovery call."
}
]
},
"company": {
"id": 42,
"name": "Acme Corp",
"description": "Acme Corp is a B2B SaaS platform for enterprise sales teams, offering CRM integrations, pipeline analytics, and AI-assisted outreach across 30+ countries.",
"linkedin_url": "https://www.linkedin.com/company/acmecorp/",
"website": "https://acmecorp.com",
"industry": "software",
"hq": "San Francisco, US",
"revenue": "24M",
"employees": "180",
"technologies": ["Salesforce", "HubSpot", "AWS", "Stripe", "Segment"],
"problems": [
"Enterprise sales teams waste hours on manual prospect research before each call",
"Reps lack competitive intelligence when entering discovery calls with informed buyers",
"Sales managers struggle to ensure consistent pre-call preparation across distributed teams",
"Outbound sequences lack personalization due to limited time for prospect research"
],
"services": [
"AI-powered pre-call research briefs generated from LinkedIn, company news, and CRM data",
"Competitive intelligence alerts when prospects engage with competitor content",
"Sales playbook automation that tailors discovery questions to prospect personas",
"CRM-native workflow that surfaces research directly inside Salesforce and HubSpot"
],
"icp": [
"Enterprise SaaS companies with 50+ person sales teams running high-volume outbound",
"RevOps leaders standardizing pre-call processes across distributed sales orgs",
"Sales managers at B2B companies with ACV over $20k requiring consultative selling",
"SDR teams doing account-based outreach where personalization directly impacts reply rates"
],
"synergy_points": [
"CallPrep directly eliminates Acme Corp's core pain point — reps spending 45+ minutes on pre-call research — by automating enrichment in under 60 seconds.",
"Acme Corp's Salesforce and HubSpot integrations align with CallPrep's CRM-native positioning, enabling seamless adoption without workflow disruption.",
"Acme Corp sells to enterprise sales teams — the exact buyers who understand and budget for sales productivity tools, reducing education overhead in the sales cycle.",
"Acme Corp's focus on outbound personalization makes CallPrep's LinkedIn post analysis and opening talk generation directly relevant to their reps' daily workflow."
],
"discovery_questions": [
"You've invested in Salesforce and HubSpot — how are your reps currently pulling pre-call intelligence from those systems before discovery calls, and where does the process break down?",
"With 180 people and a distributed team, how do you ensure consistent pre-call preparation standards across your SDR and AE org — is that a process or a tooling challenge?",
"Your LinkedIn post mentioned cutting sales cycles by 18% through pre-call briefs — what's the biggest bottleneck preventing you from scaling that behavior across the full team?"
],
"clients": [{ "count": "1,200+" }],
"news": [
{
"summary": "Acme Corp raised a $12M Series A to expand enterprise integrations and grow its European GTM team.",
"url": "https://techcrunch.com/2026/03/acme-corp-series-a",
"date": "2026-03"
}
],
"insights": [
"Competitors like Gong and Chorus position on call recording and post-call analytics — Acme Corp's gap is pre-call intelligence, which CallPrep fills directly.",
"Market gap: most sales intelligence tools provide contact data but not contextual, AI-generated talking points — CallPrep's differentiation is actionable output, not raw data.",
"CallPrep gives Acme Corp reps a measurable edge: personalized opening lines grounded in prospect LinkedIn activity have 3x higher engagement in discovery calls.",
"Key concern: Acme Corp may already evaluate competing tools for pre-call features — reps need to position CallPrep as complementary enrichment, not a competing workflow tool."
]
},
"decision_makers": [
{
"id": 1,
"full_name": "Sarah Mitchell",
"position_title": "VP of Sales",
"linkedin_url": "https://www.linkedin.com/in/sarah-mitchell/",
"picture_url": "https://media.licdn.com/dms/image/...",
"location_text": "San Francisco, CA",
"company_name": "Acme Corp"
},
{
"id": 2,
"full_name": "James Okafor",
"position_title": "Chief Revenue Officer",
"linkedin_url": "https://www.linkedin.com/in/james-okafor/",
"picture_url": "https://media.licdn.com/dms/image/...",
"location_text": "San Francisco, CA",
"company_name": "Acme Corp"
}
]
}
}
```
***
## Response — failed
**HTTP 200**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "failed",
"error": "internal_error"
}
```
You can resubmit the same request — a new credit will be consumed.
If the failure was caused by an infrastructure error, the credit is refunded automatically.
***
## Status values
| Status | Meaning | Action |
| ------------ | ----------------------------- | ---------------- |
| `queued` | Job waiting to be picked up | Continue polling |
| `processing` | Enrichment in progress | Continue polling |
| `completed` | All enrichment steps finished | Use the data |
| `failed` | Unrecoverable error occurred | Resubmit request |
***
## Error responses
**401 — Missing or invalid API key**
```json theme={null}
{ "error": "invalid_key" }
```
**404 — Research ID not found**
```json theme={null}
{ "error": "not_found" }
```
***
For a production-ready polling implementation with timeouts and error handling,
see the [Handling errors](/guides/handling-errors) guide.
For a full description of all fields in the `data` object,
see the [Response fields](/api-reference/response-fields) reference.
# POST /research
Source: https://docs.callprep.app/api-reference/post-research
Submit a prospect email to start an enrichment job.
## Overview
Submit a prospect's email address to start an enrichment job.
Returns immediately with a `research_id` — enrichment runs asynchronously in the background.
Poll [`GET /research-status/{research_id}`](/api-reference/get-research-status) until `status` is `completed`.
**Base URL:** `https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1`
***
## Request
```http theme={null}
POST /research
Authorization: Bearer cp_live_...
Content-Type: application/json
```
### Body parameters
| Parameter | Type | Required | Description |
| ---------------------- | ------ | -------- | ---------------------------------------------- |
| `email` | string | ✅ | Prospect's work email address |
| `prospect_name` | string | ❌ | Full name — improves match accuracy |
| `company_name` | string | ❌ | Company name — improves match accuracy |
| `linkedin_url` | string | ❌ | Prospect's LinkedIn profile URL |
| `company_linkedin_url` | string | ❌ | Company LinkedIn URL — overrides auto-detected |
### Minimal request
```bash theme={null}
curl -X POST \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "sarah.mitchell@acmecorp.com" }'
```
### Full request
```bash theme={null}
curl -X POST \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "sarah.mitchell@acmecorp.com",
"prospect_name": "Sarah Mitchell",
"company_name": "Acme Corp",
"linkedin_url": "https://www.linkedin.com/in/sarah-mitchell/",
"company_linkedin_url": "https://www.linkedin.com/company/acmecorp/"
}'
```
***
## Response
**HTTP 202 — Accepted**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "processing",
"estimated_seconds": 30
}
```
| Field | Description |
| ------------------- | --------------------------------------------------- |
| `research_id` | Unique ID for this job — use it to poll for results |
| `status` | Always `processing` on initial response |
| `estimated_seconds` | Estimated completion time in seconds |
***
## Error responses
**401 — Missing or invalid API key**
```json theme={null}
{ "error": "missing_api_key" }
{ "error": "invalid_key" }
```
**400 — Invalid request**
```json theme={null}
{ "error": "missing_email" }
{ "error": "invalid_email_format" }
```
**429 — Credits exhausted**
```json theme={null}
{
"error": "credits_exhausted",
"used": 50,
"limit": 50
}
```
See the [Errors reference](/api-reference/errors) for the full list of error codes.
***
## Notes
Providing `prospect_name` and `company_name` improves match accuracy
and is recommended whenever you have that information available.
After submitting, poll [`GET /research-status`](/api-reference/get-research-status)
every **5 seconds** until `status` is `completed`. See the
[Handling errors](/guides/handling-errors) guide for a production-ready polling implementation.
# Response fields
Source: https://docs.callprep.app/api-reference/response-fields
Complete reference of all fields returned by GET /research-status.
## Completed response example
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "completed",
"completed_at": "2026-05-03T08:27:08.004+00:00",
"data": {
"prospect": {
"id": 42,
"name": "Sarah Mitchell",
"title": "VP of Sales",
"company": "Acme Corp",
"email": "sarah.mitchell@acmecorp.com",
"linkedin_url": "https://www.linkedin.com/in/sarah-mitchell/",
"photo": "https://media.licdn.com/dms/image/...",
"last_role_start": "2024-03-01",
"summary": "Sarah Mitchell is VP of Sales at Acme Corp, leading a 40-person enterprise sales team. Her LinkedIn activity shows a focus on outbound pipeline efficiency and CRM adoption, suggesting an interest in tools that reduce manual research overhead for her reps.",
"opening_talk": [
"Your post about reducing sales cycle length through better pre-call prep resonated — that's exactly the problem CallPrep solves for teams like yours.",
"You mentioned your reps spend 45 minutes on average researching before each call — we can cut that to under 60 seconds.",
"Your comment on LinkedIn ROI for outbound caught my attention — CallPrep pulls live LinkedIn activity into every research brief automatically."
],
"posts": [
{
"text": "We cut our average sales cycle by 18% last quarter. The change? Requiring reps to complete a pre-call brief before every discovery call...",
"posted": "2026-04-21 09:15:00",
"post_url": "https://www.linkedin.com/feed/update/urn:li:activity:...",
"reshared": false,
"summary": "Reduced sales cycle 18% by mandating pre-call research briefs before every discovery call."
}
]
},
"company": {
"id": 42,
"name": "Acme Corp",
"description": "Acme Corp is a B2B SaaS platform for enterprise sales teams, offering CRM integrations, pipeline analytics, and AI-assisted outreach across 30+ countries.",
"linkedin_url": "https://www.linkedin.com/company/acmecorp/",
"website": "https://acmecorp.com",
"industry": "software",
"hq": "San Francisco, US",
"revenue": "24M",
"employees": "180",
"technologies": ["Salesforce", "HubSpot", "AWS", "Stripe", "Segment"],
"problems": [
"Enterprise sales teams waste hours on manual prospect research before each call",
"Reps lack competitive intelligence when entering discovery calls with informed buyers",
"Sales managers struggle to ensure consistent pre-call preparation across distributed teams",
"Outbound sequences lack personalization due to limited time for prospect research"
],
"services": [
"AI-powered pre-call research briefs generated from LinkedIn, company news, and CRM data",
"Competitive intelligence alerts when prospects engage with competitor content",
"Sales playbook automation that tailors discovery questions to prospect personas",
"CRM-native workflow that surfaces research directly inside Salesforce and HubSpot"
],
"icp": [
"Enterprise SaaS companies with 50+ person sales teams running high-volume outbound",
"RevOps leaders standardizing pre-call processes across distributed sales orgs",
"Sales managers at B2B companies with ACV over $20k requiring consultative selling",
"SDR teams doing account-based outreach where personalization directly impacts reply rates"
],
"synergy_points": [
"CallPrep directly eliminates Acme Corp's core pain point — reps spending 45+ minutes on pre-call research — by automating enrichment in under 60 seconds.",
"Acme Corp's Salesforce and HubSpot integrations align with CallPrep's CRM-native positioning, enabling seamless adoption without workflow disruption.",
"Acme Corp sells to enterprise sales teams — the exact buyers who understand and budget for sales productivity tools, reducing education overhead in the sales cycle.",
"Acme Corp's focus on outbound personalization makes CallPrep's LinkedIn post analysis and opening talk generation directly relevant to their reps' daily workflow."
],
"discovery_questions": [
"You've invested in Salesforce and HubSpot — how are your reps currently pulling pre-call intelligence from those systems before discovery calls, and where does the process break down?",
"With 180 people and a distributed team, how do you ensure consistent pre-call preparation standards across your SDR and AE org — is that a process or a tooling challenge?",
"Your LinkedIn post mentioned cutting sales cycles by 18% through pre-call briefs — what's the biggest bottleneck preventing you from scaling that behavior across the full team?"
],
"clients": [{ "count": "1,200+" }],
"news": [
{
"summary": "Acme Corp raised a $12M Series A to expand enterprise integrations and grow its European GTM team.",
"url": "https://techcrunch.com/2026/03/acme-corp-series-a",
"date": "2026-03"
}
],
"insights": [
"Competitors like Gong and Chorus position on call recording and post-call analytics — Acme Corp's gap is pre-call intelligence, which CallPrep fills directly.",
"Market gap: most sales intelligence tools provide contact data but not contextual, AI-generated talking points — CallPrep's differentiation is actionable output, not raw data.",
"CallPrep gives Acme Corp reps a measurable edge: personalized opening lines grounded in prospect LinkedIn activity have 3x higher engagement in discovery calls.",
"Key concern: Acme Corp may already evaluate competing tools for pre-call features — reps need to position CallPrep as complementary enrichment, not a competing workflow tool."
]
},
"decision_makers": [
{
"id": 1,
"full_name": "Sarah Mitchell",
"position_title": "VP of Sales",
"linkedin_url": "https://www.linkedin.com/in/sarah-mitchell/",
"picture_url": "https://media.licdn.com/dms/image/...",
"location_text": "San Francisco, CA",
"company_name": "Acme Corp"
},
{
"id": 2,
"full_name": "James Okafor",
"position_title": "Chief Revenue Officer",
"linkedin_url": "https://www.linkedin.com/in/james-okafor/",
"picture_url": "https://media.licdn.com/dms/image/...",
"location_text": "San Francisco, CA",
"company_name": "Acme Corp"
}
]
}
}
```
***
## prospect fields
| Field | Description |
| ----------------- | ------------------------------------------------------------------------ |
| `name` | Full name |
| `title` | Job title, translated to English |
| `company` | Current company name |
| `email` | Work email address |
| `linkedin_url` | LinkedIn profile URL |
| `photo` | Profile photo URL (nullable) |
| `last_role_start` | Start date of current role — `YYYY-MM-DD` (nullable) |
| `summary` | AI-generated 2-3 sentence professional summary |
| `opening_talk` | 3 conversation starters personalised to the prospect's LinkedIn activity |
| `posts` | Up to 3 recent LinkedIn posts with AI-generated summaries |
***
## company fields
| Field | Description |
| --------------------- | --------------------------------------------------------- |
| `name` | Company name |
| `description` | Company description, translated to English |
| `linkedin_url` | LinkedIn company page URL |
| `website` | Company website |
| `industry` | Industry (e.g. `software`, `marketing & advertising`) |
| `hq` | Headquarters location |
| `revenue` | Estimated annual revenue (nullable) |
| `employees` | Estimated employee count |
| `technologies` | Tech stack detected for the company |
| `problems` | 4 problems the company solves for its customers |
| `services` | 4 main services or products the company offers |
| `icp` | 4 Ideal Customer Profiles of the company |
| `synergy_points` | 4 synergies between this company and **your product** |
| `discovery_questions` | 3 discovery questions tailored for this specific prospect |
| `clients` | Estimated client count (nullable) |
| `news` | Up to 3 recent company news items (max 12 months old) |
| `insights` | 4 competitive insights about the company |
| `posts` | Up to 3 recent LinkedIn company posts with AI summaries |
***
## decision\_makers fields
Decision makers are only available on the **BDR plan and above**. On other
plans, `decision_makers` will be an empty array.
| Field | Description |
| ---------------- | ---------------------------- |
| `full_name` | Full name |
| `position_title` | Job title |
| `linkedin_url` | LinkedIn profile URL |
| `picture_url` | Profile photo URL (nullable) |
| `location_text` | Location (nullable) |
| `company_name` | Company they work at |
***
## Nullable fields
Fields may be `null` when data is not available for a given prospect or company.
Always handle `null` values in your code:
```javascript theme={null}
const summary = data.prospect?.summary ?? 'No summary available';
const dm = data.decision_makers ?? [];
```
# API Keys
Source: https://docs.callprep.app/authentication/api-keys
How to generate, manage, and secure your CallPrep API keys.
## Generating an API key
1. Go to [API Keys](https://call-prep-api.vercel.app/api-keys) in the dashboard
2. Click **Generate new key**
3. Enter your product URL — we'll automatically extract your product name and description
4. Review the generated product context (name, description, key features, target customers)
5. Click **Create key**
Your key will be shown **once**. Copy it immediately and store it securely.
If you lose your API key, you'll need to generate a new one. There is no way
to retrieve an existing key after it's been shown.
## Product context
Every research is generated against **your product** — its name, description, key
features and target customers — so the insights are about what you actually sell.
Your account has **one product**, shared by every key and by the Chrome extension
and the Autopilot. Edit it under **Settings → Product** in the dashboard.
Older accounts may still have more than one product from before this changed.
Only the current one is used for new research.
## Key format
```
cp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Keys always start with `cp_live_` followed by 42 random characters.
## Using your key
Pass your key as a Bearer token in the `Authorization` header:
```bash theme={null}
curl -H "Authorization: Bearer cp_live_..." \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research-status/res_xxx
```
## Key limits by plan
| Plan | API keys |
| ---------- | -------- |
| Free | 1 |
| Individual | 1 |
| BDR | 3 |
| BDR+ | 10 |
| Enterprise | Custom |
## Security best practices
API keys should only be used server-side. Never include them in browser JavaScript,
mobile apps, or public repositories. Use environment variables instead.
```bash theme={null}
# .env
CALLPREP_API_KEY=cp_live_...
```
Generate a new key periodically and update your integrations.
Delete old keys from the dashboard once they're no longer in use.
Use different keys for development, staging, and production.
This way you can revoke a compromised dev key without affecting production.
Immediately delete the key from the dashboard and generate a new one.
Check your research logs in the dashboard to identify any unauthorised usage.
## Revoking a key
Go to [API Keys](https://call-prep-api.vercel.app/api-keys) in the dashboard,
find the key and click **Revoke**. Revocation is immediate — any requests using
that key will return `401 invalid_key`.
# Rate limits
Source: https://docs.callprep.app/authentication/rate-limits
Understanding credit consumption and request limits.
## Credits
Each call to `POST /research` consumes **1 credit** from your monthly plan.
Credits reset at the start of your billing cycle, and are shared with the Chrome
extension and the Autopilot — one pool for the whole account.
| Plan | Credits/month |
| ---------- | ------------- |
| Free | 10 |
| Individual | 125 |
| BDR | 300 |
| BDR+ | 800 |
| Enterprise | Custom |
`GET /research-status` does **not** consume credits — poll as many times as
needed.
## Cache hits
If a prospect or company was recently enriched (within cache TTL),
the request returns cached data immediately. A credit is still consumed,
but the response is near-instant.
## Credit alerts
You can enable email notifications when you reach 75% and 95% of your monthly credits
in [Settings → Notifications](https://call-prep-api.vercel.app/settings).
## What happens when credits run out
`POST /research` will return `429 credits_exhausted`.
Your existing data and API keys remain active — you just can't start new research jobs
until the next billing cycle or until you upgrade your plan.
```json theme={null}
{
"error": "credits_exhausted",
"used": 300,
"limit": 300
}
```
## Concurrent requests
There is no hard limit on concurrent requests, but the research worker
processes one job per invocation. For high-volume batch enrichment,
consider spacing requests out or contact us about custom plans.
## API key limits
See the [API Keys](/authentication/api-keys) page for key limits per plan.
# Activity and troubleshooting
Source: https://docs.callprep.app/autopilot/activity
Where to look when a message did not go out, where a lead you expected went, and how to end one lead's sequence by hand.
Four places answer almost every question about an Autopilot. In order:
1. **The row** on the Autopilot list — counters and the delivery chip
2. **Activity** — one lead's timeline, with the reason for every skip
3. **The queue** — leads that have not matched anything yet
4. **Upcoming sends** and the capacity bars — what is scheduled and what is behind
***
## The row
Each Autopilot shows **leads · active · responded · closed** and a **responded
rate**.
The **delivery chip** appears only when leads are sitting **uncontacted** — it
stays quiet when everything is moving, so when it does speak it is worth reading.
Expanding it shows how many leads are waiting for a first message, and how many of
this Autopilot's leads are reachable per channel.
Low reach on a channel is a configuration answer, not a fault: prospected lists
rarely have phone numbers or Telegram handles, inbound sign-ups usually do.
***
## Activity — one lead at a time
**Activity** on an Autopilot opens the lead list. Filter by outcome — *active*,
*responded*, *closed*, *failed* — or search by name, email or company.
Expanding a lead shows its timeline: every touch, in order, with the channel, the
step, when it was scheduled, when it was sent, **the message that actually went
out**, and — where something did not happen — the reason.
### Why a message was skipped
Nothing is skipped silently. These are the reasons you will actually see:
| Reason on the timeline | What it means | What to do |
| -------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **No phone number on the contact** | No number in any of the properties you listed | Add the number in HubSpot, or accept that this lead is email/LinkedIn only |
| **No Telegram @username on the contact** | Telegram cannot open a chat from a number | See [Telegram](/integrations/telegram) |
| **No LinkedIn profile on this lead** | Enrichment found no profile, or the URL was unusable | Nothing — the other channels carry on |
| **Already a connection — no invite needed** | You are already connected | Nothing. Message steps behind it still run |
| **LinkedIn didn't return that profile** | The profile could not be reached at send time | Usually transient; check the profile still exists |
| **LinkedIn invite was never accepted** | A message step waited a week past its date | Nothing — the sequence moves on |
| **Lead never warmed up** | A WhatsApp or Telegram step waited three weeks for a reply or an accepted invite | Expected on warm-only channels. Pair them with email or LinkedIn |
| **Couldn't write a credible note / message** | The research produced nothing true to build on for that channel | Nothing to fix per lead. A generic message is the thing we refuse to send |
| **Nothing to follow up with** | A follow-up email with no angle to build on — the first email still went | As above |
| **The address stopped accepting mail** | A hard bounce ended the email steps for this lead | Other channels continue |
### What is *not* a skip
A message held back by the daily limit, the sending window, or a weekend is
**rescheduled**, not skipped — it moves to the next available slot and keeps its
place. If your queue is deep, "not sent yet" is far more common than "not sending".
### Why a sequence ended
The lead row carries the reason: *Lead replied*, *Connected on a call*, *Meeting
booked*, *LinkedIn invite accepted*, *CRM property changed*, *Lead unsubscribed*,
*Closed manually*, *No reachable channel*, *Fell too far behind its schedule*,
*Domain is on the excluded list*, *Its Autopilot was archived*.
The first four are wins — see [What stops a sequence](/autopilot/stop-conditions).
***
## The queue — leads that matched nothing yet
At the bottom of the Autopilot list: **Queue — N waiting**.
* **Waiting** — the lead was created but matched no filter yet, and is re-checked
as its CRM data arrives. Each row shows roughly how long it has left.
* **Abandoned** — the wait time ran out and nothing matched. **No research was
run and no credit was spent.**
Abandoned is the normal end for leads you never intended to contact. If leads you
*did* intend to contact land there, the filter is the thing to look at — compare
it against the property values those contacts actually have.
Depending on your account, abandoned leads can be **reprocessed** (run through the
filters again as they are now) or **sent to a specific Autopilot** by hand. Both
research the lead and spend a credit at that point.
***
## Capacity and upcoming sends
The capacity bars show what today's limits allow across your LinkedIn seats and
mailboxes, and how much is already used. Under them, the queue depth: how much is
waiting behind the cap, and roughly how long it will take to clear at the current
limits.
**Upcoming sends** lists everything scheduled, nearest first, grouped by day and
filterable by channel — in the order the sender will actually work through it:
first messages before follow-ups.
Dates in that list are **intentions, not promises**. Anything that does not fit
a day's limit moves forward. If the queue is weeks deep, the fix is a higher
daily limit, another mailbox or seat, or fewer leads — not waiting.
***
## Finding one lead
The search box above the Autopilot list answers two questions at once: it filters
Autopilots by name, and it finds a **lead** by email, first or last name, or
company — telling you **which Autopilot** they are in, including archived ones.
Clicking a result opens that Autopilot's Activity with the lead's address already
filled in.
***
## Closing a lead by hand
Under a lead's timeline in Activity: **close** ends that lead's sequence on every
channel and cancels everything still queued for them.
* It cannot be undone — the lead comes back the way it arrived, through your CRM.
* It does **not** retract a win: a lead who already replied stays counted as
responded.
* It changes nothing else about the Autopilot; every other lead carries on.
***
## Nothing is sending at all
Work down this list:
1. **Is the Autopilot switched on?** The list shows it plainly.
2. **Is a channel connected**, verified, and not paused? A paused mailbox or seat
sends nothing.
3. **Email only:** is the sender identity complete? Incomplete identity holds
every email step and shows an "Emails are on hold" banner.
4. **Are leads arriving at all?** If the queue is empty and no leads are enrolled,
the problem is upstream — see
[HubSpot troubleshooting](/integrations/hubspot#troubleshooting).
5. **Are leads enrolled but nothing scheduled?** Check the delivery chip on the row
for the reachability of each channel.
6. **Is everything simply queued?** Open Upcoming sends and the capacity bars.
***
## Frequently asked questions
Either its next touch is genuinely days away, or its messages are behind the
daily limit. Expand the lead: a scheduled date in the future is the first case,
a date in the past that keeps moving is the second.
Check the delivery chip on the row: if very few of its leads have a phone number
in your CRM, that channel cannot work for this audience, and the fix is in
HubSpot rather than in the Autopilot.
Yes — expand the lead in Activity. Each sent touch carries the real body, not a
reconstruction. If it was shortened to fit a channel limit, the timeline says so.
The lead matched the Autopilot but has no address on any channel it uses — for
example a LinkedIn-only Autopilot and a lead with no LinkedIn profile. The
sequence is closed immediately rather than left pending forever.
A message drifted further past its planned date than the limit you set under
Autopilot settings, so it was dropped instead of arriving badly out of context.
That limit is off by default — see [Settings](/autopilot/settings).
# Autopilot analytics
Source: https://docs.callprep.app/autopilot/analytics
Outcomes, responded rate and the weekly trend — what each number counts, and the one setting that changes what "responded" means.
**Dashboard → Autopilot → 📈 Analytics**, or read the counters on each Autopilot
row for the same figures per Autopilot.
***
## The four outcomes
Every lead that has ever entered an Autopilot is in exactly one of these:
| Outcome | Meaning |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Active** | The sequence is still running |
| **Responded** | A win — see below |
| **Closed** | It ended without a win: the sequence finished, they unsubscribed, a CRM property changed, you closed it by hand, there was no reachable channel |
| **Failed** | Something went wrong that was not the lead's doing. In practice this is empty |
**Responded rate** = responded ÷ all leads enrolled. It is the number worth
watching over time, and the one that makes two Autopilots comparable.
An outcome is not the same as the operational status. A lead who replied on day
two counts as **responded** immediately, even while the last messages are being
cancelled.
***
## What counts as responded
Four things, broken out separately in the mix:
| Win | Comes from |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Email reply** | They answered your email |
| **Call connected** | A call logged on the HubSpot contact with a *Connected* outcome |
| **Meeting booked** | A meeting logged on the contact after the lead was enrolled |
| **LinkedIn accept** | The connection request was accepted — **only counted on Autopilots where you switched that stop condition on** |
**That last row is the one to understand.** If *LinkedIn invite accepted* is not
one of your stop conditions, an acceptance is recorded as a **connection**, not
as a response — it appears in its own `Connected` count and stays out of your
responded rate.
This is deliberate. Leaving that condition off is a statement that connecting is
not the goal, and a responded rate that counted it anyway would flatter the
Autopilot for something you said did not count.
Autoresponders and out-of-office replies never count.
***
## The weekly trend
Leads enrolled per week as bars, responded rate as a line over them. Read them
together: a good-looking rate on a week with four leads is noise, and a dip on a
week you imported a list is arithmetic, not a regression.
***
## Reading the numbers honestly
* **A responded rate below your expectations, with a healthy first-message count**,
is a message problem — read what actually went out in
[Activity](/autopilot/activity) and fix the angles.
* **A responded rate that cannot move because almost nothing was sent** is a
delivery problem. Check the delivery chip and the capacity bars first.
* **A high closed count with no wins** on a young Autopilot usually means leads
ran out of sequence rather than out of interest — the sequence may be too short,
or too much of it landed on channels those leads are not reachable on.
* **Wins that arrive after the sequence ended** still count. A reply weeks later
is recorded against the lead.
***
## Frequently asked questions
Because accepted LinkedIn invites are being recorded as connections. If
connecting is what you wanted from that sequence, switch on **LinkedIn invite
accepted** in its stop conditions and future acceptances count as wins.
No. Wins are recorded as they are observed, with the rules in force at that
moment. Last quarter's chart is not rewritten by a checkbox today.
Not here. CallPrep records what happened to the outreach — replies, calls,
meetings. What those turn into is your CRM's question, which is why every touch
is written back onto the HubSpot contact.
Yes. Archiving takes an Autopilot out of routing; it does not rewrite what it
did. Its leads, messages and wins stay in the record.
# Build your first Autopilot
Source: https://docs.callprep.app/autopilot/build
Step by step through the builder — who it contacts, where it reaches out, how it sounds, what it sends, and when it stops.
Go to **Dashboard → Autopilot** and either **Start from a template** (see
[Templates](/autopilot/templates)) or **Build from scratch**. This page walks
through the second one, because that is the version where you see every decision.
***
## 1. Name
Name it after **who it targets**, not what it sends — `Agencies — Poland`, not
`Email sequence 2`. In three months this list will have eight rows and you will be
reading it in a hurry.
***
## 2. Who should it contact?
The filter reads your **HubSpot contact properties**: pick a property, an
operator, and a value.
| Operator | Use it for |
| ---------------------------------------- | ----------------------------------------------- |
| **is** / **is not** | Exact match. Several values means *any of them* |
| **contains** / **doesn't contain** | Free-text properties |
| **>**, **≥**, **\<**, **≤**, **between** | Numbers — a score, a headcount, a value |
| **is known** / **is unknown** | Whether the property is filled in at all |
Add up to three rules and choose whether **all** must be true (AND) or **any**
(OR).
**An Autopilot with no rules matches every lead.** The list marks it
*Catch-all* and greys out everything below it — nothing further down will ever
see a lead. Deliberately, that is a useful bottom-of-the-list Autopilot. By
accident, it takes over your whole account.
Properties that get filled in during onboarding are often empty at the moment
the contact is created. That is expected and handled: the lead waits in the
queue and is re-checked as its data arrives. See
[Settings](/autopilot/settings#queue-wait-time).
***
## 3. Where can it reach out?
Toggle the channels this Autopilot may use. Only channels you have actually
connected can send.
Under the channels is a line worth reading before you commit: **how many of this
Autopilot's leads are reachable that way**. A LinkedIn step is useless to a lead
with no LinkedIn profile, and a WhatsApp step is useless without a phone number in
your CRM. This is where you find that out, rather than two weeks later.
WhatsApp and Telegram only message leads who already engaged, unless you switch
that off per channel. A sequence made **only** of those steps will wait
indefinitely for a warm signal it cannot itself produce — pair them with email
or LinkedIn.
***
## 4. How should it sound, and what is it for?
**Tone** changes the voice and the greeting. It never changes what a message asks
for.
| Tone | Reads like |
| ------------ | ---------------------------------------------------------------- |
| **Friendly** | Warm and casual, like writing to someone you know a little |
| **Direct** | The shortest version, no softeners |
| **Formal** | Polite and professional, no contractions — for enterprise buyers |
| **Founder** | Personal and honest, typed quickly by a busy founder |
**Goal** is what every message works towards, and some goals need something from
you before the Autopilot can be switched on:
| Goal | Needs |
| --------------------- | ------------------------------------------------------------------------------------- |
| **Engage with leads** | Nothing — messages simply invite a reply |
| **Meetings** | Your calendar link (Calendly, Cal.com, anything) |
| **Self serve** | Your pricing page |
| **Qualify** | One to three qualifying questions, in the order you want them asked — one per message |
A goal without its input cannot be enabled. This is deliberate: a "book a
meeting" sequence with no link is a series of messages asking for something the
reader cannot do.
**How did these leads come to you?** — one sentence, in your words
(*"they registered for a trial"*, *"they downloaded the pricing guide"*). It
becomes the **opening line of the first message**, which is most of the difference
between a cold email and an obvious one.
***
## 5. The sequence
Each touch is a **day**, a **channel**, and an **angle**.
* **Day** — 1 is the day the lead is enrolled. Later touches are business days
from there.
* **Channel and kind** — email; LinkedIn *connection request* or *message*;
WhatsApp; Telegram.
* **Angle** — your instruction for what this message should be about. Not the
final text: the message is written per lead from the angle plus that lead's
research.
* **Subject angle** (email) — only on the first email when threading is on, since
the rest reply inside the same thread.
* **Uses research** — which signals to build on: their post, their title, the
company, the fit with your product. Leave it alone for a sensible default.
### Show example
**Show example** renders a real message for a real lead of yours, through the same
code path that sends, with your signature underneath. If you would not send that
one, change the angle and look again.
### What the angle can and cannot do
Your angle **outranks** the house writing rules — it is your campaign. Four things
it cannot override, because they are not style preferences:
* the length a channel physically allows
* **inventing facts** — including a performance number that is not in your product
description or in the research
* the rule that a reshared post is not the lead's own words
* the sender-identity block on commercial email
Quote a sentence in the angle and it will be reproduced nearly word for word in
**every** message from that step. That is a good way to pin an opening you like
— and a good way to send four hundred people the same sentence. The builder
warns you when a step does this.
### A note on LinkedIn steps
LinkedIn works in two moves: a **connection request**, then a **message** once it
is accepted. A message step waits for the acceptance on its own and gives up after
a week rather than hanging forever. A second connection request in the same
sequence can never do anything — LinkedIn allows one — and the builder says so.
### ✨ Build with AI
**Build with AI** drafts the whole sequence from your product, channels and
filter. Treat it as a first draft: read every angle, run **Show example** on at
least the first two touches, and cut anything you would not send yourself.
***
## 6. When should it stop?
| Condition | What it means |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **They reply — on any channel** | Email, LinkedIn, WhatsApp or Telegram. Leave this on. |
| **You log a connected call, or they book a meeting (in HubSpot)** | Someone on your side already picked this lead up. |
| **LinkedIn invite accepted (you're connected)** | A choice, not a default — see below. |
| **A CRM property changes** | Nominate a property and a value. Leave the value empty to stop on any change. |
**An accepted connection request is a connection, not an answer.** Turn this on
if connecting *is* the goal of the sequence — the acceptance is then counted as
a win. Leave it off if you plan to say something afterwards, or the sequence will
end exactly when it becomes useful.
Full detail, including what deliberately does **not** stop a sequence, is on
[What stops a sequence](/autopilot/stop-conditions).
***
## 7. Save and switch on
Save from the bar at the bottom, then switch the Autopilot on in the list.
From that moment, a **new** contact matching the filter is researched and enrolled
within about a minute. Existing contacts are not swept up — the Autopilot acts on
contacts created from now on.
After a day or two, check:
* the **counters on the row** — leads, active, responded
* the **delivery chip**, which appears only when leads are sitting uncontacted
* **[Activity](/autopilot/activity)** for individual leads and skip reasons
***
## Frequently asked questions
Three or four over two weeks is a sequence. Twelve is a campaign nobody finished
reading. Note that the last email is written to ask for nothing — it exists to
close politely, so cutting it makes the sequence end mid-pitch.
Yes, and the changes apply to messages that have not been sent yet. Leads
already enrolled keep their place in the sequence. Deleting a step from the
middle can leave already-scheduled messages without a matching angle — they fall
back to a generic one rather than sending something written for a different step.
It stops routing new leads and leads in flight are stopped, but everything it
did stays: the leads, the messages, the analytics. Nothing is deleted, and a lead
that already replied keeps its win.
LinkedIn accepts one connection request per person. A second one in the same
sequence can only ever be skipped, so the builder defaults a new LinkedIn touch to
*message* once a request already exists.
Yes. Channels belong to your account, not to one Autopilot, and the daily limits
are per mailbox or per seat — shared across everything you run.
# Autopilot overview
Source: https://docs.callprep.app/autopilot/overview
How the Autopilot turns a new CRM contact into researched, personalised outreach across LinkedIn, email, WhatsApp and Telegram — and stops the moment someone answers.
## What an Autopilot is
An Autopilot is **three decisions**:
1. **Who** gets contacted — a filter over your HubSpot contact properties
2. **What** is said to them — a sequence of touches, each written per lead from that lead's research
3. **When it stops** — a reply, a booked meeting, a CRM change
Everything between those decisions happens on its own: the research, the writing,
the sending, the pacing, the daily limits and the record in your CRM.
```
New contact in HubSpot
↓
Does it match one of your Autopilots? ── no ──→ waits in the queue, then is
↓ yes abandoned (no research, no credit)
Research the person and their company (1 credit)
↓
Enrol into that Autopilot's sequence
↓
Touch by touch, from your own LinkedIn account / mailbox / number
↓
They answer → the sequence stops, on every channel
```
You can run several Autopilots side by side — one for inbound signups, one for
a prospected list, one as a catch-all. A lead only ever belongs to **one** of
them at a time.
***
## What you need before it can run
| Requirement | Where | Why |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| A product | Settings → Product | Every message is written from your product description. Without it there is nothing to say. |
| HubSpot connected | [Integrations → HubSpot](/integrations/hubspot) | This is what tells CallPrep a lead exists. |
| At least one channel | [LinkedIn](/integrations/linkedin), [Email](/integrations/email), [WhatsApp](/integrations/whatsapp), [Telegram](/integrations/telegram) | A channel is where messages send from. An Autopilot with no connected channel has nowhere to go. |
| Sender identity (email only) | Settings → Outreach → Email signature | Commercial email must identify the sender. Email steps are held until it is complete. |
***
## Which Autopilot gets a lead
Autopilots are checked **top to bottom**, and the **first matching filter wins**.
Nothing further down ever sees that lead. Use the arrows on the list to reorder —
specific Autopilots above, broad ones below.
* An Autopilot with **no filter** matches every lead. That is occasionally what
you want, as a catch-all at the bottom of the list, and never what you want by
accident. The list marks it **Catch-all** and greys out everything beneath it as
*Never reached*.
* A **fallback Autopilot** is checked differently: it is skipped during the normal
pass and gets the **last look** at a lead just before it would be abandoned. It
keeps its own filter — a fallback is not the same thing as a catch-all.
* A lead already in an active sequence is not enrolled a second time.
***
## What it costs
**One credit per researched lead** — the same pool as the API and the Chrome
extension. Sending, retrying, skipping and rescheduling are all free.
A lead costs **nothing** when it is:
* abandoned after matching no filter
* skipped by *Addresses to skip* or *Excluded domains*
* already researched for this product (with **Research each contact only once** on,
the default)
See [Credits](/plans/credits).
***
## Where everything lives
Name, filter, channels, voice, the sequence, stop conditions.
What ends it, what deliberately does not, and the one CRM gap to know about.
Why a message was not sent, where a missing lead went, closing a lead by hand.
Start from a ready-made Autopilot instead of a blank one.
Outcomes, responded rate, and what counts as a win.
Queue wait time, addresses to skip, dropping messages that fall behind.
***
## Frequently asked questions
Nothing is wrong yet — an Autopilot only acts on contacts **created after** it
was switched on. Existing contacts are not swept up. If a new contact was
created and still nothing happened, work through
[Activity and troubleshooting](/autopilot/activity).
Only if your filter lets it. Two guards help: **Excluded domains**
(Settings → Outreach) skips whole companies for good, and a stop condition on a
CRM property ends a sequence the moment someone's lifecycle stage moves. Both
are worth setting before the first Autopilot goes live.
No. The first matching filter wins, and a lead in an active sequence is not
enrolled again. It can enter a different Autopilot later, once the first
sequence has ended and the contact comes back through your CRM.
No. You write the **angle** for each touch — the instruction — and the message
itself is written per lead from that lead's research. **Show example** in the
builder renders a real one before you commit.
The first email still goes out, because a first message can honestly ask what
someone is trying to achieve. Later messages and the other channels need
something real to say, and skip rather than send filler — with the reason
recorded on the lead's timeline.
Switch the Autopilots off, or pause the channel: a paused LinkedIn seat or
mailbox stops sending while keeping every lead enrolled. Leads resume where they
left off.
# Autopilot settings
Source: https://docs.callprep.app/autopilot/settings
The three account-wide settings behind every Autopilot — how long a lead waits for a match, when a late message is dropped, and which addresses are skipped outright.
**Dashboard → Autopilot → the gear icon.** These apply to every Autopilot on the
account, not to one of them.
***
## Queue wait time
**2, 6, 12, 24 or 48 hours. Default: 6.**
Contacts are often created *before* the data that qualifies them — someone signs
up, the contact appears, and the onboarding answers your filter needs arrive
minutes or hours later.
So a contact that matches nothing yet waits in the queue and is re-checked as its
CRM data arrives: every minute for the first quarter of an hour, then less often,
until this window runs out. Then it is **abandoned** — never researched, no credit
spent.
| Set it shorter if | Set it longer if |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Your filter uses properties that are set at creation time, and waiting only delays the obvious | Your filter uses onboarding answers, enrichment fields, or anything a human fills in later |
If you have a **fallback Autopilot**, it gets the last look at the lead at the
end of this window, and takes it if its own filter matches.
***
## Drop messages that fall too far behind
**Off, 7, 14, 30 or 60 days. Default: off.**
When there is more to send than the daily limits allow, messages move forward
rather than being dropped. Usually that is right. But a follow-up written for
"three days after the intro" landing five weeks later is worse than not landing:
it reads as though nobody was paying attention.
This is **lateness, not age** — how far past its planned date a message is. Fourteen
days behind plan means the same thing in a two-week sequence and a two-month one.
Leads dropped this way end with *Fell too far behind its schedule* in
[Activity](/autopilot/activity).
Turning this on is a symptom fix. If messages routinely fall weeks behind, the
real answer is a higher daily limit, another mailbox or seat, or fewer leads
entering at once — check the capacity bars.
***
## Addresses to skip
**Both off by default.** Each one narrows who your Autopilots contact, and
narrowing is a choice — so neither is on unless you say so.
| Setting | Skips |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Skip role-based addresses** | Mailboxes named after a function, not a person: `info@`, `contact@`, `kontakt@`, `office@`, `sales@`, `support@` and similar |
| **Skip free mailbox providers** | Personal mailbox domains: Gmail, Outlook, Yahoo, iCloud, Proton, wp.pl and similar |
Both are checked **before anything else**, so a skipped lead costs no research and
no credit, and appears with its reason in the HubSpot intake log.
Free-provider skipping is blunter than it looks: a founder on Gmail is still a
founder. Only switch it on if your product genuinely does not sell to people at
a personal address.
Turning either setting on does not touch leads already enrolled, and turning one
off does not go back for leads it skipped earlier — those come back the way any
lead does, through your CRM.
***
## Related settings elsewhere
| Setting | Where | What it does |
| ---------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Excluded domains** | Settings → Outreach | Companies never researched or contacted at all — your own domain, partners, customers |
| **Research each contact only once** | Settings → Outreach | Stops a re-created or re-fired contact from spending a second credit. On by default |
| **Content language** | Settings → Language | The language every generated message is written in |
| **Email signature and sender identity** | Settings → Outreach | Required before any email step can send |
| **Daily limits, sending hours, warm-up** | Each channel's integration page | Per seat and per mailbox — see [LinkedIn](/integrations/linkedin) and [Email](/integrations/email) |
***
## Frequently asked questions
No. A lead in the queue has not been researched — that is the whole point of the
wait. Credits are spent at the moment a filter matches.
Only leads that match nothing. A lead matching a filter is researched and
enrolled within about a minute, whatever the wait time is set to.
No — all three are account-wide. Per-Autopilot decisions (filter, channels,
voice, stop conditions) live in the builder.
# What stops a sequence
Source: https://docs.callprep.app/autopilot/stop-conditions
Every signal that ends an Autopilot sequence, the ones that deliberately do not, and the two gaps worth knowing about before they surprise you.
A sequence should end the moment a human takes over. This page is the complete
list of what ends it — and, just as importantly, what does not.
Every condition below except unsubscribe, bounce and manual close is a
**toggle** on the Autopilot, under **When should it stop?**. What is checked is
what you switched on.
***
## What stops it
| Signal | Detail |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **They reply, on any channel** | An answer on email, LinkedIn, WhatsApp or Telegram stops **every** channel, not just the one they answered on. Counted as a win. |
| **You log a connected call** | A call on the HubSpot contact with a *Connected* outcome, logged after the lead entered the sequence. |
| **They book a meeting** | A meeting logged on the HubSpot contact after the lead entered the sequence. |
| **A CRM property changes** | The property and value you nominated — for example *Lifecycle stage is opportunity*. Empty value means any change. |
| **A LinkedIn invite is accepted** | Only if you switched this on. It then counts as a win. |
| **They unsubscribe** | Always, on every channel, permanently. Not a toggle. |
| **You close the lead by hand** | From the lead's timeline in [Activity](/autopilot/activity#closing-a-lead-by-hand). |
| **A hard bounce** | Stops the **email** steps for that lead only — the address failed, not the person. Other channels continue. |
### When the check happens
Twice, on purpose:
* **Right before every message goes out** — so an answer that arrived overnight is
seen before the next send, not after it
* **Between messages**, on a regular sweep — so the panel reflects reality even
while nothing is being sent
***
## What does not stop it
This is the part people do not expect.
### Out-of-office and automatic replies
An auto-reply is not an answer, and treating it as one would silently end
sequences every time someone goes on holiday. Out-of-office messages, delivery
receipts and system acknowledgements are recognised and ignored — the sequence
carries on.
### A call or meeting from before the lead entered the sequence
Only activity logged **after** enrolment counts. A lead who spoke to you six months
ago and has now come back through your CRM gets a fresh sequence — that is the
intent, not an oversight.
### Activity logged only on a deal
If you log a call or a meeting on a **deal** and it is not associated with the
contact — HubSpot shows *0 contacts* on the activity — **CallPrep cannot see it
and the sequence keeps running.**
**What to do:** log activity on the contact, or associate the contact with the
activity on the deal. It takes one click in HubSpot and is worth making a team
habit.
This is a known limitation, not a bug being fixed in the background. Without this
paragraph the symptom is "the Autopilot wrote to someone I had already spoken to"
with no visible cause.
### A reply to a message someone sent outside the sequence
If a colleague emails a lead from their own mailbox and the lead replies to
**that** message, the answer may be invisible to CallPrep — it never saw the
original thread.
**What to do:** reply inside the sequence's thread where you can, or close the
lead by hand in Activity. If the email is logged on the HubSpot contact, that also
stops it.
### An email reply to a custom-domain sender
A mailbox connected as a **custom domain** is send-only: replies land in your
inbox and CallPrep has no access to it, so *stop on reply* cannot fire from
email for those leads.
Gmail and Outlook mailboxes do not have this problem — see
[Email](/integrations/email#replies-and-bounces). For a custom domain, log the
reply on the HubSpot contact or close the lead by hand.
***
## What counts as a win
Four outcomes are wins, and they are what the responded rate in
[Analytics](/autopilot/analytics) is built from:
* **Lead replied** — on any channel
* **Connected on a call**
* **Meeting booked**
* **LinkedIn invite accepted** — *only* on Autopilots where you switched that
condition on
Everything else that ends a sequence — completed, unsubscribed, CRM property,
closed by hand, no reachable channel — is closed, not won.
A lead that replied **keeps** its win even if the sequence is later closed by
hand or the Autopilot is archived. A win, once recorded, is never taken back.
***
## Frequently asked questions
Almost always one of three things: the reply came to a mailbox CallPrep cannot
read (custom domain), the reply was to a message sent outside the sequence, or
the activity was logged only on a deal. All three are above, with what to do
about each.
No. It neither stops the sequence nor counts as a response, so your responded
rate is not inflated by holiday responders.
Switch on **LinkedIn invite accepted (you're connected)**. Bear in mind that any
LinkedIn messages you scheduled after the request will then never send — which is
correct if connecting was the point, and a mistake if you meant to follow up.
Not from the panel. A lead comes back the way it arrived: through your CRM. That
is deliberate — an "undo" would re-send a schedule you already decided against.
Yes. An unsubscribe stops every active sequence for that person, on every
channel, and is never cleared — not by a re-import, not by a new Autopilot.
# Autopilot templates
Source: https://docs.callprep.app/autopilot/templates
Start from a ready-made Autopilot instead of a blank one — what a copy brings with it, what it deliberately leaves behind, and why it always arrives switched off.
## The library
**Dashboard → Autopilot → 📚 Templates.**
Each template expands to show everything it would give you: who it targets, the
whole sequence day by day with each touch's angle and the research it uses, what
it asks for, and what stops it. A timeline across the top shows the shape of the
sequence from the lead's side — how many messages, how close together, on which
channels.
**Use this template** copies it into your account.
***
## What a copy is
A **snapshot**, not a link. Once copied, the Autopilot is yours: editing it changes
nothing anywhere else, and if the template is later changed or removed, your copy
carries on exactly as it is.
The copy arrives:
* **Switched off**, always — see below
* **At the bottom of your priority list**, so a new, often broad Autopilot cannot
quietly take leads from the specific ones you already run
* **Renamed** if you already have one with that name
***
## Read the warnings before switching it on
A template was written against someone else's CRM and someone else's connected
channels, so a copy can arrive with gaps. They are listed right after copying:
| Warning | What it means | What to do |
| ----------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Filter properties this portal does not have** | The template filters on a property that does not exist in your HubSpot | Rebuild those rules with your own properties. The rules are kept, not silently deleted, so you can see what was intended |
| **Channels you have not connected** | It has steps on a channel you do not have | Connect it, or delete those steps |
| **Matches every lead** | Its filter carries no usable rule in your portal | Add a filter before enabling, unless you deliberately want a catch-all |
| **The goal needs your link** | A meetings or self-serve template cannot carry someone else's calendar or pricing URL | Paste yours in the builder |
**This is why a copy is never enabled automatically.** An Autopilot whose filter
did not survive the copy matches *everyone* — and an enabled one would start
contacting your whole CRM before you had read a single line of it.
***
## What a template never carries
Two things are deliberately left behind, because they belong to one account and
would be wrong — not merely empty — in another:
* **Your calendar or pricing link.** Someone else's booking link is worse than no
link: the sequence looks configured and sends leads to the wrong place.
* **How the leads came to you.** That sentence becomes the opening line of the
first message. A template claiming *"I saw you just registered"* to a prospected
list is a lie in the first sentence.
Tone, goal and qualifying questions **do** come across.
***
## Before you enable a copy
1. Fix anything the warnings named
2. Fill in **How did these leads come to you?** in your own words
3. Run **Show example** on the first two touches — this is the moment to find out
whether the template's voice fits yours
4. Check the stop conditions, especially *LinkedIn invite accepted*
5. Check where it sits in the priority list
***
## Frequently asked questions
No. A copy is a snapshot taken at the moment you clicked. Nothing about it
changes unless you change it.
Not yourself — the library is curated. If you have built one that works well and
would be useful to others, tell us at
[hello@callprep.app](mailto:hello@callprep.app).
They answer different questions. A template is a proven shape you adapt; **Build
with AI** drafts a sequence from your own product and audience. Both need the
same review before switching on.
Only if it is more specific than what sits above it. Order decides who takes a
lead first, and the more specific Autopilot should always be the one that wins.
# Calendar and battlecards
Source: https://docs.callprep.app/extension/calendar
Connect Google Calendar so new meetings are researched before you get to them, and a briefing email lands in your inbox with everything worth knowing.
## What this does
With Google Calendar connected, the extension lists your upcoming meetings and can
research the person you are about to meet **before the meeting exists in your
head** — then email you a battlecard about them.
```
A new meeting appears in your calendar
↓
It has an external attendee who has not been researched yet
↓
Research runs on its own (1 credit)
↓
A battlecard email arrives · the meeting card in the panel fills in
```
Calendar access is **read-only**. CallPrep never creates, edits, moves or
cancels anything in your calendar.
***
## Connecting
Google Calendar is connected as part of signing in to the extension with Google —
approve the calendar permission when Chrome asks for it. Your meetings then appear
in the side panel.
If the connection later expires, the panel says so and offers **Reconnect with
Google**. Nothing is lost while it is disconnected, except the automatic research
that would have run.
***
## Automatic research
**Settings → auto research** in the extension.
Off, meetings are listed and you research whoever you like by hand. On, a new
meeting is researched on its own.
### Exactly what gets researched
* **One person per new meeting** — the first external attendee who has not been
researched yet. Not every attendee, and not the whole room
* **You are never researched**, and neither is anyone on an
**[excluded domain](/autopilot/settings)**
* A meeting whose attendees you have already researched is skipped
* **One credit per automatic research**, from the same monthly pool as everything
else
A busy calendar spends credits without being asked. If you sit in a lot of
internal or recurring meetings with outside guests, add those domains to
**Excluded domains** first, or leave automatic research off and research the two
meetings a week that matter.
When your credits run out, automatic research stops until they reset. Nothing
breaks; the meetings are simply listed unresearched.
***
## The battlecard email
When a meeting-driven research finishes, CallPrep emails you a briefing. It is
worth having in your inbox: it arrives while you are somewhere else, and it is
readable on a phone two minutes before the call.
It comes in three shapes, depending on what was actually found:
| Shape | When | What you get |
| -------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Full battlecard** | The person and their company were both found | Who they are, what the company does, synergy points with your product, opening lines from their recent activity, discovery questions |
| **Short battlecard** | The person was found but there is nothing solid about the company | The person, the opening lines and the questions. The company sections are left out rather than printed empty |
| **A short note** | Nothing solid was found | One paragraph saying so, and why — either there was no public trace of this person, or a data source did not answer |
That third one is deliberate. Silence would leave you wondering whether the
briefing was still coming; a note tells you there is nothing to wait for, while
there is still time to look them up yourself.
To stop these emails: **Settings → Notifications → Meeting battlecard** in the
dashboard.
***
## What is stored, and for how long
| Data | Kept |
| --------------------------------------------------------- | -------------------------------------------------------------- |
| Meetings read from your calendar (title, time, attendees) | **30 days**, then deleted automatically |
| The research itself (prospect, company, insights) | Under the normal [data retention](/plans/data-retention) rules |
Disconnecting the calendar (**Settings → auto research → Disconnect Google
Calendar**) deletes the meetings CallPrep stored and stops any further reading of
your calendar. **Research you already ran stays** — it is yours, and it is not
calendar data.
***
## Frequently asked questions
The primary calendar of the Google account you signed in with, read-only.
Meetings without an external attendee are ignored.
No — one person per meeting, the first external attendee who has not been
researched yet. Research the others by hand from the meeting card if you need
them.
It runs when the meeting **appears in your calendar**, not on a timer before it.
A meeting booked a week out is researched a week out; one booked an hour before
is researched then. That is also when the battlecard email is sent.
No. What reaches the AI is the prospect's public professional data — name,
title, company, LinkedIn activity — plus your product description. Meeting
titles, descriptions and attendee lists are not sent to it.
Not today — the calendar connection lives in the extension. Any research started
from a meeting produces the email; researching by email address in the dashboard
or the API does not.
Yes, the stored meetings are deleted on disconnect, and they would have been
deleted after 30 days anyway. Your research history is untouched.
# Chrome extension
Source: https://docs.callprep.app/extension/overview
Research a prospect from a side panel in your browser, see your upcoming meetings with the attendees already researched, and connect LinkedIn in one click.
## What it is
**AI Call Prep App** is a Chrome side panel: your upcoming meetings, the people in
them already researched, and a box to research anyone else by email. Same account,
same credits and same research as the dashboard and the API — a different way in.
Add it to Chrome, then pin it to your toolbar.
Automatic research for new meetings, and the briefing email that follows.
***
## Setup
### 1. Install and open
Install from the Chrome Web Store, pin the icon to your toolbar, and click it to
open the side panel. It opens beside whatever page you are on and stays there
while you work.
### 2. Sign in with Google
**Continue with Google.** The extension signs in with Google only — if your
CallPrep account uses the same Google address, it is the same account, with the
same plan and the same credits.
### 3. Tell it about your product
On first run it asks for three things, and they are worth two minutes:
| Question | Why it matters |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **Your product** — paste your website and it drafts the description, features and target customers | Every insight is written against this. A vague product description produces vague synergy points |
| **Sales methodology** — Consultative, Challenger, Solution, Sandler, Value or Inbound Selling | Shapes the discovery questions you get |
| **Excluded domains** | Companies never researched or contacted — your own domain, partners, customers |
All three stay editable later, in **Settings** inside the extension and in the
dashboard under **Settings → Product** and **Settings → Outreach**. They are the
same settings, not a second copy.
***
## What you can do in the panel
### Research someone by email
Type an email address and the research starts. It takes roughly 20 to 60 seconds,
or a couple of seconds if that person was researched recently. **One credit per
research.**
You can have a couple of researches running at once; the panel says so when you
reach the limit.
### See your meetings
With Google Calendar connected, the panel lists your upcoming meetings, and each
external attendee can be researched from the meeting card — or researched
automatically before you get there. See
[Calendar and battlecards](/extension/calendar).
### Read the research
Each result opens in tabs:
* **Insights** — a summary of the person, opening lines drawn from their recent
LinkedIn activity, synergy points with your product, competitive notes
* **Company** — description, industry, HQ, size, tech stack, what they sell, who
they sell to, discovery questions to ask
* **Decision makers** — other senior people at that company, with titles and
LinkedIn profiles *(BDR plan and above)*
* **Social** — recent posts from the prospect and the company, and company news
### Find an earlier research
The research list holds everything your account has researched, searchable and
sortable, with an **Extension only** filter when you want just the ones from here.
### Connect LinkedIn in one click
**Settings → LinkedIn integration** connects the LinkedIn account your Autopilot
outreach sends from, using the session already open in this browser — no password,
and it works if you sign in to LinkedIn with Google. This is the same seat you
would otherwise connect in the dashboard; see
[LinkedIn](/integrations/linkedin).
***
## Credits and plans
The extension draws on the **same monthly credits** as everything else: one credit
per research, whether it was started by you or automatically from a meeting.
Cached results still cost a credit — see [Credits](/plans/credits).
Decision makers require the **BDR plan or above**; on other plans that tab stays
empty.
***
## Frequently asked questions
No. Sign in with the Google address on your CallPrep account and it is the same
account — same plan, same credits, same research history, same product.
No. It reads nothing from the pages you browse. It talks to CallPrep, to Google
for sign-in and calendar, and — only when you connect LinkedIn from Settings —
to your LinkedIn session in this browser, once, to establish that connection.
That means the data sources had little to say about that address. It happens with
personal mailboxes and very small companies. The company block is still built
from the email domain where possible, and you keep the discovery questions.
Yes, and they share everything: one product, one credit pool, one research
history. A prospect researched in the extension returns instantly through the
API, and the other way round.
Chrome, and Chromium browsers that install from the Chrome Web Store and support
side panels (Edge, Brave, Arc). Firefox and Safari are not supported.
# Handling errors
Source: https://docs.callprep.app/guides/handling-errors
Best practices for handling errors and retries in production.
## Error categories
**Client errors (4xx)** — caused by invalid requests. Fix the request before retrying.
**Server errors (5xx)** — caused by infrastructure issues. Safe to retry with backoff.
**Job failures** — `status: failed` in research results. Usually safe to resubmit.
## Production-ready error handler
```javascript theme={null}
class CallPrepClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
}
async research(email, options = {}) {
const res = await fetch(`${this.baseUrl}/research`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, ...options }),
});
const data = await res.json();
if (!res.ok) {
throw new CallPrepError(data.error, res.status, data);
}
return data;
}
async pollStatus(researchId, { intervalMs = 5000, timeoutMs = 180000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await sleep(intervalMs);
const res = await fetch(`${this.baseUrl}/research-status/${researchId}`, {
headers: { 'Authorization': `Bearer ${this.apiKey}` },
});
const data = await res.json();
if (data.status === 'completed') return data;
if (data.status === 'failed') {
throw new CallPrepError('job_failed', 200, data);
}
}
throw new CallPrepError('polling_timeout', 0, { researchId });
}
}
class CallPrepError extends Error {
constructor(code, status, body) {
super(`CallPrep error: ${code}`);
this.code = code;
this.status = status;
this.body = body;
}
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
```
## Handling specific error codes
```javascript theme={null}
try {
const { research_id } = await client.research(email);
const result = await client.pollStatus(research_id);
return result.data;
} catch (err) {
if (!(err instanceof CallPrepError)) throw err;
switch (err.code) {
case 'credits_exhausted':
case 'trial_limit_reached':
// Notify user to upgrade — don't retry
notifyUser('Credit limit reached. Please upgrade your plan.');
break;
case 'invalid_key':
// Configuration issue — alert the developer
alertDev('Invalid CallPrep API key');
break;
case 'invalid_email_format':
// Validate email before calling API
console.warn('Invalid email:', email);
break;
case 'job_failed':
// Pipeline error — safe to retry once
console.error('Research job failed:', err.body);
break;
case 'polling_timeout':
// Job took too long — check research_id manually
console.error('Polling timed out for:', err.body.researchId);
break;
default:
// Unexpected error — log and alert
console.error('Unexpected CallPrep error:', err);
}
}
```
## Retry strategy
| Error | Retry? | Strategy |
| ---------------------- | ------ | ---------------------------------- |
| `credits_exhausted` | ❌ | Wait for next billing cycle |
| `invalid_key` | ❌ | Fix configuration |
| `invalid_email_format` | ❌ | Fix input data |
| `job_failed` | ✅ | Retry once after 5s |
| `5xx` errors | ✅ | Exponential backoff, max 3 retries |
| `polling_timeout` | ✅ | Resubmit with fresh `research_id` |
## Logging
Always log the `research_id` — it's essential for debugging with support:
```javascript theme={null}
const { research_id } = await client.research(email);
logger.info('Research started', { research_id, email });
const result = await client.pollStatus(research_id);
logger.info('Research completed', { research_id, duration_ms: result._meta?.duration_ms });
```
# Polling best practices
Source: https://docs.callprep.app/guides/polling-best-practices
How to efficiently poll for research results without wasting requests.
## Recommended polling pattern
Poll every **5 seconds** with a maximum timeout of **3 minutes**.
Most requests complete in 20–60 seconds.
```javascript theme={null}
async function pollResearch(researchId, apiKey, options = {}) {
const {
intervalMs = 5000, // 5 seconds between polls
timeoutMs = 180000, // 3 minute max
onProgress = null,
} = options;
const BASE_URL = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, intervalMs));
const res = await fetch(`${BASE_URL}/research-status/${researchId}`, {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
const data = await res.json();
onProgress?.(data);
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(`Research failed: ${data.error}`);
}
throw new Error('Research timed out after 3 minutes');
}
```
## Status values
| Status | Meaning | Action |
| ------------ | ------------------------------------------- | ------------------- |
| `queued` | Job is waiting to be picked up by a worker | Continue polling |
| `processing` | Pipeline is running | Continue polling |
| `completed` | All enrichment steps finished successfully | Use the data |
| `failed` | Pipeline encountered an unrecoverable error | Check `error` field |
## Polling does not consume credits
`GET /research-status` is **free** — you can poll as many times as needed.
Credits are only consumed when you call `POST /research`.
## Cache hits return immediately
If data for the same email was recently enriched (within cache TTL),
`status` will be `completed` on the **first poll** — sometimes even before your first poll.
Build your code to handle an immediate `completed` response.
```javascript theme={null}
// Always check status immediately after submitting
const initial = await fetch(`${BASE_URL}/research-status/${researchId}`, ...);
const data = await initial.json();
if (data.status === 'completed') {
// Cache hit — no need to poll
return data;
}
// Otherwise, start polling
```
## Exponential backoff (optional)
For high-volume usage, consider exponential backoff to reduce load:
```javascript theme={null}
let delay = 2000; // start at 2s
while (/* not done */) {
await new Promise(r => setTimeout(r, delay));
delay = Math.min(delay * 1.5, 10000); // cap at 10s
// ... check status
}
```
# Email Integration
Source: https://docs.callprep.app/integrations/email
Send outreach from your own mailbox — Gmail, Outlook or your own domain — with per-mailbox limits, warm-up, threading, and reply and bounce detection.
## Overview
Connect the mailbox your outreach is sent from. Emails go out **through your real
mailbox**, so they look and thread like anything else you send, and replies come
back to you.
What is sent — who gets contacted, what each email says, how many days apart — is
set up in **Dashboard → Autopilot**. A connected mailbox on its own never sends
anything.
```
Autopilot decides who and what
↓
One mailbox is assigned to the lead for the whole sequence
↓
First email · follow-ups reply inside the same thread
↓
A reply (or a hard bounce) stops the emails
↓
Everything logged on the HubSpot contact
```
***
## Setup
Go to **Integrations → Email** and click **Add another mailbox** (or connect your
first one). The page shows how many mailboxes your plan allows — *N of M
mailboxes*.
### Option A — Gmail or Google Workspace (recommended)
Click **Connect Gmail** and approve access. Sending happens through your real
Gmail account: no DNS to set up, best deliverability, native threading.
### Option B — Outlook or Microsoft 365 (recommended)
Click **Connect Outlook** and approve access. Same as above, through Microsoft.
### Option C — Your own domain
For a sending address on a domain you own that is not on Google or Microsoft.
Click **Set up a custom-domain address**, enter the from name and address, then
add the DNS records shown to your domain and click **Check verification**. DNS
changes usually take a few minutes.
Personal mailbox domains (gmail.com, outlook.com, yahoo.com…) are rejected on
this path — use the Gmail or Outlook buttons instead.
With a custom-domain address, replies land in your mailbox and CallPrep cannot
see them, so sequences are **not** stopped automatically by an email reply.
See *Replies and bounces* below.
### Finish: your sender identity
Commercial email has to say who is sending it. Under
**Settings → Outreach → Email signature** set your name, your company and a
postal address, and optionally your photo or logo, phone and website.
Until the identity is complete, email steps are **held** — the dashboard shows
an "Emails are on hold" banner with a link to the right card. LinkedIn,
WhatsApp and Telegram are unaffected.
Every outreach email also carries a standard **unsubscribe header**, which mail
clients turn into their own one-click unsubscribe button. You can add a visible
opt-out as well: a P.S. in your own words ("just tell me and I'll stop") or a
plain unsubscribe link. An unsubscribe stops every channel for that person, not
just email.
***
## Mailbox settings
Open a mailbox row to see its settings.
| Setting | What it does |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Timezone, Send from / Send until, Send on weekends** | The window this mailbox may send in |
| **Daily limit** | Emails per day from this address (up to 500). Pick what a person could plausibly send |
| **Of which imported** | A sub-limit for imported leads, so a bulk list cannot eat the day's capacity that fresh leads need. Defaults to 70% of the daily limit |
| **This mailbox sends** | *Everything*, *Organic leads only*, or *Imported leads only* — useful for keeping bulk-list risk off the address you actually talk to people from |
| **Collect replies here** | Marks this mailbox as the reply address for the account. Only a verified mailbox can be chosen |
| **Pause this mailbox** | Sequences skip it and send from the others. Leads already assigned to it wait |
### Warm-up
A newly connected mailbox starts at **5 emails on its first day and climbs by 5 a
day** until it reaches the daily limit you set. A brand-new address has no
reputation to spend, and providers score the pattern: fifty cold emails on day one
reads as a list.
The row shows `5/day · warming up` while this is happening, and how many days are
left until the full limit.
You can switch warm-up off if the address genuinely is not new to sending — an
established domain, or a mailbox reconnected after being removed. CallPrep will
warn you first and record the decision, because the cost of getting it wrong is
the reputation of the address itself, not just one campaign.
***
## How sending works
* **One mailbox per lead.** A lead is assigned a mailbox when it enters a sequence
and keeps it, so the whole conversation comes from one address.
* **Follow-ups thread.** Later emails reply inside the first email's thread, with
a `Re:` subject, so the lead sees one conversation rather than five cold emails.
This is a per-Autopilot setting.
* **First emails come first.** When there is more to send than the daily limit
allows, leads who have never been contacted are served before follow-ups — the
capacity bar on the Autopilot page shows the split, and **Upcoming sends** shows
the queue behind it.
* **One email per lead per day**, regardless of how many steps come due.
* **Several mailboxes share the load.** New leads are assigned to whichever
mailbox has the most room, counting warm-up, pause state and role.
***
## Replies and bounces
**Replies.** For Gmail and Outlook mailboxes, CallPrep notices a reply and stops
the sequence (if the Autopilot has *stop on reply* on, which is the default).
Out-of-office and auto-replies do not count as an answer. A reply logged on the
HubSpot contact also stops the sequence, which is the safety net for
custom-domain mailboxes.
**Bounces.** A permanent bounce stops the **email** steps for that lead — other
channels continue, since it is the address that failed, not the person. If more
than **5% of a mailbox's emails bounce permanently** over a week (measured once
it has sent at least 20), the mailbox is **paused automatically** and you get an
email explaining why. Temporary delays do not count.
***
## What CallPrep can read
| Provider | Access requested | What that means |
| ----------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------- |
| Gmail / Workspace | Send email · read message **metadata** | Headers only — sender, recipient, subject line, dates. Message bodies are not accessible |
| Outlook / Microsoft 365 | Send email · read basic mail | Everything except message bodies, attachments and previews |
| Own domain | none | Sending only; CallPrep has no access to your mailbox |
The reading is used for one purpose: noticing that a lead answered, and that a
message bounced. Only addresses that belong to a lead in a live sequence are
matched — anything else is passed over and nothing about it is stored.
***
## When a mailbox stops working
OAuth access can end without warning — a password change, a revoked grant, an
admin policy. When that happens the mailbox is marked **Disconnected**, the
dashboard shows a red banner, and you get an email asking you to reconnect. The
other mailboxes keep sending in the meantime, at reduced capacity.
A send that failed because the authorization was dead is **retried after you
reconnect** — those leads are not skipped.
If a send fails for another reason, the mailbox row explains it in plain language
(a Google account without Gmail enabled, a domain not verified yet, rate
limiting), with the provider's own wording under **Technical details**.
***
## Credits
Researching a lead costs **one credit**. Sending emails is free, and so is a
skipped or rescheduled email. See [Credits](/plans/credits).
***
## Frequently asked questions
On Gmail it can read **headers only** — who a message is from, to, its subject
and date — never the body. On Outlook it can read everything except the body,
attachments and previews. With a custom-domain sender it has no read access at
all. It uses this to spot replies and bounces for leads in a live sequence.
It depends on your plan — the top of the Email page shows *N of M mailboxes*
and hides the add button once you are at the limit. Reconnecting an address you
already have never counts as a new mailbox, so you can always repair what you
have. See the [Billing page](https://call-prep-api.vercel.app/billing).
That is the warm-up ramp: 5 on day one, +5 each day until it reaches your daily
limit. The row shows how many days are left. It can be switched off, but only
do that if the address has an established sending history.
Your sender identity is incomplete. Commercial email must identify the sender,
so email steps wait until your name, company and postal address are filled in
under **Settings → Outreach → Email signature**. Other channels keep running.
No. Follow-ups are sent as replies in the first email's thread, keeping one
subject and one conversation. You can turn threading off per Autopilot.
Not on Gmail or Outlook: the reply is detected and the sequence stops, and the
check is repeated right before each send. With a custom-domain mailbox CallPrep
cannot see the reply — log the email on the HubSpot contact, or close the lead
from the Autopilot's Activity view.
More than 5% of its emails bounced permanently over the last week (counted once
it has sent at least 20). That is a deliverability alarm, not a quota — check
where the addresses came from before resuming. You will have had an email
explaining it.
The mailbox marked **Collect replies here**. If a lead's sequence is sent from a
different mailbox, that address is used as the reply-to, so the conversation
lands in one place.
Yes — that is what **This mailbox sends** is for. Set one to *Imported leads
only* and another to *Organic leads only*, and bulk-list risk stays off the
address you actually converse from.
Connect the mailbox you can sign in to. Aliases and shared mailboxes work only
when the provider lets that account send as the address — if it does not, the
send is rejected and the row will say so.
The person is added to your suppression list and every active sequence for them
stops — on all channels, not just email. Suppression is permanent and is never
cleared by a re-import.
***
## Need help?
Contact us at [hello@callprep.app](mailto:hello@callprep.app) and we'll help you
get your mailbox connected.
# HubSpot Integration
Source: https://docs.callprep.app/integrations/hubspot
Connect the CRM that feeds your Autopilots — new contacts start the research and outreach, and every message is written back onto the contact timeline.
## Overview
HubSpot is where your leads come from. Once connected, CallPrep watches for
**newly created contacts** and, for each one:
1. Checks it against your Autopilot filters — does anyone want this lead?
2. Researches the person and their company, but **only if a filter matches**
3. Enrols the lead into that Autopilot's sequence
4. Writes every message that goes out back onto the HubSpot contact timeline
5. Watches the contact for a reason to stop — a reply, a logged call, a booked
meeting, or a property you nominate
There is nothing to schedule and no sync to run. A contact created in HubSpot
reaches your Autopilots within about a minute.
**Changed in 2026.** The HubSpot integration used to pull contacts on a
schedule, filter them by lead score, and write results into custom `cp_`
contact properties. That is gone. Leads now arrive as they are created, who
gets contacted is decided by each Autopilot's own filter, and results are
written to the contact timeline instead of custom fields. Properties created by
the old version are left untouched in your portal — nothing fills them any
more, and you can delete them in HubSpot if you want to.
***
## Before you start
* **A product.** Everything runs on your product description — it is what the AI
researches against and writes from. Add it under **Settings → Product**.
* **A HubSpot account** where you can approve app installs. If you cannot install
apps yourself, your HubSpot admin has to run the connect step.
***
## Setup
### Step 1 — Connect HubSpot
In the dashboard, go to **Integrations → HubSpot**, pick the product you want to
integrate, and click **Connect HubSpot**. Approve the request in HubSpot and you
are returned to CallPrep with the connection showing as *Connected*.
During the install HubSpot shows a yellow banner saying the app has not been
reviewed or approved by HubSpot, and asks you to confirm before finishing. That
is what HubSpot shows for every app not yet listed in its Marketplace — our
listing is in progress. Click through to continue.
**One product per connection.** The connection is tied to the product you
picked. To point it at a different product, disconnect and connect again.
### Step 2 — Decide who gets contacted
The connection itself contacts nobody. That decision lives in
**Dashboard → Autopilot**, where each Autopilot has its own lead filter built
from your HubSpot contact properties — for example *Lifecycle stage is customer*,
or *Country is Poland or Germany*.
Two account-wide filters sit above that:
| Setting | Where | What it does |
| --------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Addresses to skip** | Dashboard → Autopilot → Settings | Skips role mailboxes (`info@`, `contact@`, `sales@`…) and free mailbox providers (Gmail, Outlook, Yahoo…). Both **off** by default. |
| **Excluded domains** | Settings → Outreach | Companies you never want researched or contacted — your own domain, partners, existing customers. |
Both are checked before anything else, so a skipped contact costs no research and
no credit.
### Step 3 — Connect a channel and turn an Autopilot on
Messages need somewhere to send from:
[LinkedIn](/integrations/linkedin), [Email](/integrations/email),
[WhatsApp](/integrations/whatsapp) or [Telegram](/integrations/telegram).
***
## Permissions CallPrep asks for
| Permission | Why it is needed |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Read contacts and their properties | Match leads against your filters, read the phone number or handle to message, and watch for a reason to stop |
| Write to contacts | Log outreach on the contact timeline — a sent email becomes an Email activity, everything else a Note |
| Read contact property settings | List your portal's properties in the pickers, so you can build filters without typing internal names |
| Read owners | On a portal shared by several reps, route each lead to the CallPrep account of the HubSpot owner |
**CallPrep never edits your contacts.** The write permission is used for timeline
activity and nothing else — no property is changed, and no deal, pipeline,
workflow or list is touched.
If you connected HubSpot a long time ago and leads are not arriving on a portal
shared with colleagues, click **Reconnect**. A permission added after your
connection was made is not granted retroactively, and without owner access
every lead on a shared portal is skipped.
***
## What lands in HubSpot
For every message that actually goes out, CallPrep logs it on the contact:
* **Emails** appear as an *Email* activity on the timeline, with the subject and
body that were sent
* **LinkedIn invites and messages, WhatsApp and Telegram messages** appear as a
*Note*
Notes say whether the message was written and sent by the Autopilot or typed by
hand, so a colleague reading the record knows what they are looking at.
***
## When the outreach stops
A sequence should end the moment a human takes over. CallPrep reads these signals
from HubSpot, and each one can be switched on per Autopilot:
| Signal | What it means |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reply** | The lead answered — on email, LinkedIn, WhatsApp or Telegram. Counted as a win. |
| **Call connected** | A call logged on the contact with a *Connected* outcome. |
| **Meeting booked** | A meeting logged on the contact after the lead entered the sequence. |
| **Property changed** | A contact property you nominate reaches a value you nominate — for example *Lifecycle stage is opportunity*. Leave the value empty to stop on any change. |
| **Email logged in HubSpot** | An incoming email recorded on the contact, even if it landed in a mailbox CallPrep cannot read. |
Stop conditions are **read** from HubSpot with your own connection, right before
each message goes out and again between messages — so an answer that arrives
overnight is seen before the next message is sent.
***
## The waiting queue
Contacts are often created *before* the data that qualifies them: someone signs
up, the contact is created, and the onboarding answers your filter needs arrive
three minutes later.
So a contact that matches nothing yet is **not** thrown away and **not**
researched. It waits in the queue and is re-checked as its CRM data arrives. If
nothing matches within the wait time, it is **abandoned**: never researched, no
credit spent.
* The wait time is set in **Dashboard → Autopilot → Settings** (2 to 48 hours,
6 hours by default)
* The queue is visible at the bottom of the Autopilot list — **Queue — N waiting**
* If you have a **fallback Autopilot**, it gets the final check at the end of the
wait and takes any lead its own filter matches
***
## Several reps on one HubSpot portal
Each rep connects HubSpot from **their own** CallPrep account and connects their
own channels. A new contact is routed to the CallPrep account of the person who
**owns it in HubSpot**, and the outreach goes out from that person's LinkedIn
seat or mailbox. Nobody sends on anyone else's behalf.
***
## Credits
One credit per **researched** contact — the same pool as the API and the Chrome
extension.
* A contact that matches no Autopilot and is abandoned costs **nothing**
* A contact skipped by *Addresses to skip* or *Excluded domains* costs **nothing**
* With **Research each contact only once** on (Settings → Outreach, on by
default), a contact re-created or re-fired by HubSpot is not researched twice
See [Credits](/plans/credits) for monthly limits and reset dates.
***
## Disconnecting
**Integrations → HubSpot → Disconnect** removes CallPrep's access. New contacts
stop reaching your Autopilots immediately; leads already enrolled keep running on
what CallPrep already knows about them, minus any stop signal it can no longer
read. Everything already written to your HubSpot timeline stays.
***
## Troubleshooting
| Symptom | What to check |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nothing happens when a contact is created | Is at least one Autopilot **enabled**? Does its filter match the contact's current property values? Is the address caught by *Addresses to skip* or *Excluded domains*? |
| Leads land in the queue and are abandoned | The filter never matched. Open **Queue → Abandoned**, then compare the filter against the values those contacts actually have. |
| Works for you, not for a colleague's contacts | The colleague needs their own CallPrep account with HubSpot connected. If they have one, click **Reconnect** on both accounts so owner access is granted. |
| Everything is enabled but nothing sends | The Autopilot needs a channel that is actually connected — and the lead needs an address on it (a LinkedIn profile, a phone number, a handle). |
***
## Frequently asked questions
No. Lead score filtering belonged to the old scheduled sync and is gone. Each
Autopilot now has its own filter over any contact properties you like — you can
still filter on a score property if you have one, using **greater than** or
**between**.
Usually inside a minute. If it does not match a filter yet it goes into the
queue and is re-checked every minute for the first quarter of an hour, then
every few minutes, until your wait time runs out.
No. It reads contacts and writes activity onto the contact timeline. It never
edits contact properties, deals, pipelines, workflows or lists.
They stay in your portal exactly as they are. Nothing has filled them since the
scheduled sync was retired, so they are safe to delete in HubSpot if you want
the space back. Results now live on the contact timeline instead.
Not from one CallPrep account — the connection carries one product, and
switching products means disconnecting and connecting again. Separate CallPrep
accounts can each connect the same portal with their own product.
Because CallPrep is not yet listed in the HubSpot App Marketplace — the banner
and the extra confirmation dialog appear for every unlisted app, whoever built
it. Our domain is verified with HubSpot and the listing is in progress. The
install is a standard OAuth grant and you can revoke it in HubSpot at any time.
Open **Dashboard → Autopilot → Queue**. *Waiting* means the wait time has not
run out and the lead is still being re-checked; *Abandoned* means it never
matched a filter and was dropped without spending a credit. From the abandoned
list you can still send a lead to an Autopilot by hand.
Yes — every contact created in the portal is treated the same way, including
contacts created by an import. Before loading an old list, check your Autopilot
filters, and use *Excluded domains* for anything you do not want touched.
Yes. Open the Autopilot's **Activity**, find the lead and close it — the
remaining messages are cancelled and nothing else about the Autopilot changes.
No. Connecting the app is the whole setup. Do not build a workflow to push
contacts to CallPrep — new contacts already arrive on their own, and a workflow
that re-creates or re-fires contacts can only cause duplicate work.
***
## Need help?
Contact us at [hello@callprep.app](mailto:hello@callprep.app) and we'll get back
to you within one business day.
# LinkedIn Integration
Source: https://docs.callprep.app/integrations/linkedin
Connect the LinkedIn account your outreach sends from — personalised connection requests and follow-up messages, within limits that keep your account safe.
## Overview
This page is about **one thing**: connecting the LinkedIn account (a *seat*) that
your outreach is sent from, and setting the limits it sends within.
What is actually sent — who gets contacted, what each message says, how many days
apart — is set up in **Dashboard → Autopilot**. A connected seat on its own never
sends anything.
```
Autopilot decides who and what
↓
Connection request with a note written for that person
↓
They accept
↓
Follow-up message on LinkedIn · reply stops the sequence
↓
Everything logged on the HubSpot contact
```
Invites always go out from **your own** LinkedIn account. CallPrep never uses a
shared or synthetic profile.
***
## Setup
Go to **Integrations → LinkedIn**. There are three ways to connect — all three
end with the same result, so pick whichever suits how you sign in to LinkedIn.
### Option A — Chrome extension (one click)
Install the CallPrep Chrome extension, open it and go to
**Settings → LinkedIn integration**. It reuses the session you are already signed
into, so it works with Google / SSO logins and needs no password.
### Option B — Email and password
Click **Connect account** and sign in to LinkedIn in the window that opens.
Two-factor authentication is fully supported — authenticator app, SMS or one-time
code.
### Option C — Session cookie (for Google / SSO sign-in)
If you sign in to LinkedIn with Google and have no LinkedIn password, click
**Sign in to LinkedIn with Google? Connect without a password** and follow the
steps: in a browser where you are logged into LinkedIn, open DevTools (F12) →
**Application → Cookies → [https://www.linkedin.com](https://www.linkedin.com)**, copy the value of the
`li_at` cookie and paste it in.
The cookie is used once to establish the connection and is not stored by
CallPrep.
***
## Sending limits
Open a connected seat to set its schedule. These are the settings that protect
the account:
| Setting | Default | Notes |
| -------------------------- | ----------------------- | ----------------------------------------------------------- |
| **Timezone** | Your browser's timezone | Everything below is in this timezone |
| **Send from / Send until** | 06:00 – 22:00 | Outside 06:00–22:00 you must confirm the risk before saving |
| **Send on weekends** | Off | Turning it on also needs a confirmation |
| **Daily limit** | Up to **20** invites | A hard ceiling — it cannot be raised, only lowered |
**20 invites a day per account is the maximum.** LinkedIn restricts accounts
that behave unlike a person — sending at night, at weekends, or at volume. To
send more, connect an additional LinkedIn account: each one has its own
allowance.
On top of the daily limit, CallPrep spaces sends out on its own:
* **5–15 minutes** between two invites from the same account, randomised
* Up to **90 minutes** of random delay when a batch of leads arrives at once
* Anything that does not fit today's limit or window is moved to the next
available slot — it is never dropped for being late
***
## What gets sent
**The connection note** is written per lead from the research and from the angle
you gave the step in your Autopilot. LinkedIn allows 200 characters, so notes are
short by necessity. If a note comes back longer, it is trimmed back to a complete
sentence — never cut mid-word — and the Activity timeline marks it as shortened.
**If there is nothing credible to say, nothing is sent.** When the research turned
up no usable angle, or the lead's name is not a real name (a role mailbox like
*Info* or *Admin*), that invite is skipped with a reason on the timeline rather
than sent as a generic note.
**Follow-up messages** are a separate step type and only work on people you are
connected to. A message step waits for the invite to be accepted and, if that has
not happened within a week of its scheduled date, it is dropped with the reason
*never connected*. Message steps allow more room than the note, but stay short on
purpose — a long DM reads as a pitch.
**People you are already connected to are never invited.** The invite step is
skipped as *already connected*, and any message steps behind it run normally.
***
## Multiple accounts
Connect more than one LinkedIn account to increase throughput — each has its own
20-a-day allowance, its own hours and its own timezone. On a shared HubSpot
portal, a lead is routed to the rep who owns the contact, and goes out from that
rep's own seat.
***
## Pausing, reconnecting and disconnecting
* **Pause** stops that seat sending while keeping the connection. Leads stay
enrolled and resume when you unpause.
* **Reconnect** is what a *checkpoint* or *disconnected* status needs — LinkedIn
asked for verification again, or the session ended.
* **Disconnect** removes the account. LinkedIn steps have nowhere to send from
until another account is connected.
| Status | What it means |
| ----------------------------- | ------------------------------------------------------------------------------------ |
| **Connected** | Ready to send |
| **Pending** | The connection was started but not finished — complete the sign-in |
| **Checkpoint** | LinkedIn asked for extra verification. Reconnect, or use the email + password method |
| **Disconnected** | The session ended. Reconnect |
| **Error · already connected** | This LinkedIn account is already on your CallPrep account — remove the duplicate row |
***
## Credits
Researching a lead costs **one credit**. Sending invites and messages is free, and
so is a skipped or rescheduled touch. See [Credits](/plans/credits).
***
## Frequently asked questions
You do not have to. The Autopilot writes each note and message per lead from the
research and from the angle you set on the step. What you control is the angle,
the tone and the goal — in **Dashboard → Autopilot**.
No. It is enforced in the product, not just in the form, so a higher number
would be a setting that silently does nothing. Connect another LinkedIn account
to add another 20 a day.
The limits exist for exactly this reason: no more than 20 invites a day, a
randomised gap between them, working hours only, weekends off by default, and no
invite to someone you are already connected to. You can go outside the safe
hours or send at weekends, but only deliberately — CallPrep asks you to confirm
and records that you did.
Yes — use the Chrome extension (one click, no password), or the session-cookie
option on the LinkedIn integration page.
No. A lead is enrolled in one Autopilot at a time, an invite is sent once per
sequence, and anyone you are already connected to is skipped instead of invited.
Whatever your Autopilot says. If you switched on **LinkedIn invite accepted**
as a stop condition, the sequence ends there and the acceptance is counted as a
win. If you did not, the follow-up messages you designed keep going.
Open the Autopilot's **Activity** and expand the lead — every skip carries its
reason: no LinkedIn profile found, already connected, no credible angle to write
from, or the seat's daily limit and window. Nothing is skipped silently.
There is no per-invite approval queue any more. Use **Show example** on a step
in the Autopilot builder to see exactly what that step produces for a real lead
of yours, and adjust the angle until you like it.
Yes — HubSpot is what tells CallPrep a lead exists. See
[HubSpot Integration](/integrations/hubspot).
No, and they should not: LinkedIn ties the account to a person and to their
usual devices and locations. Each rep connects their own account from their own
CallPrep account.
***
## Need help?
Contact us at [hello@callprep.app](mailto:hello@callprep.app) and we'll help you
get running.
# n8n
Source: https://docs.callprep.app/integrations/n8n
Automate prospect research with n8n workflows.
## Overview
[n8n](https://n8n.io) is a workflow automation platform that lets you connect CallPrep
to your CRM, email tools, Slack, and more — without writing code.
The official CallPrep n8n template is coming soon.
In the meantime, use the HTTP Request node as described below.
## Setting up credentials
1. In n8n, go to **Credentials** → **New** → **Header Auth**
2. Set **Name** to `Authorization`
3. Set **Value** to `Bearer cp_live_your_api_key_here`
4. Save as **CallPrep API**
## Workflow: Research a prospect from a form submission
This workflow triggers when a new prospect is added (e.g. from a Typeform or HubSpot form)
and enriches them using CallPrep.
### Step 1 — Trigger
Use any trigger node: Webhook, HubSpot, Salesforce, Google Sheets, etc.
### Step 2 — POST /research
Add an **HTTP Request** node:
| Field | Value |
| ----------------- | ---------------------------------------------------------------- |
| Method | POST |
| URL | `https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research` |
| Authentication | Header Auth → CallPrep API |
| Body Content Type | JSON |
**Body:**
```json theme={null}
{
"email": "={{ $json.email }}",
"prospect_name": "={{ $json.name }}",
"company_name": "={{ $json.company }}"
}
```
### Step 3 — Wait 30 seconds
Add a **Wait** node set to 30 seconds.
### Step 4 — GET /research-status
Add another **HTTP Request** node:
| Field | Value |
| -------------- | ------------------------------------------------------------------------------------------------ |
| Method | GET |
| URL | `https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research-status/={{ $json.research_id }}` |
| Authentication | Header Auth → CallPrep API |
### Step 5 — IF: Check status
Add an **IF** node:
* Condition: `{{ $json.status }}` equals `completed`
* True → continue to next step
* False → loop back to Wait node (use a **Loop** node)
### Step 6 — Use the data
Send the enriched data to Slack, update your CRM, create a brief document — whatever fits your workflow.
## Example use cases
Trigger on new HubSpot contact → CallPrep research → Update HubSpot contact properties
with summary, company insights, and opening talk tracks.
Trigger 30 minutes before a Google Calendar meeting →
Research the attendee's email → Post a brief to your Slack channel.
Webhook from LinkedIn → CallPrep → Add to CRM with full enrichment.
## Coming soon
The official CallPrep n8n node will support:
* Native credential management
* Automatic polling (no Wait node needed)
* Structured output mapping
# Integrations overview
Source: https://docs.callprep.app/integrations/overview
How HubSpot, LinkedIn, Email, WhatsApp and Telegram fit together — one CRM as the source of leads, four channels the outreach sends from.
## The short version
CallPrep has three moving parts, and every integration belongs to one of them.
| Part | What it is | Where you set it up |
| ---------------- | ----------------------------------------------------------------------------------- | -------------------------- |
| **The source** | HubSpot. A new contact in your CRM is what starts everything. | Integrations → HubSpot |
| **The decision** | Autopilot. Who gets contacted, what is said, on which channels, and when it stops. | Dashboard → Autopilot |
| **The channels** | LinkedIn, Email, WhatsApp, Telegram — the accounts messages are actually sent from. | Integrations → the channel |
```
New HubSpot contact
↓
Does it match one of your Autopilots? ──── no ──→ waits in the queue, then is abandoned
↓ yes (never researched, no credit spent)
Research the person and their company (1 credit)
↓
Enrol into that Autopilot's sequence
↓
Messages sent from your own LinkedIn / mailbox / WhatsApp / Telegram
↓
Every message logged back on the HubSpot contact · a reply stops the sequence
```
Connecting a channel does not send anything on its own. A channel only sends
when an Autopilot has a step on it — see **Dashboard → Autopilot**.
***
## The channels at a glance
| Channel | What you connect | Hard daily limit | Cold outreach | Typical plan |
| ---------------------------------- | -------------------------------------------------- | ---------------------------------- | ----------------------------------- | ----------------- |
| [LinkedIn](/integrations/linkedin) | Your own LinkedIn account (a "seat") | 20 invites per account | Yes | Individual and up |
| [Email](/integrations/email) | Gmail, Outlook / Microsoft 365, or your own domain | Up to 500 per mailbox, your choice | Yes | BDR and up |
| [WhatsApp](/integrations/whatsapp) | Your WhatsApp number (QR scan) | 20 messages | Only after you switch off warm-only | BDR+ |
| [Telegram](/integrations/telegram) | Your Telegram account (QR scan) | 20 messages | Only after you switch off warm-only | BDR+ |
Every channel sends from **your** account, never from a shared or synthetic one.
See the [Billing page](https://call-prep-api.vercel.app/billing) for what your plan includes.
***
## What each integration is for
The source of leads and the record of what happened. New contacts start the
flow; every message sent is written back onto the contact timeline.
Connection requests with a personalised note, and follow-up messages once the
request is accepted.
Outreach from your real mailbox, threaded into one conversation, with reply
and bounce detection.
Short follow-ups to leads who already engaged. Cold messaging is off by
default on both.
***
## Setup order
1. **Add your product** — Settings → Product. Everything else runs on it.
2. **Connect HubSpot** — this is what feeds leads in.
3. **Connect at least one channel** — LinkedIn is the quickest to start with.
4. **Create an Autopilot** — Dashboard → Autopilot, from scratch or from a template.
Step 4 is the one that decides whether anything is sent. An account with
HubSpot and a LinkedIn seat connected but no Autopilot enabled will sit quietly
and never contact anyone.
***
## Need help?
Stuck on any of this? Write to [hello@callprep.app](mailto:hello@callprep.app) and
we'll get back to you within one business day.
# Telegram Integration
Source: https://docs.callprep.app/integrations/telegram
Connect your Telegram account and follow up with leads who already engaged — warm-only by default, 20 messages a day, addressed by @username.
## Overview
Connect the Telegram account your outreach is sent from. Messages go out from
**your own account**, exactly as if you typed them.
Like WhatsApp, Telegram is a follow-up channel: out of the box it messages **only
leads who already engaged** — someone who accepted your LinkedIn invite or
replied to your email.
Add a **Telegram step** to a sequence in **Dashboard → Autopilot** — a connected
account on its own never sends anything. Telegram is available on the BDR+
plan; see the [Billing page](https://call-prep-api.vercel.app/billing).
**Telegram needs a @username, not a phone number.** Telegram cannot open a chat
from a number, so a lead is reachable only if their handle is stored in HubSpot.
There is no standard HubSpot field for it — most portals need a custom property.
***
## Setup
### Step 1 — Connect your account
Go to **Integrations → Telegram** and click **Connect Telegram**. A page opens
with a QR code: scan it from your phone
(**Telegram → Settings → Devices → Link Desktop Device**). You are returned to
CallPrep and the account shows as *connected*.
### Step 2 — Create a place to store handles, and point CallPrep at it
1. In HubSpot, create a contact property for the handle if you do not have one —
for example *Telegram username*, a single-line text field.
2. In CallPrep, under **Telegram handle source**, list the properties that hold
it, in the order they should be tried.
Handles can be stored as `@name`, `name` or a `t.me/name` link — all three work.
A number will not.
A lead with no usable handle is skipped with *No Telegram handle on the contact*
on the timeline, and the handle is looked up again in HubSpot right before each
send, so one added later still gets used.
### Step 3 — Set the schedule
| Setting | Default | Notes |
| -------------------------- | ----------------------- | ----------------------------------------------------------------- |
| **Timezone** | Your browser's timezone | Everything below is in this timezone |
| **Send from / Send until** | 06:00 – 22:00 | A message at 3 a.m. is worse than no message |
| **Send on weekends** | Allowed | Telegram is not a professional network — weekends are normal here |
| **Daily limit** | Up to **20** messages | A hard ceiling. It cannot be raised, only lowered |
***
## Who can be messaged
**Warm leads only (recommended, on by default).** A Telegram step is sent only
after the lead has shown a warm signal — they accepted your LinkedIn invite or
replied to your email. Until then the message waits and is re-checked; if the
lead never warms up within three weeks it is dropped with *never warm* on the
timeline, and the rest of the sequence carries on.
**Cold outreach (off by default).** Turning warm-only off lets this account
message people who never responded. CallPrep asks you to confirm and records the
decision: an unsolicited message from an unknown account is what gets reported on
Telegram, and reported accounts are banned outright.
***
## What gets sent
Messages are written per lead from the research and from the angle you gave the
step. They are deliberately **very short** — a couple of lines, no links, no
emoji, and they say who you are.
If the research found nothing credible to build on, the message is skipped with a
reason rather than sent as filler.
Every message sent is logged as a note on the HubSpot contact, and a reply stops
the sequence on all channels.
***
## Pausing and disconnecting
* **Pause** stops this account sending while keeping the connection.
* **Disconnect** removes it. Telegram steps have nowhere to send from until an
account is connected again. You can also end the session from
**Telegram → Settings → Devices**.
***
## Frequently asked questions
No. Telegram only opens a chat from a @username, so the handle has to be in
HubSpot. This is a Telegram rule, not a CallPrep one.
Ask for it where you already ask for a phone number — a sign-up form, an
onboarding question, a booking form — and map that field to a HubSpot contact
property. Leads without a handle simply skip the Telegram step; the rest of the
sequence is unaffected.
Telegram could not resolve it: the username was changed, deleted, or was never
a username (a phone number, an email, a first name). The reason is on the step
in the Autopilot's **Activity** view.
No. It is your own Telegram account, linked as a device — so the lead sees a
message from you, not from a bot account, and can reply in the normal way.
Only if you switch **Warm leads only** off, confirm the warning and accept the
risk. Unsolicited messages are the fastest way to get a Telegram account
reported and banned.
No. The limit is enforced in the product, so a higher number would be a setting
that silently does nothing. It protects the account.
The sequence stops on every channel, and the reply counts as a win in the
Autopilot's analytics. Answer them yourself in Telegram as usual.
***
## Need help?
Contact us at [hello@callprep.app](mailto:hello@callprep.app) and we'll help you
connect your account.
# WhatsApp Integration
Source: https://docs.callprep.app/integrations/whatsapp
Connect your WhatsApp number and let the Autopilot follow up with leads who already engaged — warm-only by default, 20 messages a day, from your own number.
## Overview
Connect the WhatsApp number your outreach is sent from. Messages go out from
**your own number**, exactly as if you typed them.
WhatsApp is a follow-up channel by design. Out of the box it messages **only
leads who already engaged** — someone who accepted your LinkedIn invite or
replied to your email. Cold messaging is possible, but you have to switch it on
deliberately.
Add a **WhatsApp step** to a sequence in **Dashboard → Autopilot** — a
connected number on its own never sends anything. WhatsApp is available on the
BDR+ plan; see the [Billing page](https://call-prep-api.vercel.app/billing).
***
## Setup
### Step 1 — Connect your number
Go to **Integrations → WhatsApp** and click **Connect WhatsApp**. A page opens
with a QR code: scan it from your phone the same way you would sign in to
WhatsApp Web (**WhatsApp → Settings → Linked devices → Link a device**). You are
returned to CallPrep and the number shows as *connected*.
Keep the phone online and the linked device in place. If you unlink CallPrep
from your phone, or the session expires, the number goes to *disconnected* and
WhatsApp steps stop until you reconnect.
### Step 2 — Tell CallPrep where the phone number lives
Under **Phone number source**, list the HubSpot contact properties that hold the
lead's number, in the order they should be tried. The default is *Mobile phone
number*, then *Phone number*.
Numbers should be in international format (for example `+48 600 123 456`). A lead
with no number on any of the listed properties is skipped, with *No phone number
on the contact* on the timeline — and the number is looked up again in HubSpot
right before each send, so a number added later still gets used.
### Step 3 — Set the schedule
| Setting | Default | Notes |
| -------------------------- | ----------------------- | ----------------------------------------------------------------- |
| **Timezone** | Your browser's timezone | Everything below is in this timezone |
| **Send from / Send until** | 06:00 – 22:00 | A WhatsApp message at 3 a.m. is worse than no message |
| **Send on weekends** | Allowed | WhatsApp is not a professional network — weekends are normal here |
| **Daily limit** | Up to **20** messages | A hard ceiling. It cannot be raised, only lowered |
***
## Who can be messaged
This is the most important setting on the page.
**Warm leads only (recommended, on by default).** A WhatsApp step is sent only
after the lead has shown a warm signal — they accepted your LinkedIn invite or
replied to your email. Until then the message waits and is re-checked; if the
lead never warms up within three weeks it is dropped with *never warm* on the
timeline, and the rest of the sequence carries on.
**Cold outreach (off by default).** Turning warm-only off lets this number
message people who never responded. CallPrep asks you to confirm and records the
decision, because messaging strangers' phone numbers is the exact pattern
WhatsApp bans for — and WhatsApp bans outright, with no restricted middle state
like LinkedIn's.
***
## What gets sent
Messages are written per lead from the research and from the angle you gave the
step. They are deliberately **very short** — a couple of lines, no links, no
emoji, and they say who you are, because an unexplained message from an unknown
number gets reported.
If the research found nothing credible to build on, the message is skipped with a
reason rather than sent as filler.
Every message sent is logged as a note on the HubSpot contact, and a reply stops
the sequence on all channels.
***
## Pausing and disconnecting
* **Pause** stops this number sending while keeping the connection.
* **Disconnect** removes it. WhatsApp steps have nowhere to send from until a
number is connected again. You can also remove CallPrep from your phone under
**WhatsApp → Linked devices**.
***
## Frequently asked questions
No. It links to your existing WhatsApp account as a device, the same mechanism
as WhatsApp Web — so messages come from your normal number, with no templates
to get approved and no per-message fee.
Only if you switch **Warm leads only** off, confirm the warning and accept the
risk. We keep it on by default because unsolicited WhatsApp messages are what
gets numbers banned.
No. The limit is enforced in the product, so a higher number would be a setting
that silently does nothing. It protects the number.
Almost always because the lead has no phone number in HubSpot. Open the
Autopilot's **Activity** and expand a lead to see the reason on each step, and
check the delivery chip on the Autopilot row for how many of its leads have a
number at all. Inbound leads who filled in a form usually have one; prospected
lists usually do not.
From HubSpot, using the properties you listed under **Phone number source**, in
order. CallPrep does not buy or guess phone numbers.
Yes. The link works like WhatsApp Web: your phone has to be reachable, and if
the device is unlinked the connection ends and CallPrep tells you it is
disconnected.
There is no marker on the message — it comes from your number and reads like
something you wrote. Which is also why the messages are short, mention who you
are, and never carry a link.
The sequence stops on every channel, and the reply counts as a win in the
Autopilot's analytics. Answer them yourself in WhatsApp as usual.
***
## Need help?
Contact us at [hello@callprep.app](mailto:hello@callprep.app) and we'll help you
connect your number.
# How it works
Source: https://docs.callprep.app/introduction/how-it-works
Understanding how CallPrep enriches your prospects.
## Async pipeline
CallPrep uses an **asynchronous pipeline** — when you submit a research request,
we immediately return a `research_id` and run enrichment in the background.
Poll `/research-status/{research_id}` every 5 seconds until `status` is `completed`.
```
POST /research → research_id (instant response)
↓
[background enrichment]
↓
GET /research-status → completed + data
```
## What we enrich
For every prospect email you submit, CallPrep returns:
**Prospect data**
* Job title (translated to English), company, LinkedIn URL, photo
* AI-generated professional summary
* 3 personalised conversation starters based on LinkedIn activity
* Up to 3 recent LinkedIn posts with AI summaries
**Company data**
* Industry, HQ, revenue, employee count, tech stack
* 4 problems the company solves for customers
* 4 synergy points with **your product** (personalised per API key)
* 3 tailored discovery questions to ask in the call
* Competitive insights and recent company news
**Decision makers** *(BDR plan and above)*
* Key people at the company — Directors, VPs, C-level
* Name, title, LinkedIn profile, location
## Product personalisation
Every research is generated against **your product** — its name, description, key
features and target customers. That context is what makes synergy points, discovery
questions and opening lines specific to what you sell rather than generic sales
advice.
Your account has **one product**, shared by everything: the API, the Chrome
extension and the Autopilot. Edit it under **Settings → Product** in the
dashboard — paste your website and it drafts the description for you, or write it
by hand.
Changing the product affects everything generated **from now on**. Research
already cached is not rewritten.
## Typical completion time
Most research requests complete in **20–60 seconds**.
If the prospect was recently researched, results may return much faster.
# Overview
Source: https://docs.callprep.app/introduction/overview
Research every prospect before you talk to them — and let the Autopilot do the outreach that gets you the conversation.
# What is CallPrep?
CallPrep does two things, and you can use either one on its own:
* **Research** — give it a prospect's email address and it returns who they are,
what their company does, where your product fits, and what to open with. In the
dashboard, in a Chrome side panel, or through the API.
* **Outreach** — the **Autopilot** takes new contacts from your CRM, researches
them, and runs a personalised sequence across LinkedIn, email, WhatsApp and
Telegram from your own accounts, stopping the moment someone answers.
New CRM contact in, researched outreach out.
Your meetings, researched before you get to them.
Your first research request in under 5 minutes.
HubSpot, LinkedIn, email, WhatsApp, Telegram.
## What a research gives you
**About the person**
* Job title, company, LinkedIn profile, photo
* An AI summary of who they are professionally
* Three conversation openers drawn from their recent LinkedIn activity
* Their recent posts, summarised — and marked when a post is a reshare rather
than their own words
**About the company**
* Industry, HQ, size, revenue signals, tech stack
* What they sell and who they sell to
* Synergy points with **your** product, not generic advice
* Discovery questions tailored to the call
* Competitive notes and recent company news *(BDR+)*
**Decision makers** *(BDR and above)*
* Other senior people at that company — name, title, LinkedIn, location
## Why it is different
* **Everything is written against your product.** Your product description, key
features and target customers are the context for every insight — so "synergy
points" means points between *their* company and *your* product.
* **It does not guess.** Where the research turns up nothing credible, the
Autopilot skips that message rather than sending a generic one, and tells you
why on the lead's timeline.
* **The outreach sends from your accounts** — your LinkedIn profile, your mailbox,
your number — inside limits that protect them.
* **Results are cached**, so researching the same person again is instant.
## Where to start
| You want to | Start here |
| --------------------------------------------- | ----------------------------------------- |
| Automate outreach from your CRM | [Autopilot overview](/autopilot/overview) |
| Prepare for meetings already in your calendar | [Chrome extension](/extension/overview) |
| Enrich prospects from your own code | [Quickstart](/introduction/quickstart) |
## Base URL
```
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1
```
# Quickstart
Source: https://docs.callprep.app/introduction/quickstart
Make your first research request in under 5 minutes.
## 1. Generate an API key
Go to the [CallPrep dashboard](https://call-prep-api.vercel.app/api-keys) and create an API key.
If you have not set up your product yet, do that first under **Settings → Product** —
paste your website and it drafts the description for you. Every insight is written
against that product, so it is the difference between generic output and useful output.
Keep your API key secret. Never expose it in client-side code or public repositories.
## 2. Submit a research request
```bash theme={null}
curl -X POST \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "john.doe@acmecorp.com" }'
```
**Response:**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "processing",
"estimated_seconds": 30
}
```
## 3. Poll for results
Use the `research_id` to check the status every 5 seconds until `status` is `completed`.
```bash theme={null}
curl \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research-status/res_1a7b6e9c35a9b848 \
-H "Authorization: Bearer cp_live_..."
```
**Response when completed:**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "completed",
"completed_at": "2026-05-05T10:27:08.004Z",
"data": {
"prospect": {
"name": "John Doe",
"title": "VP of Sales",
"summary": "John is VP of Sales at Acme Corp...",
"opening_talk": [
"Your post about reducing sales cycles resonated...",
...
]
},
"company": { ... },
"decision_makers": [ ... ]
}
}
```
## Complete Node.js example
```javascript theme={null}
const API_KEY = 'cp_live_...';
const BASE_URL = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
async function researchProspect(email) {
// 1. Submit
const res = await fetch(`${BASE_URL}/research`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const { research_id } = await res.json();
console.log('Research started:', research_id);
// 2. Poll until completed
while (true) {
await new Promise(r => setTimeout(r, 5000));
const statusRes = await fetch(`${BASE_URL}/research-status/${research_id}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
});
const data = await statusRes.json();
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(data.error);
console.log('Still processing...');
}
}
// Usage
const result = await researchProspect('john.doe@acmecorp.com');
console.log(result.data.prospect.opening_talk);
```
Typical completion time is **20–60 seconds** depending on data availability.
If cached data exists, results return in under 2 seconds.
## Next steps
Learn about API key best practices
Full reference of all returned fields
How to handle errors gracefully
Automate with n8n templates
# Credits
Source: https://docs.callprep.app/plans/credits
How credits work and when they reset.
## What is a credit?
One credit = **one researched prospect**. It costs the same wherever the research
was started from, and it is one pool for the whole account:
| Started by | Costs |
| --------------------------------------------- | ------------------------------------------------------- |
| `POST /research` (API) | 1 credit |
| A research in the Chrome extension | 1 credit |
| An automatic research from a calendar meeting | 1 credit per meeting — one attendee, not the whole room |
| A lead matching an Autopilot filter | 1 credit |
A credit is consumed whether the result comes from cache or from live lookups.
## What is free
* A lead that matched no Autopilot and was **abandoned** — no research, no credit
* A lead skipped by *Addresses to skip* or *Excluded domains*
* A contact re-created or re-fired by HubSpot, while **Research each contact only
once** is on (the default)
* Sending, retrying, skipping or rescheduling any outreach message
* Polling `GET /research-status`
## Credit reset
Credits reset at the start of your **billing cycle** — the same day each month
as when you first subscribed. You'll see the exact reset date on your
[Analytics](https://call-prep-api.vercel.app/analytics) page.
Unused credits **do not roll over** to the next month.
## Monitoring usage
You can track your credit usage in real time:
* **Sidebar** — remaining credits shown in the dashboard sidebar
* **Analytics page** — daily usage chart and credit donut
## Credit alerts
Enable email notifications at 75% and 95% usage in
[Settings → Notifications](https://call-prep-api.vercel.app/settings).
## Cache hits still consume credits
When a research request returns cached data (response in under 2 seconds),
1 credit is still consumed. This is intentional — you're using the service
and the cached data was paid for on a previous request.
## Credit refunds
Credits are refunded automatically if a research job fails due to an infrastructure error
(timeout, pipeline crash). Credits are **not** refunded when:
* The prospect wasn't found in any data source
* The AI step failed but other data was returned
* You submitted an invalid email
If you believe a credit was incorrectly consumed, contact
[hello@callprep.app](mailto:hello@callprep.app) with the `research_id`.
# Data retention
Source: https://docs.callprep.app/plans/data-retention
What CallPrep stores about the people you research, how long it keeps it, what happens on a deletion request, and how GDPR applies.
## The short version
* Enriched data about a person is **deleted or stripped of personal data after 12
months**, automatically, every night.
* Working records that carry an address in passing are cleared after **90 days**.
* Meetings read from your calendar are deleted after **30 days**.
* An **unsubscribe is kept for good** — deleting it on a timer would mean
contacting someone who asked you not to.
* Deleting your account removes your data **immediately**, not on a timer.
***
## What is stored, and for how long
| What | Kept for | Then |
| ---------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| Prospect records — name, title, LinkedIn profile, photo, AI summary | **365 days** since last update | Deleted |
| Company records | **365 days** since last update | Deleted |
| Decision-maker records scraped from company pages | **180 days** | Deleted |
| Research jobs and their results | **365 days** | Personal data removed; the record of the run — date, status, cost — is kept |
| Outreach sequences and the messages sent in them | **365 days** | Names, addresses and message content removed; the record of what was sent and when is kept |
| Working records that carry an address in passing (enrolment decisions, bounce records, queued leads) | **90 days** | Deleted |
| Meetings read from Google Calendar | **30 days** | Deleted |
| Unsubscribes and suppression entries | **Kept** | Never deleted |
| Consent and audit records | **Kept** | Never deleted |
**Why some records are stripped rather than deleted.** A sent message is two
things at once: a record about a person, and a record of what your account did.
Deleting it would quietly rewrite your own history — fewer leads last quarter,
different response rates, costs that no longer add up. So the personal data comes
out and the shape of the record stays. After that it is no longer personal data,
and your analytics still say what they said yesterday.
The purge runs **daily**. Nothing has to be requested for it to happen.
***
## Refresh windows — different thing, same page
Retention is when data is *deleted*. A refresh window is how long enriched data is
considered **fresh enough to reuse** instead of paying to look it up again.
| Data | Refresh window |
| --------------- | -------------- |
| Prospect | 30 days |
| Company | 90 days |
| Decision makers | 180 days |
Inside the window, a repeat research returns what is already there — see
[Caching behaviour](/guides/caching-behavior). Outside it, the data is looked up
again and the record is overwritten.
***
## Deleting one person's data on request
If someone asks to be deleted — a prospect, a lead, anyone in your CRM — send the
request to [hello@callprep.app](mailto:hello@callprep.app) with the email address.
We run a targeted erasure:
* Their **prospect record is deleted**
* **Research jobs, sequences and sent messages** covering them keep their shape
with the personal data removed
* **Any running sequence for them is stopped**
* Their address is **added to the suppression list by default**, so a later import
cannot start contacting them again. Say so if you do not want that
* The erasure itself is **recorded permanently** — that record is the proof the
request was honoured, and when
The **company record is not deleted**: it describes an employer, is shared by
everyone at that domain, and is not personal data about the person asking.
We can scope the erasure to your account alone, or run it across every account
where that address appears — say which when you write.
***
## Deleting your account
Deleting your CallPrep account removes your data **immediately and in full**:
research, prospects, companies, sequences, messages, mailboxes, seats, integration
connections, API keys, calendar meetings.
Two things survive, deliberately and without identifying you: consent and audit
records, kept as evidence that permissions were granted and warnings were shown,
and operational error logs used to keep the service running.
***
## GDPR
CallPrep processes personal data (names, business email addresses, LinkedIn
profiles, job titles) **on your behalf, as a processor**. You are the controller,
and you are responsible for having a lawful basis for contacting the people you
put into it.
* **Data is stored in the EU** — Supabase, `eu-west-1` (Ireland)
* **Your data is scoped to your account.** Other customers never see it
* **We never sell your data**, and we never make it available to another customer
* Enrichment necessarily involves **sub-processors** — data providers, an AI model
provider, email delivery and messaging infrastructure. They are named in the DPA
* **Deletion requests** from the people you research are handled as described
above
For a **Data Processing Agreement**, write to
[hello@callprep.app](mailto:hello@callprep.app).
***
## Frequently asked questions
It runs from when the record was **last updated**. Researching the same person
again refreshes them, and the clock restarts from that point.
No. That is the reason records are stripped rather than deleted: counts, dates,
outcomes and costs stay exactly as they were.
It is a record that says "do not contact this address" — deleting it on a
schedule would produce exactly the harm the request was meant to prevent. It is
kept, it is never used for anything else, and it stops every channel.
Yes. Write to [hello@callprep.app](mailto:hello@callprep.app) and we will export
the research and outreach records on your account.
No. Your product description and prospect data are sent to the model to generate
the output you asked for, under terms that do not permit training on it.
Retention is unchanged by the plan — the same windows apply. Cancelling stops
new research; deleting your account removes the data.
# Plans overview
Source: https://docs.callprep.app/plans/overview
What each CallPrep plan includes — credits, outreach channels, mailboxes and API keys.
## Plans
**\$0** · 10 credits to try everything with no commitment
**\$25/mo** · 125 credits · LinkedIn outreach
**\$59/mo** · 300 credits · LinkedIn + email · decision makers
**\$199/mo** · 800 credits · every channel · sales pitch and company news
**Enterprise** — custom credits and seats, every channel, SSO, priority support
and a dedicated Slack channel. [Talk to us](mailto:hello@callprep.app).
Annual billing is cheaper per month — $22 on Individual, $49 on BDR, \$149 on BDR+.
Write to us to switch.
***
## What each plan includes
| | Free | Individual | BDR | BDR+ | Enterprise |
| ----------------------------------------- | ----- | ---------- | ---------- | -------- | --------------- |
| Credits per month | 10 | 125 | 300 | 800 | Custom |
| Prospect and company enrichment | ✅ | ✅ | ✅ | ✅ | ✅ |
| AI insights and discovery questions | ✅ | ✅ | ✅ | ✅ | ✅ |
| LinkedIn posts analysis | ✅ | ✅ | ✅ | ✅ | ✅ |
| Chrome extension and calendar battlecards | ✅ | ✅ | ✅ | ✅ | ✅ |
| Decision makers | ❌ | ❌ | ✅ | ✅ | ✅ |
| Sales pitch and company news | ❌ | ❌ | ❌ | ✅ | ✅ |
| **Autopilot — LinkedIn** | ❌ | ✅ | ✅ | ✅ | ✅ |
| **Autopilot — email** | ❌ | ❌ | ✅ | ✅ | ✅ |
| **Autopilot — WhatsApp and Telegram** | ❌ | ❌ | ❌ | ✅ | ✅ |
| Sending mailboxes | 1 | 2 | 3 | 5 | Custom |
| API keys | 1 | 1 | 3 | 10 | Custom |
| Support | Email | Email | Email, 24h | Priority | Dedicated Slack |
**Credits are one pool.** A research costs one credit whether it came from the
API, the Chrome extension, a calendar meeting or an Autopilot. See
[Credits](/plans/credits).
***
## Add-ons
Need more of one thing without moving up a plan — extra credits every month, an
extra mailbox, extra API keys? Write to
[hello@callprep.app](mailto:hello@callprep.app) and we will add it to your
subscription. Add-on credits stack on top of your plan's monthly allowance and
renew with it.
***
## Changing plan
Upgrades, downgrades and annual billing all go through
[hello@callprep.app](mailto:hello@callprep.app) — there is no self-serve checkout
yet. Your current plan and usage are on the
[Billing page](https://call-prep-api.vercel.app/billing) in the dashboard.
Downgrading lowers your monthly credits and can take away a channel your
Autopilots are using. Check what is running before you drop a tier.
# cURL
Source: https://docs.callprep.app/sdks/curl
Using the CallPrep API with cURL.
## Submit a research request
```bash theme={null}
curl -X POST \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{"email": "john.doe@acmecorp.com"}'
```
## With all optional fields
```bash theme={null}
curl -X POST \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research \
-H "Authorization: Bearer cp_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "john.doe@acmecorp.com",
"prospect_name": "John Doe",
"company_name": "Acme Corp",
"linkedin_url": "https://www.linkedin.com/in/johndoe",
"company_linkedin_url": "https://www.linkedin.com/company/acme"
}'
```
**Response:**
```json theme={null}
{
"research_id": "res_1a7b6e9c35a9b848",
"status": "processing",
"estimated_seconds": 30
}
```
## Poll for results
```bash theme={null}
curl \
https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1/research-status/res_1a7b6e9c35a9b848 \
-H "Authorization: Bearer cp_live_..."
```
## Shell script — submit and poll
```bash theme={null}
#!/bin/bash
API_KEY="cp_live_..."
BASE="https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1"
EMAIL="${1:-john.doe@acmecorp.com}"
# Submit
echo "Submitting research for: $EMAIL"
RESPONSE=$(curl -s -X POST "$BASE/research" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\"}")
RESEARCH_ID=$(echo "$RESPONSE" | grep -o '"research_id":"[^"]*"' | cut -d'"' -f4)
echo "Research ID: $RESEARCH_ID"
# Poll
while true; do
sleep 5
STATUS_RESPONSE=$(curl -s "$BASE/research-status/$RESEARCH_ID" \
-H "Authorization: Bearer $API_KEY")
STATUS=$(echo "$STATUS_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ]; then
echo "Done!"
echo "$STATUS_RESPONSE" | python3 -m json.tool
break
elif [ "$STATUS" = "failed" ]; then
echo "Failed!"
echo "$STATUS_RESPONSE"
exit 1
fi
done
```
**Usage:**
```bash theme={null}
chmod +x research.sh
./research.sh john.doe@acmecorp.com
```
# Node.js
Source: https://docs.callprep.app/sdks/nodejs
Using the CallPrep API with Node.js.
## Installation
No SDK required — use the native `fetch` API (Node.js 18+) or any HTTP client.
```bash theme={null}
# Optional — for older Node.js versions
npm install node-fetch
```
## Minimal example
```javascript theme={null}
const API_KEY = process.env.CALLPREP_API_KEY;
const BASE = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
async function research(email) {
// 1. Submit
const res = await fetch(`${BASE}/research`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const { research_id } = await res.json();
// 2. Poll
while (true) {
await new Promise(r => setTimeout(r, 5000));
const s = await fetch(`${BASE}/research-status/${research_id}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
});
const data = await s.json();
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(data.error);
}
}
const result = await research('john.doe@acmecorp.com');
console.log(result.data.prospect.opening_talk);
```
## With TypeScript
```typescript theme={null}
interface ResearchResult {
research_id: string;
status: 'completed' | 'failed' | 'processing' | 'queued';
completed_at: string | null;
data?: {
prospect: {
name: string;
title: string;
company: string;
summary: string;
opening_talk: string[];
};
company: {
name: string;
synergy_points: string[];
discovery_questions:string[];
};
decision_makers: Array<{
full_name: string;
position_title: string;
linkedin_url: string;
}>;
};
}
async function research(email: string): Promise {
const BASE = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
const headers = {
'Authorization': `Bearer ${process.env.CALLPREP_API_KEY}`,
'Content-Type': 'application/json',
};
const init = await fetch(`${BASE}/research`, {
method: 'POST', headers, body: JSON.stringify({ email }),
});
const { research_id } = await init.json();
while (true) {
await new Promise(r => setTimeout(r, 5000));
const res = await fetch(`${BASE}/research-status/${research_id}`, { headers });
const data = await res.json() as ResearchResult;
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(data.error as string);
}
}
```
## With Express.js
```javascript theme={null}
import express from 'express';
const app = express();
app.use(express.json());
app.post('/enrich', async (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).json({ error: 'email required' });
try {
const data = await research(email);
res.json(data);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000);
```
# Python
Source: https://docs.callprep.app/sdks/python
Using the CallPrep API with Python.
## Installation
```bash theme={null}
pip install requests python-dotenv
```
## Minimal example
```python theme={null}
import os, time, requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('CALLPREP_API_KEY')
BASE = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1'
HEADERS = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
}
def research(email: str, **kwargs) -> dict:
# 1. Submit
res = requests.post(
f'{BASE}/research',
headers=HEADERS,
json={'email': email, **kwargs},
)
res.raise_for_status()
research_id = res.json()['research_id']
# 2. Poll
while True:
time.sleep(5)
status_res = requests.get(
f'{BASE}/research-status/{research_id}',
headers=HEADERS,
)
data = status_res.json()
if data['status'] == 'completed':
return data
if data['status'] == 'failed':
raise RuntimeError(f"Research failed: {data.get('error')}")
# Usage
result = research('john.doe@acmecorp.com', prospect_name='John Doe')
prospect = result['data']['prospect']
print(prospect['summary'])
print('\n'.join(prospect['opening_talk']))
```
## With error handling
```python theme={null}
from requests.exceptions import HTTPError
def safe_research(email: str) -> dict | None:
try:
return research(email)
except HTTPError as e:
code = e.response.json().get('error')
if code in ('credits_exhausted', 'trial_limit_reached'):
print('Credit limit reached — upgrade your plan')
elif code == 'invalid_key':
print('Invalid API key')
elif code == 'invalid_email_format':
print(f'Invalid email: {email}')
else:
print(f'API error: {code}')
return None
except RuntimeError as e:
print(f'Job failed: {e}')
return None
```
## Batch enrichment
```python theme={null}
import concurrent.futures
def research_batch(emails: list[str], max_workers: int = 5) -> list[dict]:
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(safe_research, email): email for email in emails}
for future in concurrent.futures.as_completed(futures):
email = futures[future]
result = future.result()
if result:
results.append({'email': email, 'data': result['data']})
return results
# Enrich 10 prospects in parallel
emails = ['a@company.com', 'b@company.com', ...]
results = research_batch(emails, max_workers=3)
```
When running batch enrichment, space out requests to avoid hitting concurrent limits.
A `max_workers` of 3-5 is recommended for most use cases.
# Contact & support
Source: https://docs.callprep.app/support/contact
How to get help with the CallPrep API.
## Support channels
| Plan | Channel | Response time |
| ---------- | ----------------------- | ------------- |
| Free | Email | 48h |
| Individual | Email | 48h |
| BDR | Email | 24h |
| BDR+ | Email, priority | Priority |
| Enterprise | Dedicated Slack channel | Priority |
## Email support
[hello@callprep.app](mailto:hello@callprep.app)
When reporting an issue, please include:
* Your `research_id` (if applicable)
* The email address you were researching
* The error code or unexpected behaviour you observed
* Timestamp of the issue
## Reporting bugs
Found a bug? Email [hello@callprep.app](mailto:hello@callprep.app) with:
* Steps to reproduce
* Expected vs actual behaviour
* `research_id` if relevant
## Feature requests
We love hearing from users. Send feature requests to
[hello@callprep.app](mailto:hello@callprep.app) with subject line `Feature request: ...`
## Something looks broken
Before writing in, two places answer most of it:
* **Autopilot** — the row's delivery chip and a lead's timeline in
[Activity](/autopilot/activity) say why a message was not sent
* **API** — a job that ends `failed` carries an `error`; see
[Handling errors](/guides/handling-errors)
If it is us, tell us at [hello@callprep.app](mailto:hello@callprep.app) — include
the `research_id` or the lead's email address and roughly when it happened.
# FAQ
Source: https://docs.callprep.app/support/faq
Frequently asked questions about the CallPrep API.
Typically **20–60 seconds**. If the prospect was recently researched, results
may return much faster — sometimes in under 2 seconds.
If no data is found for the provided email, the pipeline still attempts to
enrich the company via the email domain and generate AI insights from
available data. The `status` will still be `completed`, but some fields like
`summary` or `opening_talk` may be `null`.
No. Only `POST /research` consumes 1 credit. You can poll `GET
/research-status` as many times as needed without any credit impact.
Yes. CallPrep caches enriched data per `(email, product)` combination.
Subsequent requests for the same email return data much faster. A credit is
still consumed per request — but the response may be near-instant.
All AI-generated fields (`summary`, `opening_talk`, `synergy_points`, etc.)
are generated in the language set in your account preferences. Supported
languages: English, Polish, German, French, Spanish, Italian, Portuguese,
Dutch. Source data is always translated to your preferred language.
No. Decision makers are a **plan-gated feature** available on the **BDR plan and
above**. If your plan doesn't include this feature, `decision_makers` will be an
empty array. You can check your plan on the
[Billing](https://call-prep-api.vercel.app/billing) page.
By default, CallPrep auto-detects the company LinkedIn URL from the enrichment
data. Providing `company_linkedin_url` in your request overrides
auto-detection — useful when the prospect's profile doesn't link to the
correct company page.
No. All enriched data is scoped to your account and product. Different users
never see each other's data, even for the same email address.
A job stuck in `processing` is failed automatically after 10 minutes. Most
research finishes in well under a minute, so anything past a couple of minutes
is already unusual. You can then resubmit the same request.
If this happens repeatedly, please contact
[hello@callprep.app](mailto:hello@callprep.app) with the `research_id`.
Yes — credits are refunded automatically when a job fails due to an
infrastructure error. For other cases, contact
[hello@callprep.app](mailto:hello@callprep.app) with the `research_id`.
`synergy_points` is an AI-generated list of 4 connections between the
prospect's company and **your product** — based on the product context tied to
your API key. This field is unique to CallPrep and personalised per API key,
not generic advice.
Go to **Settings → Product** in the dashboard. Your account has one product,
used by the API, the Chrome extension and the Autopilot alike. Changes apply to
everything generated from then on; research already cached is not rewritten.