Send a newsletter
You need a key with these permissions: newsletters:write to prepare, templates:submit to hand the template to WhatsApp, newsletters:test to test and newsletters:send to send. Read Sending safely first.
A newsletter is sent as a WhatsApp template, and WhatsApp must approve the template before anything is sent. So the order is: draft, approval, preview, test, send. All answers on this page belong to one story: the newsletter "Autumn menu".
#1. Create the draft
The text may contain the placeholder {{first_name}}. It is replaced by the first name of each contact. It is the only placeholder a newsletter allows. The text must not start or end with a placeholder.
curl -X POST "https://api.smartchat.marketing/v1/newsletters" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"title": "Autumn menu",
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week.",
"audience": {
"tags": [
"vip"
]
}
}'import os
import uuid
import requests
response = requests.post(
"https://api.smartchat.marketing/v1/newsletters",
headers={
"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"title": "Autumn menu",
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week.",
"audience": {
"tags": [
"vip",
],
},
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/newsletters", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "Autumn menu",
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week.",
"audience": {
"tags": [
"vip"
]
}
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (201):
{
"object": "newsletter",
"id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"title": "Autumn menu",
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week.",
"language": "en",
"button": {
"type": "url",
"text": "See the menu",
"url": "https://example.com/menu"
},
"audience": {
"tags": [
"vip"
],
"mode": "any",
"sources": []
},
"status": "draft",
"template_id": "tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
"template_status": "draft",
"ready_to_send": false,
"not_ready_reason": "template_not_approved",
"recipients": 412,
"scheduled_at": null,
"sent_at": null,
"created_at": "2026-10-25T09:41:07Z",
"updated_at": "2026-10-25T09:41:07Z"
}The newsletter is a draft. It has its own template (template_id) and the template is still a draft too, so ready_to_send is false and not_ready_reason is template_not_approved. The newsletter can be changed (update_newsletter) until it is scheduled.
#2. Get the template approved
WhatsApp decides if a template may be used. You hand it over with create_template_submissions. This needs the permission templates:submit, and it takes two calls with different Idempotency-Key values.
First call: a preview. Nothing is submitted. The answer shows what would happen and gives a plan_id that is valid for 10 minutes.
curl -X POST "https://api.smartchat.marketing/v1/template-submissions" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
]
}'import os
import uuid
import requests
response = requests.post(
"https://api.smartchat.marketing/v1/template-submissions",
headers={
"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
],
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/template-submissions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
]
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (200):
{
"object": "template_submission_plan",
"plan_id": "pln_9a8b7c6d5e4f43a2b1c0d9e8f7a6b5c4",
"expires_at": "2026-10-25T09:52:00Z",
"templates": [
{
"template_id": "tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
"name": "Autumn menu",
"status": "draft",
"outcome": "would_submit",
"issues": [],
"error_code": null
}
],
"submitted_at": null
}Second call: send the same template_ids with the plan_id. Now the template goes to WhatsApp.
curl -X POST "https://api.smartchat.marketing/v1/template-submissions" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
],
"plan_id": "pln_9a8b7c6d5e4f43a2b1c0d9e8f7a6b5c4"
}'import os
import uuid
import requests
response = requests.post(
"https://api.smartchat.marketing/v1/template-submissions",
headers={
"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
],
"plan_id": "pln_9a8b7c6d5e4f43a2b1c0d9e8f7a6b5c4",
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/template-submissions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"template_ids": [
"tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
],
"plan_id": "pln_9a8b7c6d5e4f43a2b1c0d9e8f7a6b5c4"
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (200):
{
"object": "template_submission",
"plan_id": null,
"expires_at": null,
"templates": [
{
"template_id": "tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
"name": "Autumn menu",
"status": "pending",
"outcome": "submitted",
"issues": [],
"error_code": null
}
],
"submitted_at": "2026-10-25T09:42:10Z"
}The template is pending. WhatsApp decides within a few minutes to 24 hours. Read the newsletter from time to time (get_newsletter) until template_status is approved and ready_to_send is true. If WhatsApp rejects the template, template_status is rejected; change the text and submit again.
curl "https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY"import os
import requests
response = requests.get(
"https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
headers={"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"]},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (200):
{
"object": "newsletter",
"id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"title": "Autumn menu",
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week.",
"language": "en",
"button": {
"type": "url",
"text": "See the menu",
"url": "https://example.com/menu"
},
"audience": {
"tags": [
"vip"
],
"mode": "any",
"sources": []
},
"status": "draft",
"template_id": "tpl_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
"template_status": "approved",
"ready_to_send": true,
"not_ready_reason": null,
"recipients": 412,
"scheduled_at": null,
"sent_at": null,
"created_at": "2026-10-25T09:41:07Z",
"updated_at": "2026-10-25T10:05:00Z"
}#3. Look at the preview
The preview reads and calculates. It sends nothing.
curl "https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send-preview?when=now" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY"import os
import requests
response = requests.get(
"https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send-preview",
headers={"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"]},
params={
"when": "now",
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send-preview?when=now", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (200):
{
"object": "newsletter_send_preview",
"newsletter_id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"when": "now",
"can_send": true,
"blocked_by": [],
"recipients": 412,
"audience_total": 938,
"excluded": {
"opted_out": 12,
"not_confirmed": 30,
"suppressed": 2,
"no_phone": 3,
"outside_audience": 524
},
"estimated_days": 1,
"daily_limit": 1000,
"text": "Hi {{first_name}}, our autumn menu is here. Book a table for this week."
}Check the number of recipients, the excluded contacts and the sample text. Only contacts with a confirmed opt-in are included. daily_limit is the daily limit of your WhatsApp number (how many new people WhatsApp lets it write to per day, or null if there is none), and estimated_days is how many days the send takes because of it. Here the 412 recipients fit into one day.
#4. Send a test
A test goes to the phone number you name. It uses approved templates only and never goes to a contact who unsubscribed. It needs an Idempotency-Key.
curl -X POST "https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/test-send" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"to": "+4915112345678"
}'import os
import uuid
import requests
response = requests.post(
"https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/test-send",
headers={
"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"to": "+4915112345678",
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/test-send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"to": "+4915112345678"
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (202):
{
"object": "newsletter_test_send",
"newsletter_id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"to": "+4915112345678",
"status": "handed_to_whatsapp"
}#5. Send
Sending is one step. It needs the permission newsletters:send and an Idempotency-Key. If the network fails, repeat the call with the same key: the newsletter is not sent twice.
curl -X POST "https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send" \
-H "Authorization: Bearer $SMARTCHAT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"when": "now"
}'import os
import uuid
import requests
response = requests.post(
"https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send",
headers={
"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"when": "now",
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://api.smartchat.marketing/v1/newsletters/nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"when": "now"
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Response (202):
{
"object": "newsletter_send",
"id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"newsletter_id": "nwl_3c9e1f7a5b2d4e8f9a0b1c2d3e4f5a6b",
"status": "sending",
"recipients": 412,
"scheduled_at": null
}The answer is 202: the newsletter is queued. Check the progress with get_newsletter. The events page shows which events exist for newsletters and whether they are available yet.
#If something goes wrong
| Code | Meaning |
|---|---|
template_not_approved | The template is not approved yet. Go back to step 2 and wait for the approval. |
audience_empty | The audience has no reachable contacts. |
newsletter_locked | The newsletter is already scheduled or sent. |
idempotency_key_required | You did not send the Idempotency-Key header. |
plan_expired | The plan_id of step 2 is older than 10 minutes. Make the preview call again. |
To pause or cancel a running newsletter use control_newsletter.