New in 2.2: Validators, JSONPath and signatures See what's new →

WhatsApp marketing messages leave the Cloud API on 13 October 2027: how to migrate and test

Share

If your app sends WhatsApp offers, sale alerts or newsletters through the Cloud API, it has a deadline. On 21 October 2026 Meta announced that the Cloud API will stop supporting marketing messages on 13 October 2027. After that, Meta's own overview says, "sending marketing messages on Cloud API will produce an error." Marketing templates have to go through the Marketing Messages API for WhatsApp (MM API) instead.

The change is small in code and easy to get wrong in production, because a template's category isn't fixed: Meta can move a utility template into marketing. This post covers the dates, the endpoint change, how to route each send correctly, and tests you can run without sending a single real message.

The dates

DateWhat happens
19 November 2025The Marketing Messages API for WhatsApp became generally available
21 October 2026Meta announced the Cloud API sunset for marketing messages
13 April 2027Messaging accounts created on or after this date can't send marketing messages on the Cloud API at all
13 October 2027Marketing messages on the Cloud API return an error for everyone

Service, authentication (OTP) and utility messages such as order updates stay on the Cloud API. Only templates in the MARKETING category move.

What changes in the request

Meta describes the MM API as having "a similar technical schema and the same billing model as Cloud API", and your phone numbers and approved templates carry over. The visible difference is the endpoint. A marketing template today:

Terminal
curl -X POST "https://graph.facebook.com/v26.0/$PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "919800000000",
    "type": "template",
    "template": { "name": "diwali_sale", "language": { "code": "en" } }
  }'

The same send on the MM API:

Terminal
curl -X POST "https://graph.facebook.com/v26.0/$PHONE_NUMBER_ID/marketing_messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "919800000000",
    "type": "template",
    "template": { "name": "diwali_sale", "language": { "code": "en" } }
  }'
Template categoryEndpoint
MARKETING/<PHONE_NUMBER_ID>/marketing_messages
UTILITY, AUTHENTICATION, service replies/<PHONE_NUMBER_ID>/messages

Meta's onboarding has three steps, and it estimates about two weeks for most integrations:

  • Accept the Marketing Messages API for WhatsApp terms in your business account.
  • Make your integration send marketing templates to the new endpoint, and confirm it works live.
  • Check reclassified traffic: utility templates that Meta has moved to marketing must go to the MM API too.

The part that breaks: reclassified templates

A template you created as UTILITY, say "your item is back in stock", can be reclassified as MARKETING by Meta. If your code picks the endpoint from the category it saved when the template was created, that send now goes to the wrong place. Meta documents two errors here:

  • 131063: /messages refuses a marketing template when marketing on the Cloud API is switched off for your account. Refresh the category and send it to /marketing_messages.
  • The reverse: a template moved from marketing to utility is refused by /marketing_messages and has to go back to /messages.

Meta's advice is to subscribe to the template_category_updatewebhook and update your stored category only from the completed notification's new_category field. A simplified payload:

JSON
{
  "object": "whatsapp_business_account",
  "entry": [{
    "changes": [{
      "field": "template_category_update",
      "value": {
        "message_template_id": 1234567890,
        "message_template_name": "order_back_in_stock",
        "message_template_language": "en",
        "previous_category": "UTILITY",
        "new_category": "MARKETING"
      }
    }]
  }]
}

Route by the latest category

Keep a map of template name to latest category, fed by the webhook, and choose the endpoint from it on every send. If a send fails, look the category up once from the Message Templates API, and retry only if it actually changed:

send-template.mjs
// Pick the endpoint from the template's latest category, and recover once if it changed.
const ENDPOINT = { MARKETING: "marketing_messages", UTILITY: "messages", AUTHENTICATION: "messages" };

export async function sendTemplate({ phoneNumberId, token, to, template, categories, refreshCategory }) {
  const send = (category) =>
    fetch(`https://graph.facebook.com/v26.0/${phoneNumberId}/${ENDPOINT[category]}`, {
      method: "POST",
      headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
      body: JSON.stringify({ messaging_product: "whatsapp", to, type: "template", template }),
    });

  let category = categories.get(template.name) ?? (await refreshCategory(template.name));
  let res = await send(category);
  if (res.ok) return res;

  // 131063: a marketing template went to /messages. Any other 4xx may also mean the
  // category changed, so look it up once and retry only if it really did.
  const err = (await res.clone().json()).error;
  const fresh = await refreshCategory(template.name);
  if (fresh !== category && (err?.code === 131063 || res.status === 400)) {
    categories.set(template.name, fresh);
    res = await send(fresh);
  }
  return res;
}

The "only if it changed" rule matters. A marketing message that gets retried blindly can reach the customer twice, and you pay for both. Meta's guide makes the same point: retry only after a confirmed failure, and make retries idempotent.

Testing it without sending real messages

You don't want a test suite that sends WhatsApp messages to real phones. Mock fetchinstead, and make the fake answer the way the Graph API does after the switch. Node's built-in test runner can do this with no libraries:

send-template.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import { sendTemplate } from "./send-template.mjs";

// Fake Graph API: answers like Meta does once marketing on /messages is switched off.
function fakeGraph(t) {
  const calls = [];
  t.mock.method(globalThis, "fetch", async (url) => {
    calls.push(new URL(url).pathname.split("/").pop());
    if (url.endsWith("/messages") && calls.length === 1) {
      return Response.json({ error: { code: 131063, message: "Use the Marketing Messages API" } }, { status: 400 });
    }
    return Response.json({ messages: [{ id: "wamid.TEST" }] });
  });
  return calls;
}

const base = { phoneNumberId: "123", token: "t", to: "919800000000",
  template: { name: "order_back_in_stock", language: { code: "en" } } };

test("a template reclassified to marketing moves to /marketing_messages", async (t) => {
  const calls = fakeGraph(t);
  const categories = new Map([["order_back_in_stock", "UTILITY"]]);   // stale
  const res = await sendTemplate({ ...base, categories, refreshCategory: async () => "MARKETING" });
  assert.equal(res.status, 200);
  assert.deepEqual(calls, ["messages", "marketing_messages"]);
  assert.equal(categories.get("order_back_in_stock"), "MARKETING");
});

test("no retry when the category didn't change", async (t) => {
  const calls = fakeGraph(t);
  const categories = new Map([["order_back_in_stock", "UTILITY"]]);
  const res = await sendTemplate({ ...base, categories, refreshCategory: async () => "UTILITY" });
  assert.equal(res.status, 400);
  assert.deepEqual(calls, ["messages"]);    // sent once, never twice
});

The second test is the important one: it proves a failure that isn't about the category is sent once and not retried.

Other things to check before October 2027:

  • The webhook handler.Point Meta's webhook at a capture URL, such as our webhook tester, to see real template_category_update payloads before you write the parser.
  • US numbers.Marketing messages aren't delivered to WhatsApp users in the United States, which shows up as error 131049. Don't retry it.
  • New accounts. If you onboard clients, every account created from 13 April 2027 must use the MM API from day one. Test your onboarding flow, not just existing accounts.
  • Region limits.In the EEA, UK, Japan, South Korea and a few other markets, the MM API's delivery optimisation and click reporting aren't available, so don't write tests that expect those fields there.

For more on mocking an API's failures in tests, see our guide to mock API error responses, and for retries that don't double-send, the resilience track.