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}/messageswith 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.
/ 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.
| Concern | Hand-rolled wp_remote_post | Webhook Actions |
|---|---|---|
| Templates, opt-in, business verification | In Meta's tools, whatever sends the message | Same. It sends the request; the template and the opt-in checkbox are yours |
| Twilio as the sender | Works: form-encoded wp_remote_post | Not directly. Twilio's Messages endpoint wants form-encoded parameters and the plugin sends JSON. Use the Cloud API |
| The System User token | A constant in wp-config.php | A Bearer credential in the Credentials Vault, redacted in every log |
| The template envelope and the +E.164 number | Written in your function | A pre-dispatch Code Glue snippet that builds the body from mapped fields |
| Only customers who opted in | An if in your function | A condition on the opt-in field, so the webhook never fires without it |
| A 5xx or HTTP 429 | Message lost unless you wrote retry logic | Retried 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 131026 | Arrives on your status webhook, if you built one | Also 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'] ], ], ] ], ], ];
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.