Njiwa for OpenCart

Pick the status in your own shop that means paid, sent or cancelled, WhatsApp your customers when an order reaches it, and get a message yourself when one comes in.

Start a 7 day trial See what your customer gets

What changes for your business

The phone stops ringing with the same one question

The question a small shop answers all day is the one the customer has no way of answering for themselves. Every status you move an order to is now a message on the app already open on their phone, the moment you move it: the payment is in, it has left you, it was cancelled. Nobody is typing those replies any more, and nobody is being paid to.

You keep the statuses you already use

OpenCart statuses belong to each shop, and yours may read Awaiting M-Pesa, Out for delivery or Boda sent rather than the names OpenCart ships with. So nothing is guessed. You pick, from your own list, which status means paid and which means on its way, and you go on running the shop exactly as you run it now. The settings page will not save a message that is switched on with no status behind it, and it refuses two messages pointed at one status, so you find that out there rather than from a customer who got two messages at the same instant.

You hear about a sale without watching the dashboard

The moment an order becomes real, a message reaches your own phone with the customer's name, how many items, the total, how they paid, and a link that opens that order in your OpenCart dashboard once you have signed in. Put several numbers on the list and whoever packs the orders is told at the same time as you are. It goes out on the first status an order is ever given, so a checkout somebody walked away from at the payment page never wakes you at eleven at night.

A refund explains itself before anybody rings

The two moments a customer is most likely to ring you angry are the two they hear about first, from you, before they have thought to check. A cancelled order says plainly that they have not been charged, and asks them to reply if it was not them. A refund says the money is on its way back and that banks take a few days to show it, which is the sentence that stops the second phone call.

It arrives already written, and it sounds like your shop

Every message ships with wording that works unedited, with your store's name and that order's own details filled in, so switching one on is a tick and a dropdown rather than a writing exercise. If the words are not yours, change them on the same settings page like any other setting: nobody needs a developer to change a sentence, and clearing a box is how you switch one message off and keep the rest. A shop running several stores from one OpenCart signs each message with the name of the store the order was placed in.

What your customer receives

These are the messages this integration sends out of the box, written out in full. You can change every word of them, but you do not have to: they are written to be sent as they are.

Hi Mary, thank you. Your payment for order #1043 came through and we are getting it ready. 2 x Tyre 195/65 R15 1 x Wheel balancing Total KSh 18,400 Riverside Hardware

09:24
Payment lands

Hi Mary, order #1043 is on its way to you. Thank you for shopping with Riverside Hardware.

11:06
You mark it sent

Hi Mary, we have your order #1043 for KSh 18,400. We will let you know the moment your payment comes through. Riverside Hardware

13:41
Bank transfer, still waiting

Hi Mary, we have refunded KSh 18,400 for order #1043. Banks take a few days to show it. Riverside Hardware

15:18
You refund an order

New order #1043 on Riverside Hardware. Mary Wanjiku 3 item(s), KSh 18,400 Paid by M-Pesa https://riverside.example/admin/index.php?route=sale/order/info&order_id=1043

16:52
A new order, to you

When each message goes out

Each of these is a separate switch, and every one of them is off until you turn it on. Installing this sends nothing to anybody.

What OpenCart sends, and to whom
When it happens Who hears about it
An order reaches the status you chose for Order placed, payment not in yetThe customer, told you have the order and are waiting for the money. This is the one for bank transfer, cash on delivery and anything else where the order exists before it is paid.
An order reaches the status you chose for Payment receivedThe customer, told the payment came through and you are getting the order ready. The wording that ships with it lists the items and the total.
An order reaches the status you chose for Sent, or on its wayThe customer, told it has left you. OpenCart sends its own email at the same moment; this arrives where people actually look.
An order reaches the status you chose for Order cancelledThe customer, told it was cancelled and that they were not charged, and invited to reply if that was not them.
An order reaches the status you chose for Order refundedThe customer, told the money is on its way back and that banks take a few days to show it.
The first status an order is ever given, whatever it is called in your shopYou, once per order, with a separate copy to every number in Your WhatsApp numbers. Not when the order row is written, which happens the moment somebody reaches the payment page and usually means nothing, so an abandoned checkout never wakes you up.

