Cold Email PersonalizerSign in with Google

Cold Email Personalizer docs

Cold Email Personalizer researches one lead from public sources and writes a first cold email or opening lines. Every claim in the output points to the evidence behind it. You can call it over a REST API or from an MCP client such as Claude Code.

Quick start

  1. Sign in with Google at https://coldemailpersonalizer.com/sign-in. Signing in creates your workspace.
  2. Open the API keys page and create a key. The secret starts with cep_live_ and is shown once.
  3. Add funds from the Billing page. Each run holds its price from your balance while it works.
  4. Send your first request.

A run is charged only when it returns a validated email or opening lines. The current price of each operation is at GET /v1/prices, described under Prices and charging.

Send a request

Pick a language. The HTTP tab is the raw request, for any client that can send one.

curl https://coldemailpersonalizer.com/v1/personalize \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lead": {
      "name": "Jordan Lee",
      "company_domain": "example.com"
    },
    "offer": {
      "what_you_sell": "Warehouse picking software",
      "who_it_helps": "Operations leads at multi-site 3PLs"
    },
    "output_format": "full_email"
  }'

# → the run (status "completed") or, after the wait window, { "run_id": "...", "status": "queued" }

The request waits for the run by default. If the run finishes in time, the response is the whole run. If it does not, the response is 202 with { "run_id", "status" }.

Check the run

# Check the run until status is "completed"
curl https://coldemailpersonalizer.com/v1/runs/$RUN_ID \
  -H "Authorization: Bearer $API_KEY"

A run is done when its status is completed or failed.

Authentication

The API has two ways in.

Door How Used by
API key Header Authorization: Bearer cep_live_... Your code, automation tools and headless MCP clients
Session The sign-in cookie of the dashboard The dashboard itself

Every endpoint marked "API key or session" accepts either. Endpoints marked "Session" only accept a signed-in dashboard session. An API key belongs to one workspace, and everything it creates is billed to that workspace.

MCP clients that can open a browser sign in with OAuth instead of a key. See MCP.

Manage API keys

These endpoints back the API keys page. They need a session.

Method Path Body Response
GET /v1/keys none { keys }
POST /v1/keys label, 1 to 60 characters 201 with id, label, prefix, secret, created_at
PATCH /v1/keys/:id label, 1 to 60 characters The key
DELETE /v1/keys/:id none The key, now revoked

A key in the list has id, label, prefix, status (active or revoked), created_at, last_used_at and revoked_at. The secret appears only in the POST response. Renaming a revoked key answers 409 with key_revoked. A revoked key is refused at once with 401 and invalid_key.

Personalize

POST /v1/personalize

Research one lead and write an email or opening lines. Auth: API key or session.

The body has a lead and exactly one of offer or campaign_id.

Field Type Notes
lead object Required. See the lead fields below.
offer object Your offer. Required unless you send campaign_id.
campaign_id UUID A saved campaign to use as the offer. Required unless you send offer.
output_format full_email or opening_lines Optional. Overrides the format of the offer or campaign.

Lead fields. Each is a string of at most 2,000 characters. Send linkedin_url, or email, or both name and company_domain.

Field Notes
linkedin_url The lead's LinkedIn profile URL.
email The lead's work email.
name The lead's full name. Needs company_domain.
company_domain The lead's company domain, such as example.com.
signal Something that prompted the outreach.
notes Anything else you know about the lead.
prior_contact Any earlier contact with the lead.

Offer fields. Text fields are strings of at most 2,000 characters. List fields hold at most 10 items of at most 500 characters each.

Field Type Notes
what_you_sell text Required.
who_it_helps text Required.
url text Your website.
name text Your company or product name.
core_value_proposition text
key_differentiator text
customer_outcomes list Results your customers get.
proof_points list Customers, numbers or case studies.
objections list
reference_customers list
alternatives list
integrations list
tone text
length short, medium or long
primary_goal start conversation, book meeting, ask intro or ask feedback
language two letters Such as en.
output_format full_email or opening_lines Default full_email.
example_email text An email in your voice, at most 4,000 characters.

