smartchat

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

CodeMeaning
template_not_approvedThe template is not approved yet. Go back to step 2 and wait for the approval.
audience_emptyThe audience has no reachable contacts.
newsletter_lockedThe newsletter is already scheduled or sent.
idempotency_key_requiredYou did not send the Idempotency-Key header.
plan_expiredThe 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.