Features
Cash on delivery confirmation
Confirming a COD order before you dispatch it, by tapped button, or by a code the customer sends back.
Cash-on-delivery orders are cheap to place and expensive to refuse at the door. This feature holds a COD order until the customer confirms it on WhatsApp, so a fake or mistaken order is caught before you pay for shipping in both directions.
There are two paths, and which one runs depends on whether the customer's 24-hour window happens to be open at that moment.
What has to be true first
- A registered number
- As with every event.
- The order_verification template approved
- Meta template name
order_verification, category AUTHENTICATION. It is only needed for the code path, but that is the path that runs most of the time, approve it. - The order_verification event switched on
- Off by default in the event catalog.
- COD confirmation switched on
- WooCommerce:
cod_enabled, default off. Shopify: Cash-on-delivery confirmation on the Features tab, default off. - A billing phone on the order
- An order with no readable phone is left unverified rather than held indefinitely.
- Shopify only: the write_orders scope
- Tagging and cancelling an order both write to the store. Without it the whole flow fails silently.
The two paths
The order is placed and put on hold
WooCommerce moves it to the Hold status,on-holdby default, and adds the noteAwaiting WhatsApp confirmation of this Cash-on-Delivery order.On Shopify the normal order confirmation is suppressed so the customer gets one message, not two.Interactive buttons are tried first
If the customer's 24-hour window is open, they get a message with two tappable buttons and nothing else is needed.Otherwise, a one-time code is sent
Buttons are free-form and cannot cross a closed window. So when the window is shut, which it is for almost every new customer, the approvedorder_verificationtemplate carries a six-digit code instead, and the customer sends it back.
The button path: window open
The customer sees:
Your Store: please confirm your Cash-on-Delivery order #1234 so we can send it out.
Tap Cancel if you did not place this order.
[ Confirm order ] [ Cancel order ]The two buttons are Confirm order and Cancel order. Their titles are translated, so nothing routes on the title. The decision is carried by the button's id, which is signed.
Signed button ids
| Field | Required | Type | Description |
|---|---|---|---|
| WooCommerce | No | id format | cod_ok:<orderId>:<32 hex> and cod_no:<orderId>:<32 hex>. The signature is an HMAC over the action and the order id, keyed with the site's auth salt. |
| Shopify | No | id format | cod:confirm:<orderId>:<hmac> and cod:cancel:<orderId>:<hmac>. The HMAC is truncated to 128 bits and compared in constant time. |
Signing the id is what stops a customer editing an id and confirming somebody else's order. A second, independent check then requires that the tap came from the order's own billing number. A mismatch is refused and logged with A Cash-on-Delivery confirmation button was tapped from a number that is not this order's billing number. It was refused.
What each button does
Outcomes
| Field | Required | Type | Description |
|---|---|---|---|
| Confirm, WooCommerce | No | action | Order moves to the On confirmation status ( processing by default) with the note Cash-on-Delivery order confirmed by the customer on WhatsApp. |
| Cancel, WooCommerce | No | action | Order moves to the On refusal or timeout status ( cancelled by default) with the note Cash-on-Delivery order cancelled by the customer on WhatsApp. |
| Confirm, Shopify | No | action | The order is tagged cod-confirmed. Existing tags are preserved. The order is not otherwise moved. |
| Cancel, Shopify | No | action | The order is actually cancelled with reason customer and no Shopify email, then tagged cod-cancelled. |
Both integrations write the decision before touching the order, so a redelivered webhook finds it already decided and does nothing. On Shopify the uniqueness key is the shop and the order, not the action, so once either button is tapped the other is inert for that order. Shopify also sends a short free-form acknowledgement afterwards, on a best-effort basis.
The code path: window closed
The customer receives the approved order_verification template:
{{1}} is your order confirmation code.483920 is your verification code.
12:04It is an authentication template, so Meta renders it with a security recommendation, a ten-minute expiry footer and a Copy code button. The single variable is otp.
How the customer returns it
WooCommerce accepts it two ways:
- By replying on WhatsApp. The digits are pulled out of free text, so "483920", "Confirm 483920" and "my code is 483920" all work. The order is then found by matching the sender's number against recent orders still awaiting confirmation.
- By link,
?wac_cod=<order key>&code=…on the store. Success showsThanks. Your order is confirmed.; a wrong code showsThat confirmation code is not valid.
The code is never stored in clear
This is worth being precise about, because it is the whole point.
- Generated
- Six digits by default (configurable from 4 to 10 in WooCommerce), from the platform's random source.
- Stored
- Only as a keyed HMAC-SHA256 digest, in the order meta key
_wac_cod_hash. The raw code is never written to the order. A slow password KDF is deliberately not used, the attempt counter, not hash cost, is what stops guessing. - Deleted
- The hash is removed the moment the order is confirmed, cancelled, locked or expired. There is nothing left to test against.
- Order notes
- Never. Notes carry attempt counts and outcomes only, the submitted value never appears, because a near-miss in the log is a hint at the real code.
- Delivery log
- Never, twice over. Every variable of an AUTHENTICATION event is masked to
******before the log row is written, and the COD handler then overwrites that row's variables again. Shopify does the same, replacing anyotpwith***. - Error reports
- Scrubbed by pattern, so a code that reaches an exception message is redacted before it is recorded.
- The one exception
- While the message is waiting in the send queue, the queue row holds the real value. It is the payload. The row's variables are cleared once the message is delivered.
Attempts and expiry
WooCommerce
| Field | Required | Type | Description |
|---|---|---|---|
| cod_expiry_minutes | No | setting | Minutes the customer has to reply, field Give the customer. Range 30 to 20,160. A floor of 5 minutes is applied whatever the setting says. Default: 1440 |
| Attempt limit | No | fixed | Five wrong codes. Under the limit each failure adds the note Incorrect Cash-on-Delivery confirmation code (attempt N of 5).Default: 5 |
| On lockout | No | fixed | The state becomes locked, the hash is deleted, and the note reads Cash-on-Delivery confirmation locked after 5 incorrect codes. Confirm this order by phone before dispatching. Locking rather than cancelling is deliberate: a customer fat-fingering a code should not lose their order. |
What happens if nobody answers
WooCommerce sweeps hourly for held orders whose expiry has passed, sets them to the On refusal or timeout status and adds the note Cash-on-Delivery confirmation was not received before the code expired.
Because it is an hourly sweep rather than a per-order timer, the actual cancellation can lag the expiry by up to an hour. Orders older than 14 days are not swept at all.