The output only uses facts about your offer that you provide. Fill in proof points and outcomes rather than leaving them out.

Waiting and the 202 response

The query parameter wait is true by default. With wait=true the request waits for the run, up to about 50 seconds. If the run finishes in that time, the response is 200 with the whole run. If it does not, or with wait=false, the response is 202:

{ "run_id": "...", "status": "queued" }

Poll GET /v1/runs/:id until status is completed or failed.

Retries with x-external-request-id

Send the header x-external-request-id with your own id for the request, at most 255 characters. It makes retries safe.

  • A retry with the same id and the same body returns the existing run. It does not start or charge a second run.
  • A retry with the same id and a different body answers 409 with idempotency_conflict.
  • A retry after the campaign it used was edited counts as a different body, so it also answers 409 with idempotency_conflict.

Status codes

Status Meaning
200 The run finished. The body is the run.
202 The run was accepted and is still working. The body is { run_id, status }.
400 invalid_request. The body failed validation, or x-external-request-id is longer than 255 characters.
401 No valid API key or session.
402 insufficient_balance. See Errors.
404 not_found. The campaign_id does not exist in this workspace.
409 idempotency_conflict, or campaign_deleted when the campaign was deleted.
429 rate_limited. A rate limit was reached. See Rate limits.
503 billing_unavailable or rate_limit_unavailable. Try again shortly.

Runs

GET /v1/runs

List runs, newest first. Auth: API key or session.

Query Notes
status queued, running, completed or failed.
outcome ok, no_evidence, no_relevant_angle or identity_unresolved.
q Searches the lead's name, company domain, email and LinkedIn URL. 1 to 200 characters.
campaign_id Runs that used this campaign.
since ISO 8601 date and time with an offset, such as 2026-10-01T00:00:00Z.
until ISO 8601 date and time with an offset.
page Default 1.
page_size 1 to 100. Default 20.

The response is { runs, page, page_size, total }. Each item in runs is a run, as below.

GET /v1/runs/:id

Get one run. Auth: API key or session. A run that is not in your workspace answers 404 with not_found.

Field Notes
run_id The run's id.
status queued, running, completed or failed.
outcome For a completed run: ok, no_evidence, no_relevant_angle or identity_unresolved. Otherwise null.
stage While the run works: resolve_identity, research, draft or validate. Otherwise null.
charged_usd What the run cost, once it is settled. 0 when it was not charged. null before settling.
duration_ms How long the run took. null until the run finishes.
created_at, finished_at ISO 8601 times. finished_at is null until the run ends.
request What you sent: lead, offer, output_format, external_request_id and campaign_id.
output { subject, body } for an email, { lines } for opening lines, or null.
evidence The sources found. Each has id, kind, claim, source_url, excerpt, provider (always null; the data supplier is not disclosed), field, published_at, retrieved_at and tool_call.
claims Each claim in the output, as { text, evidence_ids }.
angles Angles considered, as { angle, why_relevant, evidence_ids, used }.
identity Who the lead resolved to: name, title, company, company_domain and confidence from 0 to 1.
reason Why the run ended the way it did.
error For a failed run, { code, message }. Otherwise null.
rerun_of The run this one reruns, or null.
campaign_id The campaign used, or null.

Evidence kind is public for a public web source, provider for an enrichment record, user for something you sent, or inference for a conclusion drawn from other evidence.

Evidence is attributed to the resolved person or company before it is shown. Facts about someone else with the same name are dropped and never cited.

POST /v1/runs/:id/rerun

Run the same request again as a new run. Auth: API key or session. It takes the same wait parameter and answers like POST /v1/personalize. The new run has rerun_of set to the original. A run that is still queued or running answers 409 with not_rerunnable.

