WP Webhooks / Blog / Integrations

Sending WooCommerce Order Updates to WhatsApp With Template Messages

WooCommerce WhatsApp order notifications on the Cloud API: utility templates, the System User token, phone formats, opt-in, and the 250-customer limit.

9 min 2026-10-05
#whatsapp#woocommerce#api

TL;DR: A WooCommerce WhatsApp order notification is a template message, and nearly every failure comes from treating it like a text message.

  • The customer has not written to you, so there is no open 24-hour window. Business-initiated messages must use a template Meta has approved. Order updates are the utility category.
  • The send is one call: POST graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages with a System User token. A 200 means accepted. Delivery and failure arrive later, on your webhook.
  • You need the customer's opt-in before the first message, and a phone number with the country code. WooCommerce stores whatever the customer typed.
  • A new business can reach 250 unique customers per 24 hours with templates until it is verified.

/ Plugins

Do WooCommerce WhatsApp plugins send order notifications?

The popular ones mostly do not. The plugins with the largest install counts are click-to-chat buttons: Joinchat (700,000+) and Click to Chat (600,000+) put a button on the page that opens a chat the customer starts.¹ Useful, and a different job. Nothing is sent when an order is paid or shipped.

The large exception is Meta's own Meta for WooCommerce (400,000+), whose description includes connecting a WhatsApp Business account to update customers about their orders. Try it first. The API route below is for the cases it does not fit: your own template wording, statuses it does not cover, a shipping-tracking message from another plugin, or a store that does not want the catalogue and pixel parts of that plugin.

/ Templates

Why does a WhatsApp order notification have to be a template?

Because the customer has not written to you. A message from the customer opens a 24-hour customer service window, and only inside it can a business send free-form text. Outside it, the WhatsApp Business policy allows only approved message templates. An order confirmation is sent minutes after checkout to someone who has never messaged the store, so there is no window, and a plain text message fails with error 131047, "more than 24 hours have passed since the recipient last replied".

Every template has one of three categories: marketing, utility or authentication. Meta's categorization guide lists order management, "confirm, update, or cancel an order", as utility, with example templates for "order is confirmed" and "on its way". The guide's condition is strict: a utility template must not upsell, cross-sell or carry an offer. Add "15% off your next order" to the shipping message and it is a marketing template, reviewed and priced as one.

You create the template in WhatsApp Manager with numbered variables, for example "Hi {{1}}, your order {{2}} is confirmed.", and submit it. Review can take up to 24 hours, and only an APPROVED template can be sent.

FIG 01 — The send call returns an id, not a result: delivery and failure come back later on your own webhook

/ Cloud API

How do you send a WhatsApp template message from WooCommerce?

With one request to the Cloud API messages endpoint: POST https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages. The phone number id is the id of your business number in WhatsApp Manager, not the number itself.

Authenticate with a System User access token, not your personal one. Meta's access token guide recommends system user tokens for a business integrating directly. Generate it with whatsapp_business_messaging and whatsapp_business_management, and choose the expiry: a token can be set to never expire, or to last 60 days. The temporary token on the API setup screen expires quickly, which is why so many first integrations stop working the next day.

JSON — POST https://graph.facebook.com/v26.0/PHONE_NUMBER_ID/messages

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "+48600100200",
  "type": "template",
  "template": {
    "name": "order_confirmation",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Anna" },
          { "type": "text", "text": "10482" }
        ]
      }
    ]
  }
}

The parameters fill the template's variables in order. Send two for a template with two variables: a different count fails with 132000, and a template name or language code that does not match an approved template fails with 132001. The language is part of the template's identity, so en_US and en_GB are two templates.

A success returns 200 with a message id starting wamid.. That only means Meta accepted the request. Whether the message reached the phone arrives later as a status webhook: sent, delivered, read or failed. A customer who is not on WhatsApp produces a 200 now and a failed status with 131026 a moment later.

PHP — send the confirmation when an order is paid