The variables
order_verification
| Field | Required | Type | Description |
|---|---|---|---|
| otp | Yes | string | The COD confirmation passcode. Sent as both the body parameter and the copy-code button parameter, because Meta requires both on an authentication template. Example: 123456 |
When it does not fire
- COD confirmation is off, or the
order_verificationevent is off. Both default to off. - The order's payment method is not cash on delivery. WooCommerce requires the payment method to be exactly
cod; Shopify matches its gateway names against a cash-on-delivery pattern. - The order was already verified. An existing state on the order means nothing is started again.
- There is no usable phone number. The order is not held. It is logged as skipped with
COD order has no usable phone number, so it was left unverified.and left alone, so a missing number never strands an order. - The plugin is not connected. The interactive attempt is abandoned and the flow falls through to the code path.
- The template is not approved. With the window closed and no approved template, there is no way to reach the customer at all.
- The tap came from a different number than the order's billing phone. Refused and logged.
- The signature does not verify. Refused silently.
- The order is already decided, or the code has expired. A late tap is a no-op.
- Five wrong codes. Locked, and only a human can move it on.
Cost
The code path uses an AUTHENTICATION template: 1 credit to an Indian number and 6 elsewhere under the shipped default rate card, the same as a utility message, and far below a marketing one.
The button path is a free-form interactive message inside an open window, which is a SERVICE conversation and costs nothing. So the cheaper path is also the better experience, when the window allows it. See Credits and billing.
