Kaarya’s whole pitch is that your team updates work from WhatsApp. Before any of that can happen, someone has to link their number. That flow starts with a 6-digit OTP, sent over WhatsApp, to prove the number is theirs.
We built it. We sent the OTP as a plain, free-form text message. Every test passed. Then we tried it on a real number that had never talked to us before, and it failed every time.
Two different guards
WhatsApp’s Business Platform only allows free-form messages inside a 24-hour “service window” — the window opens when the user messages the business first, and it closes 24 hours after their last message. Outside that window, a free-form message gets rejected with error 131047. To reach someone outside the window, you have to use a pre-approved message template instead.
Here’s the part that should have been obvious sooner: a number that has never messaged you is, by definition, outside any window. There is no window to be inside, because a window is a record of a conversation, and there hasn’t been one yet. So the very first message in our funnel — the OTP that starts the whole relationship — was outside the one condition that would let it through as free text. It could never have been delivered, no matter what we did to the request.
We even had a flag in the code, bypassWindow, and someone had set it here, deliberately, thinking that would cover it. It ran. It didn’t throw. And it still couldn’t work, because that flag skips our window check — a guard we wrote, sitting in our own code, checking our own state. It has no reach into Meta’s servers, where the real window check happens. We were bypassing a guard that was never the one stopping us.
Why the tests didn’t catch it
Every test we had used a mock WhatsApp provider. The mock accepted the free-form OTP message without complaint, because we told it to accept OTP messages — that’s what a mock does. It plays back the behavior you coded into it, which is close to the behavior you assumed happens.
That’s the actual lesson here, and it’s worth sitting with: an integration test against a mock tells you your code agrees with your own assumptions about the world. It cannot tell you whether those assumptions are correct. Our assumption was “OTP delivery works like any other message.” The mock happily confirmed that assumption fifteen times over. Nothing about a green test suite touches whether the assumption underneath it holds against the real system.
The fix, and what it broke
The fix is one line in spirit: send the OTP as an AUTHENTICATION-category template instead of free-form text. Templates in that category are pre-approved for exactly this — reaching a user for the first time to authenticate them — and they aren’t subject to the service-window restriction the same way.
The line itself was small. The blast radius wasn’t. Fifteen tests across seven files broke, and all fifteen went through one shared test helper that had been quietly scraping the OTP code out of the body of a text message. The sender had changed shape — from a free-form message to a templated one with parameters — and the helper was still reading a text body that no longer existed in the same form. It wasn’t seven separate bugs. It was one assumption, baked into one helper, referenced from seven places, and none of those seven places knew it.
Nothing here was exotic. WhatsApp’s rules are documented, the error code is specific, and the fix is a category change on one API call. What made it worth writing down is how far a passing test suite can carry you before you notice it was never testing the thing you needed to know.