Why it is safe on a shop that is trading

Every message is off until you turn it on. Installing the extension sends nothing to anybody: there is no key saved, the master switch arrives off, and all six messages arrive switched off, so the first one leaves only after somebody has turned it on deliberately and chosen a status for it.

Start on a test key. A key beginning sk_test_ checks and stores every message and delivers nothing to a phone. Everything else behaves exactly as it will on the live key, real message ids included, so you can walk a test order through every status before a customer is involved. Test connection says when a test key is in force, and so does every line in the log that a test key produced.

It cannot block a checkout. Nothing is sent while the customer is being told their order went through. The handler writes one row and returns, it catches its own errors, and it deliberately returns nothing at all so that it cannot change what OpenCart hands back to the code that saved the order. A customer completes an order at the same speed whether this extension is installed, switched off, or pointed at an address that does not answer.

A customer is never messaged twice for the same thing. The claim in njiwa_message covers the life of the order, and the idempotency key covers a send that runs twice within 24 hours. That key is built from the order, the moment and the recipient together with a fingerprint of the shop's own database name and table prefix, so two OpenCart installations sharing one Njiwa account cannot claim each other's order numbers: order 1043 in one shop and order 1043 in the other are two different messages, and neither silences the other. The table survives removing the extension for the same reason: a shop that takes it off and puts it back tomorrow must not message everybody about the orders it already handled.

It cannot message a WhatsApp group. A group is addressed as something ending @g.us, and Njiwa will post to it, so anything containing an @ is refused outright wherever it was typed. One order confirmation sent to a group would go to everybody in it, from your own number.

No phone number is not an error. An order with no usable number gets a line in the log saying so, and nothing else happens. A customer who did not leave a number is normal, not a fault to be raised at the checkout.

The master switch keeps your work, and fails loudly. Turning off Send WhatsApp messages stops every send while leaving your key, your numbers, your statuses and all six wordings where they are. A message already queued when the switch went off is not silently dropped: the attempt is refused and a line in the log says how many messages were not sent and why, so a shop wondering where its messages went can find out.

The two buttons are for the admin only. Both check that you have permission to change these settings. Send me a test message acts on a POST rather than a link, so nobody can be made to press it from somewhere else, it allows one test every thirty seconds, and its wording is fixed in the code: whoever presses it supplies the recipient from their own saved numbers and nothing else. The cron address is guarded by a token and answers 403 without it.

Removing the extension removes the key. Press the red minus on the Extensions page and the settings, the API key and the event row all go. Be aware while it is installed that OpenCart keeps extension settings, this key included, in plain text in its settings table, exactly as it keeps your payment gateway keys. It has nowhere encrypted to put them.

The words are yours

Every message is plain text you can edit in your own admin, with placeholders in braces that are filled in from the order. These are the ones it knows:

  • {first_name}
  • {last_name}
  • {customer_name}
  • {order_number}
  • {order_total}
  • {order_date}
  • {order_status}
  • {payment_method}
  • {items}
  • {item_count}
  • {shop_name}
  • {order_url}
  • {admin_url}

A placeholder it does not know is removed before sending rather than posted to a customer. Empty wording sends nothing, which is how you turn one message off while leaving the event switched on.

Questions

Does it work on OpenCart 4?

No. OpenCart 4 moved and renamed nearly everything this extension touches, including the event it listens to, so it will not run there and does not pretend to. This is for OpenCart 3.0.0.0 to 3.0.3.9. OpenCart 2.x is not supported either.

Does it slow down the checkout?

No. The status change writes one row and returns; the sending happens after the shop has finished answering the customer. On PHP-FPM hosting, which is nearly all of it, the page goes back to the browser first and nobody waits at all. On older hosting the sending happens at the very end of the same request, so it can add a moment to a page, and the order is already saved by then either way. If you would rather nothing at all happened during a customer's visit, set When to send to the cron option.

