Njiwa for ERPNext and Frappe

WhatsApp your customers when you submit an invoice, post a payment or send goods out, and get a message yourself when an order comes in.

Start a 7 day trial See what your customer gets

What changes for your business

The calls asking where things are stop coming

Nearly everything a sales or accounts desk answers in a day is one question from somebody who has no way of finding the answer themselves: has the invoice been raised, did the payment land, have the goods gone out. Each of those is a document you already submit in ERPNext, and the customer now hears about it as you submit it, on the app already open on their phone. It matters most on the delivery: ERPNext tells a customer nothing at all when goods leave, and until now somebody had to remember to.

A receipt goes out the moment the payment is posted

Whoever posts the Payment Entry no longer has to write back and say the money arrived. The customer gets a receipt naming the amount and your own reference, which answers did you get my payment before it is asked and gives them something to file. Only money coming in from a customer counts: a supplier payment and an internal transfer are Payment Entries too, and neither is ever touched.

A cancellation or a credit note does not turn into an argument

The two moments a customer is most likely to ring in cross are the two they now hear about first. A cancelled invoice says plainly that there is nothing to pay on it and asks them to say so if they have already paid. A credit note says what has been put back and that money returning to a bank account takes a few days to show, which is the sentence that stops the second call.

You hear that an order is real without watching the desk

One message to your own number, with the customer, the number of items, the total and a link that opens the document on your phone. You decide which document means an order is real in your business, a Sales Order where you quote and confirm or a Sales Invoice where you sell over a counter, so an order that becomes both still tells you once and a return never tells you at all. Everybody whose number is on the list gets their own copy.

It cannot stop you invoicing

This is the promise the app is built around, because a business system that cannot raise an invoice is a business that has stopped. Nothing is sent while somebody is waiting for a document to save: it is submitted and committed first, and a background worker sends afterwards, so Njiwa being slow, unreachable or switched off cannot hold up a submit or a cancel. The same care covers going live. During a data import, a migration or a patch it sends nothing at all, so five thousand historical invoices loaded into a new site reach nobody.

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, here is invoice SINV-1043, dated 04-09-2026, for KSh 18,400. 2 x Tyre 195/65 R15 1 x Wheel balancing Riverside Hardware

09:24
You submit the invoice

Hi Mary, thank you. We have received KSh 18,400 from you and put it against your account. Our reference is PE-1043. Riverside Hardware

11:06
Payment lands

Hi Mary, your goods are on the way. Our delivery note is DN-1043. 2 x Tyre 195/65 R15 1 x Wheel balancing Riverside Hardware

13:41
Goods leave the store

Hi Mary, we have credited KSh 18,400 back to you on SINV-1044. Where money is going back to a bank account, it takes a few days to show. Riverside Hardware

15:18
You credit money back

New order SO-1043 on Riverside Hardware. Mary Wanjiku 3 item(s), KSh 18,400 https://riverside.example/app/sales-order/SO-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 ERPNext and Frappe sends, and to whom
When it happens Who hears about it
A Sales Order is submittedThe customer, told you have the order and what it comes to, with the items listed. This is the one for shops that confirm an order before they invoice it.
A Sales Order is cancelledThe customer, told the order has been cancelled and invited to reply if that is not what they expected.
A Sales Invoice is submittedThe customer, told the invoice number, its date and the amount. A Sales Invoice with Is Return ticked is a credit note, and it has an event of its own rather than being sent as an invoice.
A Sales Invoice is cancelledThe customer, told there is nothing to pay on it after all, and asked to reply if they have already paid it.
A credit note is submittedThe customer, told what has been credited back to them, and that money going to a bank account takes a few days to show.
A Payment Entry is submittedThe customer, sent a receipt naming the amount and your reference. Only money coming in from a customer: a supplier payment and an internal transfer are Payment Entries too, and neither is ever touched.
A Payment Entry is cancelledThe customer, told the receipt has been reversed in your books. Leave it off unless you want customers told about your corrections.
A Delivery Note is submittedThe customer, told the goods are on the way, with the delivery note number and the items. A sales return does not fire it.
A Delivery Note is cancelledThe customer, told a delivery they were expecting today is not coming.
A Sales Order or a Sales Invoice is submitted, whichever you nominateYou, on every number listed in Your WhatsApp numbers, one copy each. You choose which document means an order is real in your shop, so an order that becomes both tells you once, and a return never tells you at all.