Campaigns

A campaign is a saved offer. Send its id as campaign_id instead of an offer. A run copies the campaign when it starts, so later edits never change a run that already exists.

All campaign endpoints take an API key or a session.

Method Path Response
GET /v1/campaigns { campaigns }
POST /v1/campaigns 201 with the campaign
GET /v1/campaigns/:id The campaign
PATCH /v1/campaigns/:id The campaign
POST /v1/campaigns/:id/duplicate 201 with the copy, named with (copy) at the end
DELETE /v1/campaigns/:id { "deleted": true }
Field Notes
name Required. 1 to 80 characters.
what_you_sell Required. At most 2,000 characters.
who_it_helps Required. At most 2,000 characters.
website At most 2,000 characters.
proof_points One item per line. At most 2,000 characters in total. See the line rule below.
customer_outcomes One item per line. At most 2,000 characters in total. See the line rule below.
call_to_action At most 484 characters.
tone direct, friendly, conversational or formal. Default direct.
goal book_call, get_reply or share_resource. Default book_call.
language Two lowercase letters. Default en.
length very_short, short or medium. Default short.
output_format email or opening_lines. Default email.
example_email At most 4,000 characters.

Responses add id, runs_count, last_used_at, created_at and updated_at.

The line rule: proof_points and customer_outcomes become one item per non-empty line. Only the first 10 lines are used, and each line can be at most 500 characters. The call_to_action takes one of the 10 proof point slots. A campaign that could not run is refused with 400, invalid_request and details.

PATCH changes only the fields you send, and needs at least one. Deleting a campaign removes it from every list. A personalize request that names it afterwards answers 409 with campaign_deleted. A campaign that is not in your workspace answers 404 with not_found.

Refine a campaign from a website

POST /v1/campaigns/refine

Read a public website and draft campaign fields from it. Auth: API key or session.

The body is { "url": "https://example.com" }. The URL must be a public http or https address of at most 2,048 characters.

Field Notes
fields Draft values for name, what_you_sell, who_it_helps, proof_points, customer_outcomes and call_to_action. A field the site does not cover is left out. name falls back to the site's hostname.
questions Two or three questions whose answers would sharpen the offer.
source_url The URL that was read.
usable true when the site said what you sell and who it helps.
reason Why the result is not usable, when usable is false.
charged_usd What the call cost. 0 when the result is released. null only when settlement is unknown.
settlement settled when charged, released when not charged, or unknown when billing did not confirm.

Review the drafted fields before you save them as a campaign.

Status Meaning
200 The site was read. Check usable.
400 invalid_request. The URL is not a public http or https address.
402 insufficient_balance.
502 upstream_error. The site could not be read. You are not charged.
503 unavailable when refining from a website is switched off, or billing_unavailable. Try again shortly.

Prices and charging

GET /v1/prices

The current prices. It needs no API key or session.

Field Notes
personalization_usd One run of POST /v1/personalize. Default $0.50.
research_usd Research on its own. Default $0.10. No endpoint charges it today.
refinement_usd One call of POST /v1/campaigns/refine. Free by default (0).
charged_when One sentence per operation saying when it is charged.

The charging rules:

  • A personalization is charged only when the run completes with outcome ok.
  • Every other outcome and every failed run is not charged.
  • A refinement is free by default. When a price is set, it is charged only when the result is usable.
  • When a run is accepted, its price is held from your balance. The hold becomes the charge, or it is released.

GET /v1/balance

Your workspace balance. Auth: API key or session.

Field Notes
available_usd What you can spend now.
reserved_usd What is held for runs in progress.
reserved_runs How many runs hold money.
price_usd The price of one personalization.

When the balance cannot be read, the answer is 503 with balance_unavailable. It is never shown as zero.

GET /v1/usage

Runs and spending per day. Auth: API key or session.

The query period is 7d, 30d or month. The default is 30d, and month means the current calendar month in UTC.