Do I have to set up a cron job?

No, and most shops do not. Without one, a message Njiwa could not be reached for is picked up by the next order that changes status anywhere in the shop, which on a busy shop is a few minutes and on a quiet one can be the next morning. A cron job calling the address the settings page prints, every minute, retries on time instead and takes sending off customers' requests entirely. Keep that address to yourself: anybody who has the token in it can make your shop send what is already queued, though nothing else.

My statuses are named my own way. Will this still fire?

Yes, because it never guesses. Each moment has a dropdown of your own order statuses and you say which one means it, so Awaiting M-Pesa or Boda sent works exactly as well as the names OpenCart ships with. The alert to you needs no status at all: it goes on the first status an order is ever given, whatever that is called in your shop.

Will a customer get two messages if an order goes back through the same status?

No. Each message claims the order, the moment and the recipient in the njiwa_message table before it is sent, and there can only be one such claim, so a status reached twice sends one message. If a send does somehow run twice inside 24 hours, the idempotency key makes Njiwa replay its first answer rather than message the customer again.

Can I try the whole thing without messaging a real customer?

Yes, and it is the way in we recommend. Use the key beginning sk_test_. Every request is checked, every message is stored, real message ids come back, and nothing reaches a phone. Place a test order, move it through your statuses, and read storage/logs/njiwa.log, where a test send is marked Test key, so nothing reached WhatsApp. Then swap the key.

Nothing arrived. Where do I look?

Start at the panel at the bottom of the Connection tab. It says whether the extension is listening to status changes at all, whether the message table exists, and how many messages are waiting to be sent. Then read storage/logs/njiwa.log: every send, every refusal with its error code, and every order that had no usable phone number is in there with the order number. If a line carries a message id, Njiwa accepted the message and the console shows what happened to it next.

Has this been run on a real OpenCart shop?

Not yet. The code is complete and has been read closely, and the one blocker that review found, a call to a model method that does not exist before OpenCart 3.0.2.0 and would have killed the request half way through installing, is fixed. What is missing is an install on a live shop taking real orders. Run it on a test key first, and tell us what breaks.

What it does not do

Said plainly, so you are not surprised later

  • It has not been installed on a live OpenCart shop. The code is finished and the review it has had was a careful reading rather than an order placed on a real storefront, so treat the first week as a rehearsal and start on a test key.
  • It does not run on OpenCart 4, and it does not run on OpenCart 2.x. OpenCart 3.0.0.0 to 3.0.3.9 only.
  • It does not receive replies. Incoming WhatsApp and delivery receipts arrive as webhooks, and verifying one needs that number's signing secret, which the console does not yet show. Until it does, a receiving feature could not prove a request really came from Njiwa, so there is not one.
  • There is no screen in the admin listing what was sent. The Connection tab counts what is still waiting, and storage/logs/njiwa.log has the rest, line by line. The njiwa_message table holds what was sent and what happened to it, which is what stops a second message, rather than a second archive of your messages to keep in step with Njiwa's own.
  • It sends plain text only. There is no attaching an invoice PDF from the extension; put {order_url} in the wording and a customer with an account opens their own order. Njiwa's API does send files, so a developer can do it directly.
  • It does not run campaigns. Bulk sending to past customers is what the Njiwa console is for, on Business plans and above.
  • A message that has failed five times is left alone, and nothing tries it again. There is no button that puts it back in the queue, and the five attempts and the five minutes between them are fixed rather than settings.
  • There is one wording per message for the whole OpenCart install. {shop_name} is the name of the store the order was placed in, so a shop running several stores signs each message correctly, but the wording itself is shared, and it is not translated per language.
  • There is no digest. Every order that becomes real sends its own alert to every number on your list, so a shop taking hundreds of orders a day should probably leave that one off.
  • The log records that Njiwa accepted a message, not that a phone displayed it. Delivery and read status live in the Njiwa console.
  • {order_url} opens the order in the customer's account, which only helps customers who have one; guests are better sent your phone number. {admin_url} only knows where your dashboard is because the settings page wrote its own address down the last time you saved it, so if you rename your admin folder, save that page again.
  • Removing the extension leaves the njiwa_message table behind on purpose, so putting it back tomorrow does not message everybody all over again. Drop it by hand if you really want it gone.