Why it is safe on a shop that is trading

Every event is off until you turn it on. Installing the app and opening the settings sends nothing to anybody. All ten events arrive switched off and there is no key saved, so the first message leaves only after somebody has pasted a key and ticked a box on purpose. On a bench shared with other apps that matters twice over, because the alternative is messaging somebody else's customers.

Start on a test key. A key beginning sk_test_ checks and stores every message and delivers nothing to a phone. Every part of the app behaves as it will on the live key, timeline notes and message ids included, so you can submit a real invoice and read the whole trail before a customer is ever involved.

It cannot stop a submit or a cancel. Everything at that moment is inside one try, the send happens on a worker, and the job is only queued once the transaction has committed. A shop must never be unable to invoice because WhatsApp is slow, so a failure to arrange a message is written to the Error Log rather than raised at the person saving the document.

Nothing goes out during an import or a migration. The app refuses to send while Frappe is importing data, migrating, running a patch, installing an app, running the setup wizard or running tests. Historical documents loaded into a new site message nobody.

A customer is never messaged twice for the same thing. The Njiwa Sent Message row is written inside the transaction that is submitting the document, one per document, per event, per recipient, and its name is the Idempotency-Key the send carries. The row stops a second job being created; the key stops a job that runs twice turning into two messages, because Njiwa honours it for 24 hours and replays the first answer instead of messaging again. The row outlives the key, so a document that somehow reaches the same moment twice, months apart, still sends once. The recipient is part of that key, so a new order alert going to three of your own numbers stays three messages rather than collapsing into one, and the site name is hashed into it as well, so several sites on one bench sharing one Njiwa account do not collide over SINV-00001.

Two numbers in one box do not become one wrong number. People keep a second number in a Customer's Mobile No, written 0712345678 / 0722000111 or with a comma between them. That field is split on the separator before it is read and the first piece that is really a phone number is the one used, rather than the punctuation being stripped out to leave a run of digits belonging to nobody. Your own numbers are split the same way, except that every one of them is sent to and a number typed twice is sent to once.

It cannot message a WhatsApp group. Every piece of what is typed into a number box, and everything read off a Customer or a Contact, is reduced to digits, between 7 and 15 of them, before it is used. A group address is not digits, so it cannot survive the trip. One submitted invoice can never become a message to hundreds of people from your own number.

No phone number is not an error. A customer without a number gets a note on the document saying no message was sent and why, and nothing else happens. It does not stop the document and it does not appear as a fault.

Both test buttons are for System Managers only. The test send is POST only, so a browser cannot be lured into firing it by following a link; it is rate limited to ten an hour from one address, and its wording is fixed in the code, so the operator supplies the recipient and nothing else. Test connection is guarded by the role alone, and it asks a question rather than sending anything. The master switch, when it is off, refuses a send loudly rather than quietly doing nothing, so you find out rather than wonder.

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

Can a WhatsApp message stop an invoice being submitted or cancelled?

No, and this is the promise the app is built around. Everything it does at that moment is wrapped, the send happens on a background worker, and the job is only queued after the transaction has committed. If Njiwa is down, or the key is wrong, or the wording will not render, the document still submits and the problem is written to the Error Log.

I cancelled an invoice and the customer heard about it three times.

Cancelling cascades in ERPNext: cancelling a Sales Invoice usually means cancelling its Delivery Note and its Payment Entry first, and each of those is its own moment with its own event. Switch on the one cancellation your customers actually recognise, usually the invoice, and leave the other two off.

Which number does it send to, and what if there is none?

The Customer's Mobile No first, then the contact number on the document, then the Contact record behind it including its extra numbers, and the first usable number wins. A box holding two numbers, 0712345678 / 0722000111, is split rather than run together, and the first of the two that is really a number is the one used. If none of them holds a phone number, nothing is sent, nothing fails, and a note appears on the document's timeline saying there was no number for this customer.

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, the timeline notes are written with real message ids, and nothing reaches a phone. Submit a test invoice, read the trail, then swap the key.

The timeline says it was sent, but nothing arrived.

Read the note. If it says the key was a test key, the message was stored and deliberately not delivered. If it carries a message id, Njiwa accepted it and the console shows what happened next, including whether the number was reachable. If it carries a reason instead, that is the reason, and the Njiwa Sent Message record holds the same line with the failure written on it.