add_action( 'woocommerce_order_status_processing', function ( $order_id, $order ) {
    if ( $order->get_meta( '_whatsapp_opt_in' ) !== 'yes' ) {
        return; // no opt-in, no message
    }
    $to = wa_e164( $order->get_billing_phone(), $order->get_billing_country() );
    if ( ! $to ) {
        $order->add_order_note( 'WhatsApp: phone number not usable.' );
        return;
    }

    // In production: hand this to a queue instead of calling inline.
    $res = wp_remote_post( 'https://graph.facebook.com/v26.0/' . WA_PHONE_NUMBER_ID . '/messages', [
        'timeout' => 10,
        'headers' => [
            'Authorization' => 'Bearer ' . WA_SYSTEM_USER_TOKEN,
            'Content-Type'  => 'application/json',
        ],
        'body'    => wp_json_encode( [
            'messaging_product' => 'whatsapp',
            'to'                => $to,
            'type'              => 'template',
            'template'          => [
                'name'       => 'order_confirmation',
                'language'   => [ 'code' => 'en_US' ],
                'components' => [ [
                    'type'       => 'body',
                    'parameters' => [
                        [ 'type' => 'text', 'text' => $order->get_billing_first_name() ],
                        [ 'type' => 'text', 'text' => $order->get_order_number() ],
                    ],
                ] ],
            ],
        ] ),
    ] );

    $body = json_decode( wp_remote_retrieve_body( $res ), true );
    if ( isset( $body['messages'][0]['id'] ) ) {
        $order->update_meta_data( '_whatsapp_confirmation_id', $body['messages'][0]['id'] );
        $order->save();
    } elseif ( isset( $body['error']['code'] ) ) {
        $order->add_order_note( sprintf( 'WhatsApp error %d: %s',
            $body['error']['code'], $body['error']['message'] ) );
    }
}, 10, 2 );

Meta's error code reference asks you to build error handling on error.code, not on the HTTP status, which is why the code above reads the body rather than the status line.

/ Phone numbers

What phone number format does the WhatsApp API need?

The full international number. The API accepts a leading +, spaces, brackets and hyphens, and Meta strongly recommends the plus sign and country code. Without the +, Meta prepends your business number's country code, so a German customer's local number sent from a Polish business number becomes a Polish number, possibly someone else's.

WooCommerce does not normalise billing phones. It stores what the customer typed: 0151 2345678, +49 151 2345678 and 00491512345678 are all common at the same German store. Convert them once, with the billing country as the fallback for a number that has no country code, and skip the message rather than guess when the result does not look like a number.

PHP — normalise a WooCommerce billing phone to +E.164

function wa_e164( $raw, $country ) {
    $digits = preg_replace( '/[^\d+]/', '', (string) $raw );
    if ( strpos( $digits, '00' ) === 0 ) {
        $digits = '+' . substr( $digits, 2 );          // 0049… → +49…
    }
    if ( strpos( $digits, '+' ) !== 0 ) {
        $calling = WC()->countries->get_country_calling_code( $country ); // e.g. "+49"
        if ( ! $calling ) {
            return null;
        }
        $digits = $calling . ltrim( $digits, '0' );    // 0151… → +49151…
    }
    return preg_match( '/^\+\d{8,15}$/', $digits ) ? $digits : null;
}

Dropping the leading zero is right for most of Europe and wrong for a few countries, Italy among them, where the zero belongs to landline numbers. For a store that ships everywhere, the safer fix is a phone field that requires the country code at checkout.

/ Opt-in and limits

What do you need before sending WhatsApp messages to customers?

Their opt-in, first. Meta's opt-in guide requires the phone number and an opt-in that clearly says the person agrees to receive messages from your business, by name. A website checkbox counts. A pre-ticked one, or a phone number collected for delivery, is not consent. Store the answer on the order, as the _whatsapp_opt_in check above assumes, and honour opt-outs.

Second, the messaging limit: how many unique customers you may reach with templates outside a service window in a moving 24 hours. It starts at 250. Verifying the business raises it to 2,000, and it then scales automatically to 10,000, 100,000 and unlimited as quality holds. Work it out for your store: a shop with 180 orders a day that sends a confirmation and a shipping message reaches about 180 unique customers, because the limit counts people, not messages. A Black Friday with 400 orders hits 250 by mid-afternoon on an unverified account, and every later message is refused. Verify before the sale, not during it.

Third, the price. Meta charges per delivered template message by category and by the customer's country, per its pricing page, and may change rates on the first day of any quarter. If you send through Twilio, Twilio's WhatsApp pricing adds $0.005 per message on top: 2,000 notifications a month is 2,000 × $0.005 = $10 in Twilio fees before Meta's charge.²

/ Twilio

Should you use Twilio or the WhatsApp Cloud API directly?

Use Twilio if you already send SMS through it and want one provider, one log and one bill. The call is the Messages resource you may already use for SMS, with To=whatsapp:+48600100200, Basic auth with an API key, and a template sent as a ContentSid plus ContentVariables, a JSON string such as {"1":"Anna","2":"10482"}. The parameters are form-encoded, not JSON. Twilio's sandbox only messages numbers that joined it and only sends its own pre-approved templates, so production needs your own sender. The WooCommerce SMS with Twilio article covers the same hook for text messages.

Use the Cloud API directly to skip the per-message fee and an extra provider. The trade is that the template setup, the token and the status webhooks all live in Meta's tools.