From here on, it is the install

That is everything a person deciding whether to use this needs to know. What follows is for whoever puts it on your shop, and it takes about ten minutes. If that is not you, this is the point to send them the link.

What it needs

  • OpenCart 3.x, anything from 3.0.0.0 to 3.0.3.9. OpenCart 4 moved and renamed nearly everything this extension touches, including the event it listens to, so it does not run there and does not pretend to. OpenCart 2.x is not supported either.
  • PHP 7.4 or newer.
  • The PHP cURL extension. It is the only thing in the extension that opens a socket, which is how OpenCart's own payment extensions talk to the outside world as well.
  • MySQL, which OpenCart already needs. Installing creates one table, njiwa_message, next to your other OpenCart tables, with your own table prefix on the front.
  • Outbound HTTPS from your server to njiwa.upeo.ai. The extension will not follow a redirect, so it never carries your key on to a second address something in the middle pointed it at.
  • Somewhere to write in storage/logs/, which OpenCart already uses for its own log. Everything this extension does goes into njiwa.log there, and nothing else does.
  • A Njiwa account and an API key. A test key is enough to install it, set it up and rehearse the whole thing without messaging anybody.
  • A cron job, optionally. Nothing has to call the extension's address; a shop that calls it retries a message on time rather than waiting for its next order to come along.

Install it

  1. Clone it and build the archive, then install that under Extensions, Installer. The upload folder has to be at the top of the zip: OpenCart's installer copies only the entries whose path begins upload/, so an archive made from what is inside that folder installs nothing and still reports success. If you would rather use FTP, copy the three folders inside upload, which are admin, catalog and system, into your OpenCart root instead, and skip this step and the next one.
    Terminal, 3 lines
    git clone https://github.com/Upeosoft-Limited/njiwa-opencart
    cd njiwa-opencart
    zip -r njiwa.ocmod.zip upload
  2. Wait for the installer to report success, then go to Extensions, Extensions and choose Modules from the list at the top.
  3. Find Njiwa WhatsApp in that list and press the green plus. This is the step that makes the extension work at all: it adds the row to OpenCart's event table that lets the extension hear about order status changes, and it creates the njiwa_message table, which is the record of every message and the thing that stops a customer being messaged twice.
  4. Press the blue pencil next to Njiwa WhatsApp to open its settings. The whole extension is that one page, under three tabs: Connection, Messages to your customers, and The message to you.
  5. Read the panel headed How it stands, at the bottom of the Connection tab. It says whether the extension is listening to order status changes and whether the message table is there. If either is missing, saving the page puts it right.