How do I upgrade it later?

Not with install-app. Frappe answers that a site already has the app and stops, which leaves the code you just copied in unmigrated. Replace the Python package inside apps/njiwa_frappe, then run bench migrate, bench clear-cache and bench restart. Migrate is the line that applies the new version: it syncs the doctype so a new field reaches the database, and it relinks the app's assets so the desk icon keeps working.

Do I need approval from Meta before I can use this?

Not for the Web rail, which works with a WhatsApp number you already own and needs no application. If you have been through the Cloud API route instead, that works too. The app neither knows nor cares which rail a number is on: it posts to the same endpoint, and Njiwa sends over whichever rail that number uses.

Is it ready for my live site?

Here is the honest answer. The app is complete, and the events, settings and placeholders on this page are the ones in the code, but the last close review of it did not finish, and no real order or invoice at a real business has been through it. Install it on a test site, run it on a test key, and submit a few of each document you care about before the live key goes in.

What it does not do

Said plainly, so you are not surprised later

  • It has not been run on a live ERPNext or Frappe site, and the last close review of it did not return a verdict. The app is complete and everything on this page was read out of its code, and neither of those is the same thing as a real invoice at a real business. Treat the first week as a rehearsal and start on a test key.
  • 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.
  • It does not keep a copy of your messages. The Njiwa Sent Message record holds who was messaged, about which document, when, with what outcome and Njiwa's message id, and no message text. Njiwa already stores the messages themselves, and a second copy in your database is a second thing to keep in step.
  • The events send plain text only. Nothing attaches the invoice PDF to the message, so put {order_url} in the wording if your customers have portal logins. Njiwa's API does send files, and njiwa_frappe.api.send takes a document, an image, a video or a location, so a developer can do it in a few lines.
  • It watches four doctypes. Sales Order, Sales Invoice, Payment Entry and Delivery Note, and nothing else. There is no ready-made event for a Quotation, a Purchase Order, a Material Request, a Stock Entry, a Lead or a Job Card.
  • There are no scheduled messages and no scheduled jobs at all. No payment reminder before a due date, no statement at month end, no follow up on a quotation. Everything is sent at the moment a document is submitted or cancelled.
  • There is one wording per event for the whole site. No wording per customer, per company, per territory or per language, and no way to switch an event on for some customers and off for others.
  • There is no opt out field on a Customer. A customer who asks to stop hearing from you has to have their number taken off the record, or the event turned off for everybody.
  • A local number is read against your own sending number's country, not against the customer's address, because an ERPNext document does not reliably carry the customer's country. If you sell across borders, store customer numbers in full international form.
  • A send that fails is not retried by the app. Njiwa retries a message it has accepted; a message it never accepted is left alone, on the grounds that a message going out twice is worse than one that has to be sent again by hand.
  • It is not on a marketplace. Frappe has no app store to list it in, so installing and upgrading are both bench commands rather than a button somebody presses, and nobody tells you when a new version lands.

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

  • Frappe v15 or newer. The app puts its icon on the /apps screen through the hook that version added, and its workspace is written in the block format v15 introduced.
  • ERPNext, for the ready-made events. They watch Sales Order, Sales Invoice, Payment Entry and Delivery Note, which are ERPNext's own documents. On a Frappe site without ERPNext the app still installs and njiwa_frappe.api.send still sends; there is simply nothing for the events to watch.
  • Python 3.10 or newer, which is what the app declares and what a supported bench already runs.
  • No extra Python packages at all. The app depends on nothing beyond what Frappe already ships, because an app that adds dependencies to a bench is an app that can break the other apps on it.
  • Background workers running, which an ordinary bench already does. Messages are put on the short queue.
  • Outbound HTTPS from your bench to njiwa.upeo.ai. The address is refused unless it starts https://, because the API key rides on every request and that key is the whole authority over your WhatsApp account.
  • A Njiwa account and an API key. A test key is enough to install, configure and rehearse the whole thing.
  • A System Manager account. The settings, the workspace, the app icon and both test buttons are open to that role and to nobody else.

Install it

  1. Fetch it into your bench. The repository is public, so this needs no key, no token and no account, and one command does the four things a copied folder would have needed by hand: it clones the app, installs it into the bench's own Python environment, adds it to sites/apps.txt, and builds its assets, which is what puts the desk icon under /assets/njiwa_frappe/.
    Terminal, 2 lines
    cd ~/frappe-bench
    bench get-app https://github.com/Upeosoft-Limited/njiwa_frappe
  2. Install it on the site.
    Terminal
    bench --site yoursite.local install-app njiwa_frappe
  3. Clear what the desk has already cached. The Njiwa icon then appears on the /apps screen for anybody who is a System Manager.
    Terminal
    bench --site yoursite.local clear-cache

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, so a lost key is replaced rather than recovered.
  2. In the desk, open the Njiwa icon on the /apps screen, which opens the Njiwa workspace, and follow the Njiwa Settings shortcut. Typing Njiwa Settings into the awesome bar gets you to the same form.
  3. Paste the key into API key and press Save. Leave Njiwa address exactly as it is unless you were given an address of your own. The form refuses anything that does not start https://, and it refuses anything that does not look like a Njiwa key, which catches the paste that went wrong rather than the key that is merely wrong.
  4. Press Test connection. It reads the saved key, lists the WhatsApp numbers the account really has with the state of each, and says plainly when the key is a test key. It asks a question and sends nothing. Both buttons read the saved settings rather than the form on screen, so save first or they will tell you to.
  5. If the 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 warns you when what you typed matches no number on the account, because every send would then be refused.
  6. Press Send test message and give it your own number, written however you have it. One short fixed message goes out and the answer is the real outcome rather than the word queued: the message id, the status, and which of your numbers it went from. You cannot choose the wording, and the send carries no idempotency key, so pressing Send twice sends twice.
  7. Open the Messages to your customers tab, tick the events you want, and read the wording underneath each one. Every event ships with wording that works unedited, so turning one on is one click rather than one click and a writing exercise. Clearing a box is how you turn one message off while leaving the event on.
  8. Open The message to you tab if you want to hear about new orders. Tick it, say whether a Sales Order or a Sales Invoice is what counts as an order being real in your shop, and put your own numbers in, separated by commas. Everybody listed gets their own copy.
  9. Rehearse it. Put your own mobile number on a test Customer, submit a Sales Invoice for them, and open the invoice: the timeline records what went where with Njiwa's message id, or the reason nothing went.
  10. When it all reads right, swap the sk_test_ key for your sk_live_ key and save. From that point the messages reach real phones and count against your plan.

How it works

The app hooks four ERPNext doctypes, Sales Order, Sales Invoice, Payment Entry and Delivery Note, at two moments each: on_submit and on_cancel. There is deliberately no on_update and no after_save anywhere in it. Those fire when somebody corrects a postcode, and a customer who gets a WhatsApp message because a clerk fixed an address learns to ignore all of them. The cancel events are listed one per document type rather than as a single blanket cancellation, because cancelling in ERPNext cascades and one blanket event would tell the same customer about one cancellation three times.

At one of those moments the app checks, in order: whether this is an import or a migration, whether the master switch is on, whether this particular event is switched on, and whether the document is really that moment, so that a credit note is not sent as an invoice and a supplier payment is not sent as a customer receipt. Then it finds the customer's number, writes the Njiwa Sent Message row inside the running transaction, and queues the send on the short queue for after the commit. On a site that has switched nothing on, the whole cost of these hooks is one cached settings read per submit.

The worker renders your wording against the document, trims it to 4000 characters so it fits what WhatsApp accepts, and posts it to /v1/messages with your key, the Idempotency-Key and a User-Agent naming the app and its version. It never waits for delivery, whatever Wait for the result says, because nobody is watching that send. The request has a 30 second timeout so a stuck connection cannot hold a worker. Njiwa's answer becomes the timeline note and the status on the row; a refusal arrives with a stable machine readable code and a sentence a person can act on, and both are written to the site's Error Log.

None of that fences your own code out. from njiwa_frappe.api import send is one import and one call, it takes text, an image, a PDF or a location, and it ignores every setting on the events tabs. Put it in frappe.enqueue whenever a person is waiting for a screen, and give it an idempotency_key for anything a retry could duplicate. It is deliberately not whitelisted: anything that can send a WhatsApp message on your behalf should be code you wrote, not a URL somebody can find.

Get it

The code is public, at https://github.com/Upeosoft-Limited/njiwa_frappe. 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-frappe 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