On this page (5)
If your shop is not Shopify or WooCommerce — a custom site, a Laravel or CodeIgniter build, a headless storefront, an in-house CRM — it connects with two HTTP requests: one when a cart changes, one when an order changes. This guide is for whoever maintains that code.
Everything below is copy-and-paste ready. A working integration is usually an afternoon's work, and most of it is deciding where in your checkout the cart event belongs.
Step 1 — Create the store and copy its ingest URL
- 1
Add the store
Commerce → Stores → Add store → Custom store. Enter the store name, the Currency it sells in (INR), and the Default country code for its shoppers (91).
- 2
Why those two fields matter
Shopify and WooCommerce report their own currency; a custom store has to be told. And an Indian checkout usually posts 7709638776 with no country code — WhatsApp cannot deliver to that, so Wapzio prefixes the default. Numbers that already look international are left alone.
- 3
Copy the ingest URL
Open Connection details on the new card and copy the Ingest URL. It looks like this:
Ingest URL https://api.wapzio.com/api/ecommerce/webhook/custom/<your-store-token>Watch outTreat it like a password: the token in the path identifies your store. Keep it on your server, never in browser JavaScript or a mobile app. If it leaks, use Rotate token on the card's ⋯ menu.
Step 2 — Post the cart
Send a cart whenever it changes in a way worth reminding about: an item added, the checkout form filled in, payment started and not finished. Posting the same cart_id again updates that one cart instead of creating another.
{
"event": "cart_updated",
"cart_id": "CART-10432",
"phone": "919876543210",
"email": "asha@example.com",
"first_name": "Asha",
"currency": "INR",
"total": 1559,
"checkout_url": "https://myshop.com/cart/recover/CART-10432",
"whatsapp_opt_in": true,
"consent_text": "Send me order updates on WhatsApp",
"items": [
{
"product_id": "SKU-91",
"title": "Cast Iron Kadai 10 inch",
"quantity": 1,
"price": 1499,
"url": "https://myshop.com/product/cast-iron-kadai-10",
"image_url": "https://myshop.com/img/kadai.webp"
}
]
}| Field | Required | Notes |
|---|---|---|
| cart_id | Yes | Your own id for this cart. The same value later updates the same cart. id, token and cart_key are accepted too. |
| phone | Yes, in practice | The shopper's number. Without it the cart is stored but can never be messaged. mobile, customer.phone and customer.mobile are accepted. |
| total | Yes | Cart value as a number, no currency symbol. subtotal is accepted. |
| currency | Recommended | Falls back to the store's currency. |
| checkout_url | Recommended | The link the reminder button opens. Make it restore the cart, or point at a pay page for that order. cart_url and recovery_url are accepted. |
| first_name / email | Recommended | Used in the message copy and to match the shopper to a contact. |
| whatsapp_opt_in | Recommended | true when the shopper ticked a consent box. Pair it with consent_text, the exact wording they saw. |
| items[] | Recommended | title, quantity, price, url, image_url. qty, name and unit_price are accepted. |
Wapzio reads the event type from the body, and anything carrying an order id or order number is treated as an order, not a cart. A cart posted with those fields silently becomes an order event.
Step 3 — Post the order
Post an order event when payment succeeds and again at every stage change. The order_placed event is what stops the cart reminders, so send it as soon as payment is confirmed — including from your payment gateway's webhook, not only from the browser redirect.
{
"event": "order_placed",
"order_id": "10871",
"order_number": "KK-20260917-A8B838",
"cart_id": "CART-10432",
"phone": "919876543210",
"first_name": "Asha",
"currency": "INR",
"total": 1559,
"payment_method": "prepaid",
"financial_status": "paid",
"items": [
{ "title": "Cast Iron Kadai 10 inch", "quantity": 1, "price": 1499 }
]
}| event | When to send it | What Wapzio does |
|---|---|---|
| order_placed | Payment confirmed, or a Cash on Delivery order accepted by your shop. | Closes the matching cart as Recovered, stops its reminders, and sends the Order placed or COD confirmation message. |
| order_shipped | You hand the parcel to the courier. order_fulfilled works too. | Sends the shipping update. Include tracking_number, tracking_url and tracking_company for the copy. |
| order_delivered | The courier marks it delivered. | Sends the delivered message, and starts the feedback timer if that message is on. |
| order_cancelled | Your shop cancels the order. | Sends the cancellation message. Never revives cart reminders. |
- Send cart_id on the order when you have it. Wapzio also matches on the phone number, but the cart id is exact.
- payment_method: "cod" (or cod: true) is what triggers the Cash on Delivery confirmation with its Confirm and Cancel buttons.
- Posting order_placed twice for one order is safe — the second one is recognised and not messaged again.
Step 4 — Sign every request
Two headers turn your ingest URL from a secret address into a verified sender. Reveal the store's signing secret under Connection details → Request signing → Show signing secret.
| Header | Value |
|---|---|
| X-Wapzio-Timestamp | Current time in unix seconds. More than five minutes out and the request is refused. |
| X-Wapzio-Signature | sha256= followed by HMAC-SHA256 of "<timestamp>.<exact request body>", keyed with the signing secret. |
const body = JSON.stringify(cart);
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = "sha256=" + crypto
.createHmac("sha256", SIGNING_SECRET)
.update(`${timestamp}.${body}`)
.digest("hex");
await fetch(INGEST_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wapzio-Timestamp": timestamp,
"X-Wapzio-Signature": signature,
},
body, // the exact string that was signed
});$body = json_encode($cart);
$timestamp = (string) time();
$signature = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, SIGNING_SECRET);
// POST $body with headers:
// Content-Type: application/json
// X-Wapzio-Timestamp: $timestamp
// X-Wapzio-Signature: $signatureBuild the JSON string once, sign that string, and send that same string. Re-encoding the object for the request — a different key order, different spacing — produces a signature that cannot verify.
- 1
Check what Wapzio saw
Every response tells you where you stand, so log it while you are building:
Response { "success": true, "signature": "verified" }signature Meaning verified Correct. The store card shows Signed requests verified. not_signed The two headers were missing. Accepted for now, refused once you require signing. invalid The headers were there but did not match; signature_error says why. - 2
Then make it mandatory
Once the store card reads Signed requests verified, switch Only accept signed requests on. Unsigned posts are refused from then on.
Step 5 — Make delivery reliable
A cart event that never arrives is a reminder that never sends, and a shopper waiting on a checkout page should never wait on Wapzio. These four habits are what separate an integration that works on the demo from one that works on a bad Tuesday.
- Never block checkout on the call. Write the event to a table of your own, answer the shopper, and send it after the response — or from a queue worker.
- Retry failures with a backoff (a minute, two, four…) and keep each order's events in order, so a retried order_placed cannot land after the shipping update that followed it.
- Before sending a queued cart event, check the order is still unpaid. A cart event delivered late, after payment, starts reminders for someone who already bought.
- Set a short timeout (five seconds is plenty) and send a User-Agent your logs can recognise, for example MyShop-Wapzio/1.0. Requests with no agent at all can be refused by the API's bot protection.
Commerce → Abandoned Carts shows the cart, the shopper, and every reminder sent, skipped or failed for it. Commerce → Stores shows the last event received and whether it was signed. Between them you can tell a payload problem from a template problem without reading a single log file.