Klaviyo Send Email
Manage Klaviyo campaigns and campaign audiences through the Klaviyo REST API.
Core Model
Klaviyo exposes a JSON:API for campaign management. This skill covers five resource types:
- Campaigns (
campaign.sh): list, get, create, update, delete, clone, send, send-status, cancel, assign-template, list-messages, get-message, update-message, refresh-estimation, get-estimation, get-estimation-job - Campaign Audiences (
audience.sh): get, create, update - Lists (
lists.sh): list, get, create, update, delete, get-profiles, get-profile-ids, add-profiles, remove-profiles - Profiles (
profiles.sh): search, list, get, create, update, upsert, get-lists, get-segments - Segments (read via
segmentsendpoints): segments have their own API, but the scripts above can reference segment IDs in audiences
Audience endpoints are currently in beta and require the 2026-04-15.pre revision header. All campaign endpoints use the stable 2026-04-15 revision.
Never use a SendGrid API key, Instantly API key, or FREVANA_TOKEN with these scripts.
Scripts
Use exactly these bundled scripts:
scripts/campaign.sh:list,get,create,update,delete,clone,send,send-status,cancel,assign-template,list-messages,get-message,update-message,refresh-estimation,get-estimation,get-estimation-jobscripts/audience.sh:get,create,updatescripts/lists.sh:list,get,create,update,delete,get-profiles,get-profile-ids,add-profiles,remove-profilesscripts/profiles.sh:search,list,get,create,update,upsert,get-lists,get-segments
Read actions call the API immediately. Write actions dry-run by default and require --send.
API key resolution order for all scripts:
--api-keyKLAVIVO_API_KEY- locally saved key at
~/.config/klaviyo-send-email/api_key
Use --api-key <key> --save-api-key, --configure-api-key, or --clear-api-key with any script to manage the saved key.
If no API key is available, point the user to this guide to create one: https://frevana.gitbook.io/frevana-docs/email-integrations/klaviyo-integration.
Workflow A: List and Inspect Campaigns
- List campaigns by channel:
bash <skill-path>/scripts/campaign.sh list \
--filter 'equals(messages.channel,"email")'
This calls GET /api/campaigns?filter=equals(messages.channel,"email"). Present campaign name, ID, status, created_at, and scheduled_at.
- Get a specific campaign by ID:
bash <skill-path>/scripts/campaign.sh get \
--campaign-id "campaign_id_here"
This calls GET /api/campaigns/{id}. Present the campaign attributes, status, and related messages.
Workflow B: Create a Campaign
Collect required fields: campaign name, audience groups (included/excluded segment IDs), and message content (subject, from_email, from_label).
Dry-run the campaign create:
bash <skill-path>/scripts/campaign.sh create \
--name "Spring Sale 2026" \
--audience-json '{"included":["Y6nRLr"],"excluded":[]}' \
--message-json '{"subject":"Spring Sale is here!","from_email":"store@my-company.com","from_label":"My Company","preview_text":"Check out our spring deals"}'
- Send only after explicit approval:
bash <skill-path>/scripts/campaign.sh create \
--name "Spring Sale 2026" \
--audience-json '{"included":["Y6nRLr"],"excluded":[]}' \
--message-json '{"subject":"Spring Sale is here!","from_email":"store@my-company.com","from_label":"My Company","preview_text":"Check out our spring deals"}' \
--send
This calls POST /api/campaigns. The response includes the created campaign with its ID.
Workflow C: Update a Campaign
Update campaign name and/or audience:
bash <skill-path>/scripts/campaign.sh update \
--campaign-id "campaign_id" \
--name "Updated Campaign Name" \
--audience-json '{"included":["Y6nRLr"],"excluded":["UTd5ui"]}'
Send with --send to execute PATCH /api/campaigns/{id}.
Workflow D: Manage Lists
List all lists
bash <skill-path>/scripts/lists.sh list
Calls GET /api/lists. Optional --filter, --sort.
Create a list
bash <skill-path>/scripts/lists.sh create \
--name "My New List"
Send with --send to execute POST /api/lists.
Get a list
bash <skill-path>/scripts/lists.sh get \
--list-id "list_id"
Calls GET /api/lists/{id}.
Update a list name
bash <skill-path>/scripts/lists.sh update \
--list-id "list_id" \
--name "Updated Name"
Send with --send to execute PATCH /api/lists/{id}.
Delete a list
bash <skill-path>/scripts/lists.sh delete \
--list-id "list_id"
Send with --send to execute DELETE /api/lists/{id}.
Get profiles for a list
bash <skill-path>/scripts/lists.sh get-profiles \
--list-id "list_id"
Calls GET /api/lists/{id}/profiles. Optional --limit.
Get profile IDs for a list
bash <skill-path>/scripts/lists.sh get-profile-ids \
--list-id "list_id"
Calls GET /api/lists/{id}/relationships/profiles.
Add profiles to a list
bash <skill-path>/scripts/lists.sh add-profiles \
--list-id "list_id" \
--profile-ids '["profile_id_1","profile_id_2"]'
Send with --send to execute POST /api/lists/{id}/relationships/profiles.
Remove profiles from a list
bash <skill-path>/scripts/lists.sh remove-profiles \
--list-id "list_id" \
--profile-ids '["profile_id_1"]'
Send with --send to execute DELETE /api/lists/{id}/relationships/profiles.
Workflow E: Manage Profiles
Search for a profile by email
bash <skill-path>/scripts/profiles.sh search \
--email "user@example.com"
Calls GET /api/profiles?filter=equals(email,"..."). Returns the profile if found.
List all profiles
bash <skill-path>/scripts/profiles.sh list
Calls GET /api/profiles. Optional --filter, --sort, --limit.
Get a profile by ID
bash <skill-path>/scripts/profiles.sh get \
--profile-id "profile_id"
Calls GET /api/profiles/{id}.
Create a profile
bash <skill-path>/scripts/profiles.sh create \
--profile-json '{"data":{"type":"profile","attributes":{"email":"user@example.com","first_name":"John"}}}'
Send with --send to execute POST /api/profiles.
Update a profile
bash <skill-path>/scripts/profiles.sh update \
--profile-id "profile_id" \
--profile-json '{"data":{"type":"profile","id":"profile_id","attributes":{"first_name":"Jane"}}}'
Send with --send to execute PATCH /api/profiles/{id}.
Upsert (create or update) a profile
Uses the idempotent profile-import endpoint. If a profile with the given email exists, it is updated; otherwise a new profile is created.
bash <skill-path>/scripts/profiles.sh upsert \
--profile-json '{"data":{"type":"profile","attributes":{"email":"user@example.com","first_name":"John"}}}'
Send with --send to execute POST /api/profile-import.
Get lists for a profile
bash <skill-path>/scripts/profiles.sh get-lists \
--profile-id "profile_id"
Calls GET /api/profiles/{id}/lists.
Get segments for a profile
bash <skill-path>/scripts/profiles.sh get-segments \
--profile-id "profile_id"
Calls GET /api/profiles/{id}/segments.
Workflow G: Manage Campaign Audiences
Get an audience
bash <skill-path>/scripts/audience.sh get \
--audience-id "audience_id"
Calls GET /api/campaign-audiences/{id}.
Create an audience
bash <skill-path>/scripts/audience.sh create \
--campaign-id "campaign_id" \
--definition-json '{"name":"My Audience","included":["abc123"],"excluded":[],"priority":1}'
Send with --send to execute POST /api/campaign-audiences.
Update an audience
bash <skill-path>/scripts/audience.sh update \
--audience-id "audience_id" \
--definition-json '{"name":"Updated Audience","included":["abc123","def456"],"excluded":[],"priority":2}'
Send with --send to execute PATCH /api/campaign-audiences/{id}.
Workflow H: Delete a Campaign
bash <skill-path>/scripts/campaign.sh delete \
--campaign-id "campaign_id"
Send with --send to execute DELETE /api/campaigns/{id}. Returns 204 on success.
Workflow I: Clone a Campaign
Clone an existing campaign (sets status to draft, keeps original name):
bash <skill-path>/scripts/campaign.sh clone \
--campaign-id "campaign_id"
Send with --send to execute POST /api/campaign-clone.
Workflow J: Send / Schedule a Campaign
Important: Before sending, the campaign message must have a template assigned (see Workflow K).
0. Let user choose an existing campaign
First list all email campaigns and ask the user which one to send:
bash <skill-path>/scripts/campaign.sh list \
--filter 'equals(messages.channel,"email")'
Present as a table: name, ID, status, created_at. Ask the user to pick one or create new.
- If user picks a Sent/Cancelled campaign → clone it first to get a draft, then use the clone.
- If user picks a Draft campaign → use directly.
0b. Show current content & ask if modify
After user picks a campaign, list its messages to show the current content (subject, from_email, from_label, preview_text, template_id). Present as a table and ask:
"This is the current content. Do you want to modify it?"
If yes → let user provide new content and call update-message --send.
If no → keep as-is.
If no template assigned yet → proceed to Workflow K to assign one.
1. Send the campaign
bash <skill-path>/scripts/campaign.sh send \
--campaign-id "campaign_id"
Send with --send to execute POST /api/campaign-send-jobs. Returns a campaign send job ID.
Check send status
bash <skill-path>/scripts/campaign.sh send-status \
--send-job-id "send_job_id"
Calls GET /api/campaign-send-jobs/{id}. Job statuses: queued, processing, cancelled, complete.
Cancel a scheduled send
bash <skill-path>/scripts/campaign.sh cancel \
--send-job-id "send_job_id"
Send with --send to execute PATCH /api/campaign-send-jobs/{id} with status cancelled.
Workflow K: Assign Template to Campaign Message
After creating a campaign, you must assign a template to its message before sending:
- Create a template via the Klaviyo Templates API.
- Get the campaign message ID from
list-messages. - Assign the template:
bash <skill-path>/scripts/campaign.sh assign-template \
--message-id "message_id" \
--template-id "template_id"
Send with --send to execute POST /api/campaign-message-assign-template. This clones the reusable template and assigns the non-reusable clone to the message.
Workflow L: Manage Campaign Messages
List messages for a campaign
bash <skill-path>/scripts/campaign.sh list-messages \
--campaign-id "campaign_id"
Calls GET /api/campaigns/{id}/campaign-messages.
Get a single message
bash <skill-path>/scripts/campaign.sh get-message \
--message-id "message_id"
Calls GET /api/campaign-messages/{id}.
Update a message
bash <skill-path>/scripts/campaign.sh update-message \
--message-id "message_id" \
--message-json '{"definition":{"label":"Updated","content":{"subject":"New Subject","from_email":"new@example.com","from_label":"Sender"}}}'
Send with --send to execute PATCH /api/campaign-messages/{id}.
Workflow M: Estimate Campaign Recipients
Refresh estimation
bash <skill-path>/scripts/campaign.sh refresh-estimation \
--campaign-id "campaign_id"
Send with --send to execute POST /api/campaign-recipient-estimation-jobs. Triggers an async job.
Check estimation job status
bash <skill-path>/scripts/campaign.sh get-estimation-job \
--estimation-job-id "estimation_job_id"
Calls GET /api/campaign-recipient-estimation-jobs/{id}. Statuses: queued, processing, cancelled, complete.
Get estimated recipient count
bash <skill-path>/scripts/campaign.sh get-estimation \
--campaign-id "campaign_id"
Calls GET /api/campaign-recipient-estimations/{campaign_id}.
Script Capabilities
campaign.sh
list --filter FILTER [--limit N] [--sort SORT]— List campaigns by channel filter. Requires--filter(e.g.,equals(messages.channel,'email')). Optional--sort(e.g.,-created_at).get --campaign-id UUID— Get a single campaign with full attributes.create --name NAME --audience-json JSON --message-json JSON [--send]— Create a campaign. Requires name, audience groups, and email message definition.update --campaign-id UUID --name NAME [--audience-json JSON] [--send]— Update campaign name and optionally audience.delete --campaign-id UUID [--send]— Delete a campaign. Dry-run by default.clone --campaign-id UUID [--send]— Clone an existing campaign in draft status (keeps original name).send --campaign-id UUID [--send]— Trigger async campaign send viaPOST /api/campaign-send-jobs.send-status --send-job-id UUID— Check campaign send job status (queued/processing/cancelled/complete).cancel --send-job-id UUID [--send]— Cancel a campaign send job. Sets status to cancelled.assign-template --message-id UUID --template-id UUID [--send]— Clone a reusable template and assign it to a campaign message (POST /api/campaign-message-assign-template).list-messages --campaign-id UUID— List all messages for a campaign.get-message --message-id UUID— Get a single campaign message.update-message --message-id UUID --message-json JSON [--send]— Update a campaign message definition.refresh-estimation --campaign-id UUID [--send]— Trigger async recipient estimation job.get-estimation --campaign-id UUID— Get estimated recipient count for a campaign.get-estimation-job --estimation-job-id UUID— Check recipient estimation job status.
Message JSON fields for create:
- Required:
subject,from_email,from_label - Optional:
preview_text,reply_to_email,cc_email,bcc_email,label,send_options,tracking_options,send_strategy
audience.sh
get --audience-id UUID— Get audience details (beta).create --campaign-id UUID --definition-json JSON [--send]— Create an audience for a campaign (beta).update --audience-id UUID --definition-json JSON [--send]— Update an audience definition (beta).
Definition JSON fields:
name(string, nullable) — audience nameincluded(array of strings, nullable) — list of included group/segment IDsexcluded(array of strings, nullable) — list of excluded group/segment IDspriority(integer, nullable) — audience priority
lists.sh
list [--filter FILTER] [--limit N] [--sort SORT]— List all lists. Optional filter by name, sort by created/updated/name.get --list-id UUID— Get a single list with full attributes.create --name NAME [--send]— Create a new list. Dry-run by default.update --list-id UUID --name NAME [--send]— Update a list name.delete --list-id UUID [--send]— Delete a list. Returns 204 on success.get-profiles --list-id UUID [--limit N]— Get all profiles on a list.get-profile-ids --list-id UUID— Get profile relationship IDs for a list.add-profiles --list-id UUID --profile-ids JSON [--send]— Add profiles to a list.--profile-idsis a JSON array of profile ID strings.remove-profiles --list-id UUID --profile-ids JSON [--send]— Remove profiles from a list.
profiles.sh
search --email EMAIL— Search for a profile by email address (shorthand forlist --filter).list [--filter FILTER] [--limit N] [--sort SORT]— List all profiles. Optional filter by email/name/etc.get --profile-id UUID— Get a single profile with full attributes.create --profile-json JSON [--send]— Create a new profile. JSON must includedata.typeanddata.attributes.email.update --profile-id UUID --profile-json JSON [--send]— Update existing profile attributes.upsert --profile-json JSON [--send]— Create or update a profile idempotently viaPOST /api/profile-import(matches on email).get-lists --profile-id UUID— Get lists that a profile belongs to.get-segments --profile-id UUID— Get segments that a profile belongs to.
Notes
- Never invent campaign IDs, audience IDs, segment IDs, API keys, template IDs, or message IDs.
- Never print API keys. If the user shares a key in chat, advise rotation.
- Audience endpoints are in beta. Use
2026-04-15.prerevision header. GA expected in2026-07-15. - All campaign endpoints (including send, clone, delete, template assignment, messages, estimation) use the stable
2026-04-15revision. - The Klaviyo API uses
Klaviyo-API-Keyin theAuthorizationheader (not Bearer token). - Required API key scopes per script:
campaign.shandaudience.shneedcampaigns:read,campaigns:write;lists.shneedslists:read,lists:write;profiles.shneedsprofiles:read,profiles:write. - Campaign creation through the API does not immediately send. You must: create campaign → assign template → create campaign send job.
- If you cancel a campaign and want to resend it, clone the campaign first (creates a draft), then send again.
- To send an email campaign: (0) list campaigns and let user pick one, (1) if needed create campaign with message, (2)
list-messagesto get message ID, (3)assign-templateto add email template, (4)sendto schedule. - Always list existing campaigns first when the user asks to send an email — ask them to pick an existing campaign or create a new one. Never skip this step.
- For missing API key in non-interactive runs, tell the user to create a Klaviyo API key by following https://frevana.gitbook.io/frevana-docs/email-integrations/klaviyo-integration.
Complete Send Flow
Step 0: List existing campaigns and let user choose
Before creating a new campaign, always list existing campaigns and ask the user if they want to reuse one:
bash <skill-path>/scripts/campaign.sh list \
--filter 'equals(messages.channel,"email")'
Present the results as a numbered list with: name, ID, status, created_at. Ask:
"Here are the existing campaigns. Which one would you like to use, or create a new one?"
- If user wants a new one → proceed to Step 1.
- If user picks an existing campaign:
- If Sent/Cancelled → clone it first to get a draft, then use the cloned campaign's ID.
- If Draft → use directly.
Step 1 (existing campaign): Show current content & ask if modify
After the user picks a campaign (or clone), list its messages to show current content:
bash <skill-path>/scripts/campaign.sh list-messages \
--campaign-id "campaign_id"
Present the current message content in a clear table:
| Field | Value |
|---|---|
| Subject | subject |
| From Email | from_email |
| From Name | from_label |
| Preview | preview_text |
| Template ID | template.id |
Then ask the user:
"This is the current campaign content. Do you want to modify it?"
- If yes → let the user provide new subject/from_email/from_label/preview_text, then update with:
bash <skill-path>/scripts/campaign.sh update-message \ --message-id "message_id" \ --message-json '{"definition":{"label":"...","content":{"subject":"...","from_email":"...","from_label":"...","preview_text":"..."}}}' \ --send - If no → keep the existing content as-is.
If the message already has a template assigned (template.id is present), skip template assignment. If no template, proceed to Step 3 (assign-template).
Step 1 (new): Create the campaign (only if user chooses to create new)
bash <skill-path>/scripts/campaign.sh create \
--name "Campaign Name" \
--audience-json '{"included":["segment_id"],"excluded":[]}' \
--message-json '{"subject":"Subject","from_email":"from@example.com","from_label":"Sender"}' \
--send
Step 2: Get the campaign message ID (if creating new)
bash <skill-path>/scripts/campaign.sh list-messages \
--campaign-id "campaign_id"
Step 3: Assign a template to the message (if needed)
bash <skill-path>/scripts/campaign.sh assign-template \
--message-id "message_id" \
--template-id "template_id" \
--send
Step 4: (Optional) Estimate recipients
bash <skill-path>/scripts/campaign.sh refresh-estimation \
--campaign-id "campaign_id" --send
Step 5: Send the campaign
bash <skill-path>/scripts/campaign.sh send \
--campaign-id "campaign_id" --send
Step 6: Check send status
bash <skill-path>/scripts/campaign.sh send-status \
--send-job-id "send_job_id"
Example Prompts
- "Create an email campaign in Klaviyo"
- "List all my Klaviyo email campaigns"
- "Show me details for a campaign"
- "Update campaign audience settings"
- "Create a new audience for a campaign"
- "View campaign audience"
- "Delete a Klaviyo campaign"
- "Clone a Klaviyo campaign"
- "Send a campaign email" (first list existing campaigns and let user choose)
- "Use an existing campaign to send"
- "Assign an email template to a campaign"
- "Check campaign send status"
- "View estimated recipients for a campaign"
- "List all Klaviyo lists"
- "Create a new Klaviyo list"
- "Add profiles to a list"
- "Search for a Klaviyo profile"
- "Create or update a Klaviyo profile"
- "Upsert a Klaviyo profile"