curl --request POST \
--url https://api.example.com/api/v1/campaigns/{campaign_id}/personalize \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"step_index": 1,
"variant_label": "<string>",
"sender_profile_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel": "email"
}
'import requests
url = "https://api.example.com/api/v1/campaigns/{campaign_id}/personalize"
payload = {
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"step_index": 1,
"variant_label": "<string>",
"sender_profile_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel": "email"
}
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({
campaign_lead_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
step_index: 1,
variant_label: '<string>',
sender_profile_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
job_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
channel: 'email'
})
};
fetch('https://api.example.com/api/v1/campaigns/{campaign_id}/personalize', 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/campaigns/{campaign_id}/personalize",
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([
'campaign_lead_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'step_index' => 1,
'variant_label' => '<string>',
'sender_profile_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'job_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'channel' => 'email'
]),
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/campaigns/{campaign_id}/personalize"
payload := strings.NewReader("{\n \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\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/campaigns/{campaign_id}/personalize")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/campaigns/{campaign_id}/personalize")
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 \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\n}"
response = http.request(request)
puts response.read_body{
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"personalized_body": "<string>",
"credits_consumed": 123,
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"personalized_subject": "<string>",
"sender_email": "<string>",
"recipient_email": "<string>",
"all_claims_ungrounded": false
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Personalize Campaign Email
Generate personalized content for a campaign lead.
Email (the default channel) is called by the Worker Lambda (via internal
API key) or directly by authenticated users (via JWT). The LinkedIn AI
personalization channel is INTERNAL-AUTH ONLY (CB3) and returns a
body-only DM. The channel is resolved from the loaded STEP config (the
request channel is advisory only) so a JWT caller cannot bypass the
LinkedIn guard by omitting / spoofing channel.
curl --request POST \
--url https://api.example.com/api/v1/campaigns/{campaign_id}/personalize \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"step_index": 1,
"variant_label": "<string>",
"sender_profile_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel": "email"
}
'import requests
url = "https://api.example.com/api/v1/campaigns/{campaign_id}/personalize"
payload = {
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"step_index": 1,
"variant_label": "<string>",
"sender_profile_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel": "email"
}
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({
campaign_lead_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
step_index: 1,
variant_label: '<string>',
sender_profile_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
job_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
channel: 'email'
})
};
fetch('https://api.example.com/api/v1/campaigns/{campaign_id}/personalize', 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/campaigns/{campaign_id}/personalize",
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([
'campaign_lead_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'step_index' => 1,
'variant_label' => '<string>',
'sender_profile_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'job_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'channel' => 'email'
]),
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/campaigns/{campaign_id}/personalize"
payload := strings.NewReader("{\n \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\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/campaigns/{campaign_id}/personalize")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/campaigns/{campaign_id}/personalize")
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 \"campaign_lead_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"step_index\": 1,\n \"variant_label\": \"<string>\",\n \"sender_profile_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"job_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"channel\": \"email\"\n}"
response = http.request(request)
puts response.read_body{
"campaign_lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"personalized_body": "<string>",
"credits_consumed": 123,
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"personalized_subject": "<string>",
"sender_email": "<string>",
"recipient_email": "<string>",
"all_claims_ungrounded": false
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Path Parameters
Body
Request to personalize content for a campaign lead.
Email is the default channel (backward-compat). A LinkedIn AI
personalization request (epic 2) sets channel="linkedin"; for that
channel job_id is OPTIONAL (synthetic / None — used only for
metadata/logging, NEVER for settlement) and the response omits the
email-only subject/sender_email/recipient_email fields.
The campaign lead ID
The step index in the campaign
x >= 0The variant label for A/B testing
The sender profile ID to use
The outbound job ID
Channel: 'email' (default) or 'linkedin'
Response
Successful Response
Response containing personalized content.
Email responses carry the full email shape (subject + sender/recipient
email). LinkedIn responses OMIT those email-only fields (they are None)
and return only personalized_body + credits_consumed
(+ all_claims_ungrounded). The email fields are OPTIONAL so the
LinkedIn shape validates; an email response is byte-for-byte identical
to the pre-epic-2 shape because the worker always populates them.
The campaign lead ID
Personalized body
Credits consumed for personalization
The outbound job ID
Personalized email subject (email channel only)
Sender email from profile (email channel only)
Recipient email address (email channel only)
W3-D1: True when the message made factual claims and every one is ungrounded. The Lambda worker's pre-send quality gate treats this as a typed terminal no-send.