ConcernHand-rolled wp_remote_postWebhook Actions
Templates, opt-in, business verificationIn Meta's tools, whatever sends the messageSame. It sends the request; the template and the opt-in checkbox are yours
Twilio as the senderWorks: form-encoded wp_remote_postNot directly. Twilio's Messages endpoint wants form-encoded parameters and the plugin sends JSON. Use the Cloud API
The System User tokenA constant in wp-config.phpA Bearer credential in the Credentials Vault, redacted in every log
The template envelope and the +E.164 numberWritten in your functionA pre-dispatch Code Glue snippet that builds the body from mapped fields
Only customers who opted inAn if in your functionA condition on the opt-in field, so the webhook never fires without it
A 5xx or HTTP 429Message lost unless you wrote retry logicRetried with exponential backoff. It decides on the HTTP status; a WhatsApp error code returned with a 4xx is logged as failed, not retried
Delivery failure such as 131026Arrives on your status webhook, if you built oneAlso only on your status webhook. The plugin sends outbound requests; it does not receive WhatsApp status callbacks

PHP — pre-dispatch Code Glue snippet that builds the template body

// Field mapping has produced: phone, country_code ("+49"), first_name, order_number.
$to = preg_replace( '/[^\d+]/', '', (string) $payload['phone'] );
if ( strpos( $to, '+' ) !== 0 ) {
    $to = $payload['country_code'] . ltrim( $to, '0' );
}

return [
    'messaging_product' => 'whatsapp',
    'to'                => $to,
    'type'              => 'template',
    'template'          => [
        'name'       => 'order_confirmation',
        'language'   => [ 'code' => 'en_US' ],
        'components' => [ [
            'type'       => 'body',
            'parameters' => [
                [ 'type' => 'text', 'text' => (string) $payload['first_name'] ],
                [ 'type' => 'text', 'text' => (string) $payload['order_number'] ],
            ],
        ] ],
    ],
];
try_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

/ Queue

Should the WhatsApp call run inside the order status change?

No. woocommerce_order_status_processing runs in the request that moved the order, often the payment gateway's callback. A slow Graph API response inside it delays the gateway's answer, and a gateway that times out sends its notification again, which can move the order again and send the message again. Record the notification when the hook fires and send it from a background worker.

Retries need more care than with most APIs, because a duplicate is a second message on someone's phone. Store the wamid on the order, as above, and never send a template for an order and status that already has one. Throughput is rarely the problem: the default is 80 messages per second per number. The limit you will meet is 131056, too many messages to the same customer in a short time, which a confirmation, a shipping notice and a delivery notice inside one minute can trigger. Space them out. The retry policy article covers backoff.

/ Exposure

What does the WhatsApp API not protect you from?

Your quality rating. Customers can block or report your number. Enough of that lowers the number's quality and, with it, how far your messaging limit grows. Send what the customer expects and nothing else; a utility message with a coupon in it is how stores learn this.

Silent delivery failures. Without a status webhook, a customer who is not on WhatsApp looks the same as one who read the message: both returned 200. If the notification matters, subscribe to statuses and fall back to email or SMS on failed.

A token that never expires. A non-expiring System User token is a permanent key to send messages as your business. Keep it out of the theme and the repository, give it only the WhatsApp permissions, and revoke it when the person who set it up leaves.

Consent records. Meta can ask how a recipient opted in. The checkbox value on the order, with its date, is your answer; a number in a CSV is not.

/Footnotes
¹ Install counts from the WordPress.org plugin directory, read 2026-10-07.
² Twilio WhatsApp pricing, "pricing current as of September 2026": $0.005 per inbound or outbound message in addition to Meta's fee. Meta's per-message rates differ by template category and country.
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Can WooCommerce send order notifications on WhatsApp? +
Yes, through the WhatsApp Business Platform. Meta for WooCommerce offers order updates, and the Cloud API lets you send your own: an approved utility template sent to the customer's number when the order status changes. The most installed WhatsApp plugins are click-to-chat buttons and do not send notifications.
Why does my WhatsApp order message fail with error 131047? +
Because you sent a free-form message outside a customer service window. A window opens only when the customer messages you, and lasts 24 hours. Order notifications are business-initiated, so they must be approved template messages. Order confirmations and shipping updates belong in the utility category.
Which access token should WooCommerce use for the WhatsApp Cloud API? +
A System User access token with the whatsapp_business_messaging and whatsapp_business_management permissions. You choose its expiry when you generate it, including a token that never expires. The temporary token shown on the API setup screen expires quickly and is meant for testing.
How many customers can I message on WhatsApp per day? +
The messaging limit counts unique customers reached with templates outside a service window in a moving 24 hours. It starts at 250, rises to 2,000 after business verification, and then scales to 10,000, 100,000 and unlimited. A busy sale day can hit 250 on an unverified account.
Does a 200 response mean the WhatsApp message was delivered? +
No. A 200 with a wamid means Meta accepted the request. Delivery arrives later as a status webhook: sent, delivered, read or failed. A number that is not on WhatsApp returns 200 first and a failed status with error 131026 afterwards, so subscribe to status webhooks if delivery matters.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate