Templates
Invitation notes and messages are templates. Variables come from the lead's LinkedIn profile, from the sender, and from any custom fields you or your agent attach. Preview renders them all before anything is sent, with the same parser that sends them.
Variables
| Variable | Value |
|---|---|
| {{firstName}} | The lead's first name. |
| {{lastName}} | The lead's last name. |
| {{fullName}} | First and last name. |
| {{company}} | Current company. |
| {{title}} | Current job title. |
| {{headline}} | LinkedIn headline. |
| {{location}} | Location as shown on the profile. |
| {{sender.firstName}} | First name of the sender assigned to this lead. |
| {{sender.fullName}} | Full name of that sender. |
| {{fields.<key>}} | A custom field on the enrollment or the lead. Keys are letters, digits and _, starting with a letter. |
Names are case-sensitive and whitespace inside the braces is ignored: {{ company }} works, {{firstname}} doesn't. A variable outside this list is a syntax error, so a campaign whose steps use one is refused when you create or update it (422 INVALID_CAMPAIGN_SEQUENCE_ERROR, with the step and the position in details.problems). Text outside {{ }} is copied as is.
Fallbacks
Add a fallback after a pipe, in double or single quotes (\" escapes a quote inside). It is used when the value is missing or blank.
Hi {{firstName | "there"}}, saw you're leading {{title | 'the team'}} at {{company | "your company"}}.
A variable without a fallback is required. If it has no value for a lead, preview reports missing_variable for that lead, the lead is skipped at launch and nothing is ever sent to them. Other leads are unaffected. Profile fields LinkedIn hasn't returned yet count as missing, so give fallbacks to anything optional.
Custom fields
Every lead and every enrollment carries free-form fields (up to 50 keys, values up to 2,000 characters). Templates read them as {{fields.<key>}}; enrollment fields override lead fields with the same key, in that campaign only. Updates merge: keys you send are set, a key set to "" is removed, the rest stay. For a CSV, send each row's profile URL to POST /v1/lists/{listId}/leads with the other columns as fields.
This is how an agent personalizes at scale: research each lead, write one line, store it as a field, and keep a single template for the campaign.
curl -X PATCH https://api.getoutvoy.com/v1/leads/lead_0vnrCpdo \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fields": { "icebreaker": "Your post on forecast hygiene was spot on." } }'
curl -X PATCH https://api.getoutvoy.com/v1/campaigns/cmp_pToocdWF/enrollments/enr_3OosbrwY \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fields": { "icebreaker": "Congrats on the Series B." } }'
Thanks for connecting, {{firstName}}. {{fields.icebreaker | "Happy to compare notes on RevOps."}}
An agent can also write a whole message for one lead: put the full text in a field (for example fields.message) and use {{fields.message}} as the step's text. POST /v1/campaigns/{campaignId}/leads takes fields per lead for exactly this.
Length limits
LinkedIn caps invitation notes at 300 characters and messages at 8,000. Outvoy checks the rendered text for every lead, after variables are filled in. A note that renders longer is reported as note_too_long, a message as message_too_long, and that lead is never sent the text. (The raw template may be longer, up to 1,000 characters for a note and 10,000 for a message, because variables can shrink it.)
Check the longest rendered note in preview, not the template. A short template with {{headline}} can easily exceed 300 characters for some people.
Preview issues
POST /v1/campaigns/{campaignId}/preview renders every step for every lead (its lists plus the leads added directly) and returns each lead's issues. Nothing is saved and nothing is sent. Any issue makes the lead not eligible: it is skipped at launch and recorded as a stopped enrollment with that reason.
| Lead issue | Meaning |
|---|---|
| missing_variable | A variable without a fallback has no value for this lead (variables lists them). |
| note_too_long | The rendered invitation note is over 300 characters. |
| message_too_long | A rendered message is over 8,000 characters. |
| blocklisted | The person or their company is on the workspace blocklist. |
| already_in_campaign | The lead is live in another running or paused campaign of the workspace. |
| already_enrolled | The lead already has an enrollment in this campaign. |
Campaign-level issues come in campaignIssues, each with blocking: true when it stops the launch: no_senders, sender_not_active, no_steps, no_leads (none, or none eligible), live_campaign_limit, and plan_limit when the monthly enrollment cap is used up. canLaunch is false while any blocking one remains. totals always cover every lead, while leads is paginated (?limit=, default 25, and meta.nextCursor).
{
"data": {
"campaignId": "cmp_E35my9by",
"canLaunch": false,
"campaignIssues": [
{ "code": "no_leads", "blocking": true, "message": "None of the 30 leads can be enrolled: see each lead's issues" }
],
"totals": {
"leads": 30,
"eligible": 0,
"blocked": 30,
"enrollableThisMonth": 0,
"issues": {
"missing_variable": 29,
"note_too_long": 1,
"message_too_long": 0,
"blocklisted": 1,
"already_in_campaign": 0,
"already_enrolled": 0
}
},
"leads": [
{
"lead": { "id": "lead_0vnrCpdo", "fullName": "Paul Murphy", "company": "Quanta Billing", ... },
"eligible": false,
"steps": [
{ "index": 0, "type": "invite", "text": "Hi Paul, Your post on forecast hygiene was spot on. xxxx…", "length": 315 },
{ "index": 1, "type": "message", "text": "Thanks, Paul.", "length": 13 }
],
"issues": [
{ "code": "note_too_long", "message": "Step 1 note renders to 315 characters; the limit is 300", "stepIndex": 0 }
]
},
{
"lead": { "id": "lead_3lgMrkaf", "fullName": "Sophia Wright", "company": "Atlas CRM", ... },
"eligible": false,
"steps": [ ... ],
"issues": [
{
"code": "missing_variable",
"message": "Step 1 note uses {{fields.icebreaker}} with no value for this lead and no fallback",
"stepIndex": 0,
"variables": ["fields.icebreaker"]
}
]
}
]
},
"meta": { "nextCursor": "Mw" }
}
An eligible lead carries the sender it would be assigned (round-robin across the pool); steps[].text is each note and message exactly as it would be sent, with its length.