Integrations
WooCommerce and WordPress
Installing and configuring the plugin, mapping variables with merge tags, and using custom order statuses.
The WordPress plugin turns WooCommerce order events into WhatsApp messages without any code. It holds no privileged access, everything it does goes through the same public API you could call yourself. What it saves you is the configuration: the mapping from an order transition to an event, from an order field to a template variable, and the queueing, deduplication and consent handling around it.
The plugin is growcord-order-notifications-for-woocommerce. It adds a WhatsApp menu to wp-admin with a Dashboard, Inbox, Events, Templates, Automations, Settings, Forms, Logs, Errors, Support and Tools screen.
What you need first
- WordPress
- 5.6 or newer, PHP 7.4 or newer.
- WooCommerce
- Optional. The account, OTP and form events work on plain WordPress; order, shipping, payment and return events need WooCommerce. HPOS (High-Performance Order Storage) and the block checkout are both supported, all order access goes through WooCommerce CRUD.
- A platform account
- With a linked WhatsApp Business number and an API key. The key is generated once under API Keys in the dashboard and shown once.
Install and connect
Install and activate the plugin
Upload it to/wp-content/plugins/or install it through Plugins → Add New. Activation creates the plugin's tables and schedules a five-minute background pass, the pass that actually delivers messages.Paste your API key
WhatsApp → Settings, then Test connection. A failure here is nearly always a copied space at the start or end of the key, or a key that has been revoked in the dashboard.Choose the number this site sends from
The dropdown fills from your account once the key is saved. A site with one number can leave it alone.Submit the templates for approval
WhatsApp → Events → Submit templates for approval. Then wait. Press Refresh to re-read the status from Meta. Approval is usually minutes and occasionally hours.Switch on the events you want and check their variables
The defaults suit a standard WooCommerce store. The section on merge tags below covers changing them.Send a test
WhatsApp → Tools → Send a test notification. A message that arrives on your own phone is the only proof that the whole chain works.

Events and their triggers
The Events screen has one row per notification: its Meta approval state, a switch, the list of order statuses that fire it, a per-variable mapping and a delay in minutes. The plugin's catalog mirrors the platform's 18 events exactly, and ships a test that fails if the two ever drift.
Each event also carries a delay. Zero sends on the next background pass, which is right for an order confirmation. A review request is better a day or two after delivery.

