Email and text campaigns
Email or text customers who've said yes to hearing from you. Nothing sends without your OK.
Overview
Your past customers already know you. A well-timed email or text, like a service reminder or open slots this week, brings them back without paying for a new click. Your assistant can send those for you, and it checks with you before anything goes out.
First it looks at what's worked before and who's agreed to hear from you. It also pulls your email style: your logo, your colors, and the address your emails come from. Then it writes the email or text and shows you how it'll look. It can send a test to your own email and phone.
Before anything reaches customers, it tells you exactly how many people will get it. You confirm that number to send or schedule. If the list changes after you look, it stops and asks again. Texts only go out during local sending hours. There's also a limit on how often one customer gets a promo text.
Sending to customers turns on once your plan's billing has started. Drafts, previews, and tests work before that.
The tools on this page
| Tool | Reads | Writes | Asks you first |
|---|---|---|---|
| See your campaigns | Yes | No | No |
| Your email style | Yes | No | No |
| Read a past email | Yes | No | No |
| Draft a campaign | No | Yes | Yes |
| Preview an email | Yes | No | No |
| Send a test | No | Yes | Yes |
| See who gets it | Yes | No | No |
| Send a campaign | No | Yes | Always |
| Schedule or cancel | No | Yes | Yes |
See your campaigns
get_campaign_contextReads only
What you can ask
- How did last month's email do?
- Who can I text right now?
- What address do my emails come from?
What it does
Your recent campaigns and how they did, who you can email or text, and the address your emails come from.
What your assistant sends
| Field | Required | Description |
|---|---|---|
limitinteger | No | Recent campaigns to include, default 10. |
{
"limit": 5
}{
"business": "Example Service Group",
"timezone": "America/New_York",
"channels": {
"email": {
"available": true
},
"sms": {
"available": true,
"sending_numbers": [
{
"id": "77777777-7777-4777-8777-777777777777",
"number": "+19195550142",
"default": true,
"status": "active",
"campaign": "Summer tune-ups"
}
],
"send_window": "Weekdays 9:00 to 17:00, local time",
"promotional_cap": {
"used": 1,
"cap": 4,
"window_days": 30
}
}
},
"from": {
"name": "Example Service Group",
"email": "info@mail.esg.example",
"reply_to": "hello@esg.example"
},
"consent": {
"contacts": 1280,
"email_consented": 1034,
"sms_promotional_consented": 412,
"opted_out": {
"sms": 9,
"email": 21
}
},
"segments": [
{
"id": "55555555-5555-4555-8555-555555555555",
"name": "Maintenance members",
"type": "static",
"contacts": 318
}
],
"data": {
"recent_campaigns": [
{
"id": "44444444-4444-4444-8444-444444444444",
"name": "Summer tune-ups",
"status": "sent",
"channels": [
"email"
],
"scheduled_for": null,
"sent_at": "2026-09-01T14:00:00.000Z",
"stats": {
"total_sent": 409,
"total_failed": 3,
"email_sent": 409,
"email_delivered": 401
},
"results": {
"email": {
"sent": "409",
"delivered": "401",
"opened": "n/a",
"clicked": "n/a",
"replied": "n/a",
"opt_outs": "n/a"
},
"caveats": []
}
}
],
"results_caveats": [
"Open and click tracking is off to protect deliverability, so opened and clicked show n/a."
]
}
}Your email style
get_email_brand_kitReads only
What you can ask
- What should my emails look like?
- Use my logo and colors in this email.
What it does
How your emails should look: your colors, your logo, the address they come from, and the layout to start from.
What your assistant sends
Nothing. Your assistant just runs it.
{}{
"business": {
"name": "Example Service Group",
"address": "12 Main St, Raleigh, NC 27601",
"website": "https://esg.example"
},
"from": {
"name": "Example Service Group",
"email": "info@mail.esg.example",
"reply_to": "hello@esg.example"
},
"phone": {
"display": {
"variable": "{{company_phone}}",
"resolves_to": "(919) 555-0142"
},
"tel_links": {
"variable": "{{company_phone_tracking}}",
"resolves_to": "(919) 555-9999"
},
"rule": "Show the number with the display variable. Every tel: href must use the tracking variable, or the call is not credited to this email. A number typed into the HTML is refused."
},
"greeting": {
"variable": "{{first_name}}",
"rule": "Put the greeting in its own paragraph, for example <p>Hi {{first_name}},</p>."
},
"logo": {
"light_background": {
"variable": "{{company_logo_light}}",
"resolves_to": "https://cdn.esg.example/logo.png"
},
"dark_background": {
"variable": "{{company_logo_dark}}",
"resolves_to": null
}
},
"theme": {
"brand": "#0b5d3b",
"brandInk": "#e8c547",
"accent": "#f59e0b",
"accentInk": "#111827",
"ink": "#111827",
"ink70": "#585d68",
"inkFaint": "#94979e",
"surface": "#ffffff",
"surfaceAlt": "#f5f6f6",
"hairline": "#e2e3e5",
"cta": "#f59e0b",
"ctaInk": "#111827",
"kickerColor": "#f59e0b",
"danger": "#ef4444",
"warning": "#f59e0b",
"radius": 8,
"radiusControl": 8,
"radiusCard": 8,
"elevation": "hairline",
"headingWeight": 700,
"headingTracking": "normal",
"radiusCss": "8px",
"radiusControlCss": "8px",
"radiusCardCss": "8px",
"arcsize": "32%",
"headingFontStack": "'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif",
"bodyFontStack": "'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif",
"webFontImport": "<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">",
"rules": "Brand, accent, and kicker colors are TEXT colors, never backgrounds."
},
"design": {
"language": "ledger",
"rules": [
"One continuous bordered card; every section is a row inside it.",
"Keep the version marker <!-- tmg-email:ledger-v1 --> exactly once, right after <body>."
],
"scaffold": "<!DOCTYPE html><html><body><!-- tmg-email:ledger-v1 --><table role=\"presentation\">...</table></body></html>"
},
"call_to_action": {
"url": "https://esg.example/contact",
"alternatives": {
"website": "https://esg.example",
"call": "tel:{{company_phone_tracking}}"
}
},
"images": {
"rule": "Only your own library. Call list_media, then use media://<media_id> as the img src."
},
"unsubscribe": {
"rule": "Do not write one. The required footer is appended to every marketing email at send time."
},
"next_step": "Draft from the scaffold, then preview_email."
}Read a past email
get_campaign_emailReads only
What you can ask
- Show me the email we sent in July.
- Write this one in the same tone as our last newsletter.
What it does
One past email, word for word, so a new one can match it.
What it needs
- The past campaign you want to see.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | Yes | A campaign from get_campaign_context. Prefer status sent. Tone and content reference only; layout comes from the get_email_brand_kit scaffold. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444"
}{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"status": "sent",
"sent_at": "2026-09-01T14:00:00.000Z",
"data": {
"name": "Summer tune-ups",
"email_subject": "{{first_name}}, your summer tune-up is due",
"email_html": "<html><body><p>Hi {{first_name}},</p><p>Call {{company_phone}}.</p></body></html>"
},
"note_on_style": "This campaign predates the current design language. Use it for tone and content only; the layout to follow is the scaffold in get_email_brand_kit."
}Draft a campaign
draft_campaignChanges your site or sends something
What you can ask
- Draft an email about our fall tune-up special.
- Write a text to our maintenance members about open slots this week.
What it does
Draft an email, a text, or both. Never sends or schedules.
Your assistant asks you to confirm this first.
What it needs
- A name for the campaign, just for you.
- Email, text, or both.
What it won't do
It only drafts. Nothing goes out until you send it or schedule it. A text needs one of your sending numbers, and your assistant can look those up.
What your assistant sends
| Field | Required | Description |
|---|---|---|
namestring | Yes | Internal campaign name. |
channelsarray of one of: email, sms | Yes | Channels for this campaign. |
email_subjectstring | No | Required when channels include email. |
email_htmlstring | No | Required when channels include email. |
sms_messagestring | No | Required when channels include sms. A STOP line is appended for you. |
segment_idstring | No | A segment from get_campaign_context. Omitted means every contact. |
tracking_phone_number_idstring | No | The sending number for SMS. Required before an SMS campaign can send. |
scheduled_forstring | No | ISO timestamp. SMS sends only inside the local weekday 9:00-17:00 window. |
owner_requeststring | No | What the owner asked for, in their words, one sentence. Optional. Stored with the audit record so their team can see what owners ask for. |
{
"name": "Fall tune-up",
"channels": [
"email"
],
"email_subject": "Fall tune-up time",
"email_html": "<p>Book your tune-up.</p>"
}{
"campaign": {
"id": "44444444-4444-4444-8444-444444444444",
"name": "Fall tune-up",
"status": "draft",
"channels": [
"email"
],
"scheduled_for": null,
"next_step": "preview_email, then preview_recipients"
}
}Preview an email
preview_emailReads only
What you can ask
- Show me how this email looks before it goes out.
- Send me a link so I can check the draft on my phone.
What it does
Show an email the way it will arrive. Saves nothing.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | No | Preview a saved draft. The rendered result is what send_campaign will dispatch. |
email_subjectstring | No | With email_html, previews unsaved copy. Saves nothing. |
email_htmlstring | No | With email_subject, previews unsaved copy. Saves nothing. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444"
}{
"mode": "draft",
"campaign_id": "44444444-4444-4444-8444-444444444444",
"data": {
"subject": "Sarah, fall tune-up time",
"html": "<!DOCTYPE html><html><body>...</body></html>"
},
"warnings": [
{
"code": "no_logo_variable",
"message": "The email has no logo. Use {{company_logo_light}} in the header."
}
],
"preview_url": "https://app.example.com/preview/email/<token>",
"expires_in_minutes": 30,
"note": "This is the stored draft rendered by the send path, so it is what your contacts will receive. Names and the unsubscribe link are samples."
}Send a test
send_campaign_testChanges your site or sends something
What you can ask
- Send me a test of that email.
- Text the draft to my phone first.
What it does
Send a test of a draft to your own email and phone only.
Your assistant asks you to confirm this first.
What it needs
- The draft you want to test.
What it won't do
Tests only go to your own email and phone. It can't send a test to anyone else, so a test can't turn into a real send.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | Yes | The draft to test. |
channelsarray of one of: email, sms | No | Defaults to every channel on the campaign. |
owner_requeststring | No | What the owner asked for, in their words, one sentence. Optional. Stored with the audit record so their team can see what owners ask for. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444"
}{
"sent": [
"email"
],
"skipped": [
{
"channel": "sms",
"reason": "The campaign has no SMS text yet."
}
],
"sent_to": "your own address and number only"
}See who gets it
preview_recipientsReads only
What you can ask
- How many people will this email reach?
- Show me exactly what the text will say before it goes out.
What it does
How many people a draft will reach, by email and by text, and exactly what the text says.
What it needs
- The draft you want to check.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | Yes | The draft to preview. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444"
}{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"channels": [
"email"
],
"counts": {
"reachable": 412,
"email": 412,
"sms": 0,
"both": 0,
"unreachable": 37
},
"opted_out": {
"email": 21,
"sms": 0
},
"from": {
"name": "Example Service Group",
"email": "info@mail.esg.example"
},
"sms_from": null,
"data": {
"sample": [
{
"first_name": "Dana",
"channel": "email"
},
{
"first_name": null,
"channel": "email"
}
],
"sms_preview": null
},
"confirm_with_recipient_count": 412
}Send a campaign
send_campaignChanges your site or sends something
What you can ask
- Send the tune-up email now.
- Go ahead and send it to everyone on the list.
What it does
Send a draft to everyone it reaches, now.
This asks you first, every time.
What it needs
- The draft you want to send.
- The number of people you saw in the preview.
What it won't do
It won't send if the number of people changed since you looked. Texts only go out during local sending hours. There's a limit on how often one customer gets a promo text.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | Yes | The draft to send. |
confirmobject | Yes | Required. Call preview_recipients first and pass the count you saw. |
idempotency_keystring | No | Optional. Reusing a key within 24 hours returns the first send's result instead of sending again. |
owner_requeststring | No | What the owner asked for, in their words, one sentence. Optional. Stored with the audit record so their team can see what owners ask for. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"confirm": {
"recipient_count": 412,
"intent": "Send the fall tune-up offer to everyone"
},
"idempotency_key": "fall-tuneup-0930"
}{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"sent": 409,
"failed": 3,
"email": {
"sent": 409,
"failed": 3
},
"sms": {
"sent": 0,
"failed": 0
}
}Schedule or cancel
manage_campaignChanges your site or sends something
What you can ask
- Schedule the tune-up email for Tuesday at 9am.
- Cancel the campaign that's going out Friday.
- Archive the old spring promo.
What it does
Schedule, reschedule, cancel, or archive a campaign.
Your assistant asks you to confirm this first.
What it needs
- The campaign.
- What to do with it: schedule, reschedule, cancel, or archive.
What it won't do
Scheduling asks you to confirm how many people get it, same as sending now. A campaign that's in the middle of sending can't be changed. Cancelling a scheduled one puts it back in your drafts.
What your assistant sends
| Field | Required | Description |
|---|---|---|
campaign_idstring | Yes | |
actionone of: schedule, reschedule, cancel, archive | Yes | cancel returns a scheduled campaign to draft. archive retires a draft. |
scheduled_forstring | No | Required for schedule and reschedule. |
confirmobject | No | Required for schedule and reschedule: pass the count from preview_recipients. The send is held if the list grows past it. |
owner_requeststring | No | What the owner asked for, in their words, one sentence. Optional. Stored with the audit record so their team can see what owners ask for. |
{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"action": "schedule",
"scheduled_for": "2026-10-06T14:00:00.000Z",
"confirm": {
"recipient_count": 412,
"intent": "Send Monday morning"
}
}{
"campaign_id": "44444444-4444-4444-8444-444444444444",
"status": "scheduled",
"scheduled_for": "2026-10-06T14:00:00.000Z",
"recipient_count": 412
}