curl --request POST \
--url https://api.example.com/api/v1/personalize/preview \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"lead_data": {},
"sender_profile_id": "<string>",
"sender_data": {},
"custom_prompt": "<string>",
"research_context": "<string>",
"dataset_id": "<string>",
"row_index": 1,
"force_refresh_research": false
}
'import requests
url = "https://api.example.com/api/v1/personalize/preview"
payload = {
"lead_data": {},
"sender_profile_id": "<string>",
"sender_data": {},
"custom_prompt": "<string>",
"research_context": "<string>",
"dataset_id": "<string>",
"row_index": 1,
"force_refresh_research": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
lead_data: {},
sender_profile_id: '<string>',
sender_data: {},
custom_prompt: '<string>',
research_context: '<string>',
dataset_id: '<string>',
row_index: 1,
force_refresh_research: false
})
};
fetch('https://api.example.com/api/v1/personalize/preview', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/personalize/preview",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'lead_data' => [
],
'sender_profile_id' => '<string>',
'sender_data' => [
],
'custom_prompt' => '<string>',
'research_context' => '<string>',
'dataset_id' => '<string>',
'row_index' => 1,
'force_refresh_research' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/personalize/preview"
payload := strings.NewReader("{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/personalize/preview")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/personalize/preview")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}"
response = http.request(request)
puts response.read_body{
"subject": "<string>",
"email_body": "<string>",
"commonality": {
"text": "<string>",
"confidence_score": 123,
"rarity_score": 123,
"relevance_score": 123,
"supporting_evidence": [
"<string>"
]
},
"prompt_source": "<string>",
"model_used": "<string>",
"latency_ms": 123,
"research_sources": [
{
"name": "<string>",
"status": "<string>",
"summary": "",
"meta": {},
"from_cache": false
}
],
"research_chars": 0,
"research_latency_ms": 0,
"research_from_cache": false,
"research_cached_at": "<string>",
"research_meta": {},
"sources": [
{
"index": 0,
"tool_names": [
"<string>"
],
"url": "<string>",
"url_anchor": "<string>",
"accessed_at": "",
"excerpt": "",
"status": "success"
}
],
"claim_sentences": [
{
"sentence_text": "",
"source_indices": [
123
],
"body_offset_start": 123,
"body_offset_end": 123
}
],
"ungrounded_claim_count": 0,
"body_clean": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Sync preview of a personalized email for a single lead
Blocking endpoint. Runs find_commonalities + generate_email inline against the provided lead + sender profile. Returns subject, body, commonality, and the prompt source used. Does not create a campaign, does not queue a job.
curl --request POST \
--url https://api.example.com/api/v1/personalize/preview \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"lead_data": {},
"sender_profile_id": "<string>",
"sender_data": {},
"custom_prompt": "<string>",
"research_context": "<string>",
"dataset_id": "<string>",
"row_index": 1,
"force_refresh_research": false
}
'import requests
url = "https://api.example.com/api/v1/personalize/preview"
payload = {
"lead_data": {},
"sender_profile_id": "<string>",
"sender_data": {},
"custom_prompt": "<string>",
"research_context": "<string>",
"dataset_id": "<string>",
"row_index": 1,
"force_refresh_research": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
lead_data: {},
sender_profile_id: '<string>',
sender_data: {},
custom_prompt: '<string>',
research_context: '<string>',
dataset_id: '<string>',
row_index: 1,
force_refresh_research: false
})
};
fetch('https://api.example.com/api/v1/personalize/preview', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/personalize/preview",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'lead_data' => [
],
'sender_profile_id' => '<string>',
'sender_data' => [
],
'custom_prompt' => '<string>',
'research_context' => '<string>',
'dataset_id' => '<string>',
'row_index' => 1,
'force_refresh_research' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/personalize/preview"
payload := strings.NewReader("{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/personalize/preview")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/personalize/preview")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"lead_data\": {},\n \"sender_profile_id\": \"<string>\",\n \"sender_data\": {},\n \"custom_prompt\": \"<string>\",\n \"research_context\": \"<string>\",\n \"dataset_id\": \"<string>\",\n \"row_index\": 1,\n \"force_refresh_research\": false\n}"
response = http.request(request)
puts response.read_body{
"subject": "<string>",
"email_body": "<string>",
"commonality": {
"text": "<string>",
"confidence_score": 123,
"rarity_score": 123,
"relevance_score": 123,
"supporting_evidence": [
"<string>"
]
},
"prompt_source": "<string>",
"model_used": "<string>",
"latency_ms": 123,
"research_sources": [
{
"name": "<string>",
"status": "<string>",
"summary": "",
"meta": {},
"from_cache": false
}
],
"research_chars": 0,
"research_latency_ms": 0,
"research_from_cache": false,
"research_cached_at": "<string>",
"research_meta": {},
"sources": [
{
"index": 0,
"tool_names": [
"<string>"
],
"url": "<string>",
"url_anchor": "<string>",
"accessed_at": "",
"excerpt": "",
"status": "success"
}
],
"claim_sentences": [
{
"sentence_text": "",
"source_indices": [
123
],
"body_offset_start": 123,
"body_offset_end": 123
}
],
"ungrounded_claim_count": 0,
"body_clean": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Input for the sync personalization preview.
The lead fields to personalize against (company, role, linkedin_url, etc.)
ID of a saved sender profile. If omitted, sender_data must be provided.
Inline sender profile data. Ignored if sender_profile_id is provided.
Optional prompt override. Must be 100-5000 characters if provided.
Optional pre-fetched research context (overrides enrichment). Max 20000 chars.
20000Optional dataset id for research cache persistence. Paired with row_index.
Row index within the dataset. Required when dataset_id is set.
x >= 0Bypass the research cache and fetch fresh (e.g. user hit 'Refresh research').
Response
Successful Response
Show child attributes
Show child attributes
Which prompt was used: 'default', 'sender_profile_custom', or 'custom_override'.
Which enrichment sources ran and whether they succeeded.
Show child attributes
Show child attributes
Aggregate research metadata. Currently exposes zero_research_reason (one of the ZERO_RESEARCH_REASON_* enum values) when the joined research_chars was below the useful threshold. Used by ops sweep scripts to derive a per-cohort distribution of why some leads return empty research.
Per-URL personalization sources surfaced for the citations panel (plan AC-15). Empty when no research surface was available or when the preview path supplied research_context without a sources array.
Show child attributes
Show child attributes
LLM-emitted factual-claim sentences with their source_indices (1-indexed into sources) and UTF-16 code-unit offsets into body_clean (plan AC-16). Empty list when the LLM emitted no claims or when no sources were available to ground them.
Show child attributes
Show child attributes
Count of claim_sentences with empty source_indices (plan AC-10a). Used by the frontend to render the ungrounded warning banner.
x >= 0The LLM plain-text email body verbatim — the same string as email_body, surfaced separately so frontends can index claim_sentences[].body_offset_* against a stable reference even if email_body is later mutated by signature stitching elsewhere in the pipeline (plan AC-12). None on pre-feature responses.