Custom order statuses
Every event has an editable trigger list holding WooCommerce status slugs. A store that adds packed, dispatched or awaiting-pickup, whether by a snippet or a shipping plugin, types the slug into the trigger box for the event it should fire, and it fires. The wc- prefix is stripped on both sides of the comparison, so wc-dispatched and dispatched mean the same thing.
The Order status update event exists for this. It carries orderStatus as a variable, so one event covers every extra status a store invents without a template per status. Defaults out of the box are processing and on-hold.
Two events can share a trigger. Adding completed to both Order delivered and Review request, the second with a delay, sends one message now and one later, from a single status change.
Merge tags
Meta fixes the wording of a template once it is approved. What you control is every value that slots into it, and each one is an expression you edit on the Events screen rather than a hard-coded field. A store that numbers its orders with a sequential-invoice plugin, keeps the customer's phone in a custom field, or wants the shipping city in a message, changes a text box.
An expression may contain any mix of these:
- {tag}
- A known tag, replaced by its value.
- {tag|Fallback}
- The text after the pipe is used when the tag resolves empty. Use it on anything that is not always present,
{tracking_number|Not yet assigned}beats a blank in the customer's message. - Literal text
- Passed through untouched, so
Order {order_number}is a valid expression.
Order
- {order_number}
- Order number as the customer sees it
- {order_id}
- Internal order ID
- {order_total}
- Order total, formatted, no currency symbol
- {order_subtotal}
- Subtotal before shipping and tax
- {order_status}
- Status label, e.g. "Processing"
- {order_items}
- Comma-separated item list
- {order_item_count}
- Total quantity ordered
- {order_first_item}
- Name of the first item
- {order_date}
- Date the order was placed
- {order_url}
- Customer "view order" link
- {order_pay_url}
- Pay-now link for unpaid orders
- {currency}
- Currency code, e.g. INR
Customer
- {customer_name}
- Full name
- {customer_first_name}
- First name
- {customer_last_name}
- Last name
- {billing_phone}
- Billing phone, the default recipient
- {billing_email}
- Billing email
- {billing_address}
- Full billing address, one line
- {shipping_address}
- Full shipping address, one line
- {shipping_city}
- Shipping city
Shipping
Tracking values are read from whichever shipment-tracking plugin is installed, WooCommerce Shipment Tracking, Advanced Shipment Tracking, AfterShip, Shiprocket and similar. If none is, they resolve empty and your fallback text is used.
- {tracking_number}
- AWB / tracking number from your tracking plugin
- {tracking_url}
- Carrier tracking link, falling back to the order page
- {tracking_provider}
- Carrier name
Payment
- {payment_method_title}
- Payment method as shown at checkout
- {transaction_id}
- Gateway transaction ID
- {refund_amount}
- Amount refunded, formatted
- {invoice_number}
- Invoice number from your PDF-invoice plugin
- {invoice_url}
- Invoice download link
Product
- {product_name}
- Product name
- {product_url}
- Product page link
- {product_price}
- Product price, formatted
- {product_sku}
- SKU
- {review_url}
- Link to the review section of the first product
Store
- {store_name}
- Store name
- {store_url}
- Store home page
- {store_email}
- Store admin email
- {cart_url}
- Cart-recovery link (abandoned cart only)
- {discount_code}
- Configured recovery discount code
Advanced
- {meta:_key}
- Any order or user meta field. Order meta is looked up first, then user meta. This is the escape hatch for anything another plugin has written to the order,
{meta:_wcpdf_invoice_number},{meta:_billing_gstin}. - {product_meta:_key}
- Any meta field on the resolved product.
- {extra:key}
- A value supplied by the trigger or the API call rather than read from the order, an OTP, a form field, a value passed to
wac_send(). - {tag|fallback}
- Text to use when the tag resolves empty.
Values passed explicitly by a caller win over the mapping. That is how an OTP, which no merge tag could produce, reaches a template variable.
Consent at checkout
WhatsApp polices business-initiated messaging by quality signal. Recipients who block or report a number drag its rating down, and a number that falls to LOW has its messaging limit cut, which takes out order confirmations, not only marketing. Honouring opt-out is an availability concern as much as a legal one.
The plugin has three consent modes, set under Settings:
- implied
- Giving a phone number at checkout implies consent to transactional updates. Marketing events still need an explicit opt-in unless the store turns that requirement off.
- checkbox
- A pre-ticked box at checkout, which the customer can clear.
- required
- An unticked box. Nothing is sent unless it is ticked.
The checkbox renders on both the classic checkout and the block checkout, and the customer can change their mind later from My account → Account details, which is the alternative to them contacting your support.
Chat widget and shortcodes
A click-to-chat link is not a notification. It opens WhatsApp on the visitor's own device with a message they then choose to send, no template, no approval, no credit, no consent record. It is handled entirely outside the sending path.
Switch the floating button on under Settings → Chat widget. You set the number, the button label, the prefilled message, the colour, which side of the screen it sits on, and which pages it appears on (all, shop, product, or a list you choose).
Two shortcodes place the same link inside your own content:
[whatsapp_button]
[whatsapp_button number="+919876543210" text="Ask about this" message="Hi, is {product_name} in stock?"]
[whatsapp_link]Message us[/whatsapp_link]
[whatsapp_link number="+919876543210" message="Hi, I have a question about {product_name}"]Ask a question[/whatsapp_link]Both accept number, text, message and class, and all four fall back to the widget settings when omitted. The message attribute resolves merge tags against the current page, so the same shortcode in a product template produces a different prefilled message on every product.
Developer hooks
One function sends. Everything else is a filter that changes a decision the plugin has already made, which means an extension does not have to reimplement consent, deduplication or queueing to change one thing.
wac_send()
/**
* Send a WhatsApp notification.
*
* @param string $event Event key from the catalog, e.g. 'order_shipped'.
* @param string $to Recipient in E.164, e.g. '+919876543210'.
* @param array $variables Named variables for the event.
* @param array $args Optional: 'dedupe_key', 'force', 'delay', 'sync', 'source'.
* @return array{success:bool,queued?:bool,message_id?:string,error?:string,code?:string}
*/
$result = wac_send(
'order_shipped',
'+919876543210',
array(
'orderId' => '#ORD-9001',
'awbCode' => 'AWB123456789',
'trackingLink' => 'https://example.com/track/AWB123456789',
),
array( 'dedupe_key' => 'shipped:9001' )
);
if ( ! $result['success'] ) {
error_log( 'WhatsApp send failed: ' . $result['code'] . ' ' . $result['error'] );
}The call returns as soon as the notification is queued; queued is true and there is no message id yet. Pass 'sync' => true to deliver inline and get the platform's message id back, worth it for a one-off admin action, wrong for anything on a customer request path.
Always pass a dedupe_key. It is what stops a retried request sending twice. force skips the consent register and exists for authentication flows, where the person has asked for the code they are about to receive.
The filters worth knowing
- wac_should_send
( bool $send, string $event, string $to, array $variables, array $context ), the last chance to stop a send. Returningfalserecords a skip with the reason, so it shows in the log rather than disappearing.- wac_recipient
( string $raw, string $event, array $context ), change who gets the message before the number is normalised. For a store that keeps a separate WhatsApp number away from the billing phone.- wac_event_variables
( array $variables, string $event, array $context ), the resolved variables immediately before sending. For anything a merge-tag expression cannot compute.- wac_merge_tag
( string $value, string $tag, array $context ), add your own tag. Return a value for the tag you recognise and the incoming$valuefor everything else.- wac_event_catalog
( array $catalog ), add an event. It then gets its own toggle, variable mapping and trigger list in the admin, andwac_send()accepts it.- wac_events_for_trigger
( array $events, string $trigger ), decide which events a status change fires, in code, when a trigger list cannot express the rule.- wac_consent_check
( bool $allow, string $phone, string $event, array $context ), override the consent decision. An explicit opt-out has already been honoured before this runs.- wac_order_tracking
( array $result, WC_Order $order ), supply tracking number, URL and carrier for a shipping plugin the resolver does not know.
Actions to hook rather than filter: wac_notification_queued, wac_notification_sent, wac_notification_failed, wac_inbound_message, wac_opted_out, wac_opted_in and wac_form_submitted.
The delivery log
WhatsApp → Logs records four outcomes: queued, sent, failed and skipped. The last one is the reason this screen exists. "Nothing arrived and nothing failed" is the most common report a store makes, and it almost always means a skip: no usable phone number, an opt-out, an event switched off, a duplicate already claimed, or a rate ceiling on a form rule.
Every row carries the resolved variables that were sent, so a message that arrived with a - in it can be traced to the expression that resolved empty. Rows are kept for the number of days set under Settings, 30 by default.

Delivery itself runs on a five-minute background pass, or on Action Scheduler when another plugin has made it available. Failures are retried with exponential back-off up to the attempt limit set under Settings. Nothing in the checkout request waits on a remote call, so a slow platform response cannot slow a checkout down.
Next
- Zapier, Make and form plugins, the plugin's authenticated REST surface and the seven form-plugin bridges.
- Abandoned cart recovery and cash on delivery confirmation, two features the plugin switches on for you.
- Troubleshooting, when a message did not arrive and the log does not say why.