Set it up

  1. Sign in at console.upeo.ai, open API keys and create a key. Copy the test key, the one beginning sk_test_. The console shows a key once and keeps only its fingerprint afterwards, so a lost key is replaced rather than recovered.
  2. Paste it into API key on the Connection tab and save. Leave Njiwa address exactly as it is unless you were given an address of your own. Saving takes you back to the extension list, which is how every OpenCart module behaves; press the pencil again to carry on.
  3. Press Test connection. It asks Njiwa which WhatsApp numbers your account really has, lists each one with its state, and says plainly when the key is a test key. It sends nothing. Both buttons on this page read the settings as they are saved rather than as they are on the screen, so save before you test.
  4. If your account has more than one number, put the one these messages should come from into Send from, digits only in full international form such as 254712345678. Leave it empty to use the number marked default in the console. Test connection tells you when what you typed matches no number on the account, because every message would then be refused, one at a time, for ever.
  5. Fill in Country code for local numbers only if you sell in more than one country: digits and no plus, so 254 for Kenya or 44 for the United Kingdom. Left empty, a customer's number goes to Njiwa as they typed it and is read against the country of the number you send from, which is the right answer for a shop selling in one country.
  6. Open The message to you, put your own WhatsApp number into Your WhatsApp numbers, and save. Several are allowed, separated by commas, and each one gets its own copy. Then press Send me a test message on the Connection tab: one fixed message goes to the first number in that box and nowhere near a customer, and the answer gives you Njiwa's message id or the exact reason it failed. The button allows one test every thirty seconds.
  7. Open Messages to your customers. For each moment you want, tick it and choose the status in your own shop that means it. A moment with no status chosen is not saved as switched on, and the page says so rather than letting you leave believing it works. Two moments cannot share one status either, because one status change would then send the same customer two messages at the same instant.
  8. Leave the wording alone or edit it. Every message ships with wording that works unedited, so turning one on is one click rather than one click and a writing exercise. The placeholders you can use are listed at the top of that tab with what each one means.
  9. Turn on Send WhatsApp messages at the top of the Connection tab and save. Nothing at all has been sent to a customer up to this point.
  10. Place a test order and move it through the statuses you turned on, then read storage/logs/njiwa.log. It says what was sent, to whom and Njiwa's own id for it, and a send on a test key is marked Test key, so nothing reached WhatsApp.
  11. When that reads right, paste your sk_live_ key over the test one and save. From that point the messages reach real phones, and real messages cost money.

How it works

Installing the extension writes one row into OpenCart's event table. It listens on catalog/model/checkout/order/addOrderHistory/after, which is the one place in OpenCart where an order's status genuinely changes, whether the change came from a payment extension, from the order screen or from the API. Watching anything more general, such as the order being saved, would message a customer because somebody corrected their address. That row is written and read by the extension's own SQL rather than through OpenCart's event model, because the model's methods are not the same across 3.0.x: getEventByCode only arrived in 3.0.2.0, and calling a method a model does not have ends the request outright in OpenCart, half way through installing. Reading the table is the one thing that works on every 3.0.x.

When the event fires, the extension reads the order back from the database rather than trusting the arguments it was handed, because an anti-fraud extension can change the status on the way through. An order still sitting at status 0 is ignored: that is somebody who reached the payment page and may never come back. Otherwise the status is matched against the ones you chose, the customer's phone number is worked out, the wording is rendered against the order as it is right now, and one row per message is written with INSERT IGNORE against a unique key of order, moment and recipient. Everything in that handler is wrapped so that nothing it does can throw: an order must never fail to change status over a WhatsApp message.

The queue is emptied afterwards. On PHP-FPM the finished page is handed back to the web server first and the sending happens after that, so the customer waits for nothing; elsewhere it runs at the very end of the same request, which can add a moment to a page but cannot affect the order. Either way it takes the messages it just wrote plus up to five others that are due, on purpose: a shop that could not reach Njiwa for an hour must not work through the backlog on one customer's page. The cron address the settings page prints, which carries a secret token of its own, works through up to a hundred at a time instead, because nobody is waiting on it.

Each send is a POST to /v1/messages with your key, the idempotency key and a User-Agent naming the extension and its version. It gives up after 20 seconds, and after 10 if the host will not answer at all, so nothing holds a request open. Njiwa being unreachable is not treated as a failed send: the message was never accepted, so it goes back in the queue and is tried again, up to five times in all, with five minutes between attempts so that one bad minute cannot use every attempt up at once. A refusal is different, because Njiwa has looked at the message and said no, and asking again would only be told the same thing; it stops there and the reason and the error code go in the log. A message claimed by a request that died mid-send is free again after ten minutes.

Get it

The code is public, at https://github.com/Upeosoft-Limited/njiwa-opencart. Clone it, read it, and see every line that will run on your shop before you install it. Inside is a README written for the person who runs the shop rather than for a developer. It is developed at packages/njiwa-opencart in the Njiwa repository and published from there, so the two never drift apart. The API behind it is documented at https://docs.njiwa.upeo.ai/.

You need a Njiwa account for the key. A trial gives you a live key straight away and a test key in one click, so you can install this, rehearse a whole order and only then decide.

Start a 7 day trial All nine integrations