The response has period, from, to, days and totals. Each day has date and the counts ok, no_evidence, no_relevant_angle, identity_unresolved and failed. The totals are completed, ok, failed and spent_usd.

Outcomes

A completed run has one of four outcomes.

Outcome Meaning Charged
ok An email or opening lines were written, and every claim is backed by evidence. Yes
no_evidence Nothing usable was found about the lead. No
no_relevant_angle Evidence exists, but none of it connects to your offer. No
identity_unresolved The lead could not be pinned to one person. No

Only ok has an output. The other outcomes explain themselves in reason.

Failed runs

A run with status failed has an error with a code and a message. A failed run is never charged.

Code Meaning
invalid_input The request could not be run.
upstream_error A research source failed. A rerun may help.
upstream_balance A research source refused the request.
model_error The writing model failed.
validation_failed The draft could not be made to match the evidence.
timeout The run took too long. A rerun may help.
budget_exhausted The run reached its research or model spending cap.

A few other codes, such as internal and stale, mean the service itself failed the run.

Errors

Every error has the same shape:

{ "error": { "code": "not_found", "message": "Run not found" } }
Status Code Meaning
400 invalid_request The request is invalid. When the body or query fails validation, the error also carries details, a list of the problems. Other invalid requests, such as a body that is not JSON, have no details.
401 invalid_key The API key is invalid or revoked.
401 unauthenticated A session endpoint was called without signing in.
401 no_workspace The session has no workspace.
403 no_workspace On the MCP server: you are no longer a member of the workspace the client was connected to.
varies auth_error Sign-in or OAuth refused the request. The status comes from the sign-in service.
402 insufficient_balance The balance does not cover the price.
404 not_found The run, campaign, key or client does not exist in your workspace.
409 idempotency_conflict The x-external-request-id was already used with a different body.
409 not_rerunnable The run is still in progress.
409 campaign_deleted The campaign was deleted.
409 key_revoked The API key is revoked and cannot be renamed.
429 rate_limited A rate limit was reached; the body carries retry_after_s and the response a Retry-After header. See Rate limits.
502 upstream_error A website could not be read during a refinement.
503 rate_limit_unavailable The rate limiter cannot be reached right now. Try again shortly.
503 billing_unavailable Billing is unavailable. Try again shortly.
503 balance_unavailable The balance cannot be read right now.
503 unavailable Refining from a website is unavailable.
500 internal Something failed on our side.

The 402 body has extra fields at the top level:

{
  "error": { "code": "insufficient_balance", "message": "Add money to run personalizations" },
  "balance_usd": 0.2,
  "price_usd": 0.5,
  "top_up_url": "https://coldemailpersonalizer.com/billing"
}

balance_usd is your available balance, price_usd is the price of the operation, and top_up_url is the Billing page. Add funds there before you retry.

MCP

The MCP server lets an assistant such as Claude Code call the service directly. It is a streamable HTTP server. Its URL is on the MCP page of the dashboard.

Install

Claude Code:

claude mcp add --transport http cold-email-personalizer <server url from the MCP page>
  1. Run this command in your terminal.
  2. In Claude Code, run /mcp, choose cold-email-personalizer, then Authenticate.
  3. Your browser opens. Sign in and allow access.

Claude Desktop:

  1. Open Settings, then Connectors, and choose Add custom connector.
  2. Name it Cold Email Personalizer and paste the server URL from the MCP page.
  3. Choose Connect. Your browser opens to sign in and allow access.

Cursor, in .cursor/mcp.json in your project or in the global MCP settings:

{
  "mcpServers": {
    "cold-email-personalizer": {
      "url": "<server url from the MCP page>"
    }
  }
}

Then open Cursor Settings, then MCP, and choose Connect next to cold-email-personalizer. Your browser opens to sign in and allow access.

Other clients: if your client supports OAuth, add the server URL and connect. Headless clients that cannot open a browser send an API key in the Authorization header instead:

{
  "mcpServers": {
    "cold-email-personalizer": {
      "url": "<server url from the MCP page>",
      "headers": { "Authorization": "Bearer $API_KEY" }
    }
  }
}

Tools

Tool Scope What it does Charged
personalize runs:write Researches a lead and writes an email or opening lines. Only on outcome ok
get_run runs:read Gets one run with its output, evidence and claims. No
list_runs runs:read Lists runs, newest first, with filters and paging. No
list_campaigns campaigns:read Lists saved campaigns. No
get_campaign campaigns:read Gets one saved campaign. No
refine_offer runs:write Reads a public website and drafts offer fields and questions. Only on a usable result

The inputs match the REST bodies and queries. personalize also takes wait, which defaults to true, and external_request_id, which works like the x-external-request-id header. get_run takes run_id, get_campaign takes campaign_id, and refine_offer takes url.

A tool error comes back as { code, message }. The codes are insufficient_scope when the connection was not granted the tool's scope, insufficient_balance with a top_up_url, not_found, internal, and the REST codes.

Scopes

When you connect a client, you allow it these scopes for your active workspace:

Scope Allows
runs:write Creating personalizations. Each run is charged from your balance.
runs:read Reading runs, outputs and evidence.
campaigns:read Reading your saved campaigns.

Clients may also ask for offline_access, which lets them refresh their access without signing in again. An access token lasts 900 seconds. An API key grants every scope.

Connected clients and revoking

Revoke a client from the MCP page of the dashboard. Its access stops on its next request, and it can no longer refresh. A revoked client is refused with HTTP 401 and the code revoked.

Method Path Auth Response
GET /v1/mcp/clients Session { clients }, each with id, name, scopes, status (connected or revoked), connected_at and last_used_at
DELETE /v1/mcp/clients/:clientId Session { "status": "revoked" }, or 404 with not_found
GET /v1/mcp/install Session { server_url, snippets } with the install steps above

Paste into your assistant

Add this to your assistant's preferences:

When I give you a lead and an offer, use the cold-email-personalizer tools to write the first email. Show me the email and the evidence behind each claim. Never send it, never make up details about my offer, and ask me before running more than a few leads because each run costs money.

Limits

Limit Value
Lead and offer text fields 2,000 characters each
Offer list fields 10 items of at most 500 characters each
example_email 4,000 characters
Offer language 2 characters
Campaign name 80 characters
Campaign proof_points and customer_outcomes 2,000 characters in total, and 10 lines of at most 500 characters each
Campaign call_to_action 484 characters
x-external-request-id and external_request_id 255 characters
Refine url 2,048 characters
GET /v1/runs page_size 100
GET /v1/runs search q 200 characters
API key label 60 characters
OAuth access token 900 seconds

Rate limits

Requests are limited per IP address, per workspace and per account. Defaults; your deployment may differ:

Limit Default
All requests per IP 300 per minute
Read requests per IP 120 per minute
Sign-in attempts per IP 10 per minute
Sign-in attempts per email 5 per minute
Account creations per IP 3 per minute
Run submissions per workspace 30 per minute
Runs in progress per workspace 10
Website refinements per workspace 10 per day (UTC)
Changes per workspace 60 per minute
Read requests per workspace 600 per minute

A refused request answers 429 with a Retry-After header (seconds) and this body:

{
  "error": { "code": "rate_limited", "message": "Run submission limit reached (30 per minute). Try again in 12 seconds.", "retry_after_s": 12 },
  "message": "Run submission limit reached (30 per minute). Try again in 12 seconds."
}

Limits use fixed one-minute windows, so a burst can straddle a minute boundary and briefly reach about twice the limit. MCP tools return the same message together with retry_after_s. A refused request still counts toward its window.

Support

Email [email protected] for help.

Include the run_id and the error.code when you report a problem with a run.