Build notes
Cinco Salvaguardas en las Solicitudes Automáticas de Pago por WhatsApp
A customer asks about a chain. The shop sends three photos, quotes a price, and she says she will take it. Then the conversation stops. Following up is somebody's job, which means it is nobody's job, and on Thursday she buys it somewhere else. Nothing went wrong that anyone could point at.
That is the gap this note is about. She was ready. The shop was willing. The software knew both things and did nothing with either, because knowing and acting were never connected.
In short
What it does: a drip step, a behaviour rule, a milestone, a completed WhatsApp Flow, or a customer typing "how do I pay" can all trigger a real payment request in the thread, priced from the shop's own catalog and settling into the shop's own gateway account.
The guards: consent checked and failing closed, a duplicate-ask guard, an oversell guard that counts stock committed across every open order, an approved-template fallback outside the 24-hour service window, and a settlement check that refuses to read a part payment as a completed sale.
What we found along the way: a checkout the gateway refused used to leave the order in a state the duplicate-ask guard read as "still open," so the software answered "already requested" forever about someone who had never once been asked.
So we connected them. A drip sequence step, a behaviour rule, a milestone, a completed Flow, or the customer simply typing "how do I pay" can now produce a real payment request in the thread, priced from the shop's own product library, settling into the shop's own gateway account.
Building that took about a day. Everything since has been spent on what it must refuse to do, and that is the part worth writing down. Automation that asks for money is a category of software where the failure mode is not a bug report. It is a customer who feels harassed, or a shop that ships a ring it was never paid for.
One path, not five
Before any of the refusals, one structural decision makes them possible. All five surfaces call the same function. Not "similar logic in five places", one function.
This sounds like tidiness and is not. Guards written per-surface protect the surface whose author was thinking about them that day. A consent check on the sequence step and not on the Flow completion is not a partial win, it is a false sense of one. With a single path, a guard added on a Tuesday afternoon applies to a Flow somebody built in March.
The five refusals
1. It will not ask someone who said stop
cost to us: a database blip means nobody gets asked, and that revenue is simply gone
Consent is checked before anything is sent, which is unremarkable. What matters is the direction it fails in. If the consent lookup itself errors, the request is refused. Not logged and continued. Refused.
The tempting alternative is to treat an unreadable consent record as no objection, because the sale is right there and the error is probably transient. That choice converts every database hiccup into a message somebody did not agree to receive, on a channel where the regulator and the platform both take that seriously, and the person who finds out is the customer.
2. It will not ask twice for the same thing
cost to us: a genuinely stalled request needs a human to clear it
An automation that runs nightly against a segment will happily ask the same person for the same item every night until they pay or block you. So an open request for a given contact and item blocks a second one.
This guard is also where we found the more interesting bug. A checkout the gateway refused used to leave the order stranded in its initial state, which the duplicate check counted as an open request. The result was software that answered "already requested" forever, about a person who had never once been asked. The state machine now has an explicit path for a refused checkout, so a failure is a failure rather than a permanent silence.
3. It will not sell what is not there
cost to us: a shop with untracked stock has to say so explicitly
Stock is decremented when an order is fulfilled, which is correct: a made-to-order piece can be days from dispatch. But it means a plain "is there stock" check passes repeatedly for the same unit. We measured it before fixing it. One item in stock, and the system was willing to ask fifty people to pay for it.
Fifty real payments, into the shop's real account, for one ring. The shop then gets to explain itself to forty-nine people and refund them, having done nothing wrong except trust its software.
The fix counts what is already promised: every open order containing that product has committed its units even though nothing has left the shelf. No reservation state was introduced, deliberately, because a reservation is a thing that can leak a hold when an order is abandoned and then quietly hide inventory nobody can sell.
4. It will not shout through a closed window
cost to us: a template has to be approved by Meta before this works at all
WhatsApp allows freeform messages only inside 24 hours of the customer's last message. Automation, by its
nature, usually fires outside that. Inside the window we send the native interactive payment card, Meta's
order_details message with a UPI intent, priced in INR because that combination is India-only.
Outside it, an approved template.
There is a real constraint underneath that is worth knowing if you are building this yourself. Meta fixes a template's button URL at approval time, apart from a single trailing variable, so a template physically cannot carry an arbitrary gateway checkout link. We send a stable redirect ending in a per-order token, and resolve that token to whatever link the order actually has. The side effect is that the gateway becomes swappable without touching the approved template.
5. It will not call a deposit a sale
cost to us: an order sits open and somebody has to chase it
The links this product creates cannot be part-paid. But a shop can paste in a link it made by hand in its own gateway dashboard, where part payment can be switched on, and that gateway's notifications reach us regardless of who created the link.
Our webhook used to read any successful payment event as full settlement. On a ₹2,500 order, a ₹500 advance would have marked it paid, sent the customer a receipt, run fulfilment, decremented stock and closed the order. The shop packs a ring, having been paid a fifth of it, and finds out weeks later.
It now compares what arrived against what is owed. A shortfall leaves the order exactly where it was, records the real numbers in the ledger, and messages the shop with the opposite instruction from a sale:
Part payment received: ₹500.00 of ₹2,500.00
Still outstanding: ₹2,000.00
Customer: 9198xxxxxxx
This order is NOT paid and has not been fulfilled.
Collect the balance before sending anything.
And the customer receives nothing at all, because a receipt is a promise the shop has not yet been paid to keep.
The half of this nobody demos
The refusals above are about not doing harm. The quieter problem is a system that is technically correct and still leaves people worse off, which is where two of the sweeps came from.
| What used to happen | What it feels like from the other side |
|---|---|
| A customer who had part-paid was chased by the abandoned-cart reminder, because the poller read a part payment as no payment. | You send money, and the shop's system asks you to pay. You are now arguing with a robot about whether you paid. |
| An order past its deadline was expired by a cron, regardless of whether money had arrived against it. | Your deposit is attached to an order the shop's own software has written off. |
| A payment landed and the buyer was thanked, while the person who has to pack the thing was told nothing. | You paid on Tuesday. It ships when somebody happens to open a dashboard. |
All three are the same shape, and it is the shape this codebase keeps rediscovering: a feature reports success while doing nothing useful for a person. Fixing them is unglamorous. It is also most of the difference between software that collects payments and software a shop can actually run on.
What it cost, stated plainly
A decision with no cost is a slogan, so here are the ones we are carrying.
Unattended money requests are gated behind a second capability, separate from ordinary commerce, and it is off by default. A shop that wants automation to ask on its behalf has to turn that on deliberately. That is friction we chose, on the grounds that "our software started asking my customers for money" should never be something anyone discovers.
We also refuse partial payments rather than half-supporting them. Supporting instalments properly means deciding what a balance reminder looks like, what happens when it is never paid, and what the shop sees in between. Until that exists, an underpayment is an anomaly we stop and hand to a human, and we would rather state that than let the behaviour be whatever the code happens to do.
And the gateway is the shop's own. The money settles into their account, at their rates, under their relationship with their provider. We are not in the payment path, which also means we cannot fix a payment problem for you, only tell you plainly that one happened.
What is still open
Refunds run through the gateway dashboard today, not through us. Disputes and chargebacks are unhandled. Partial payments are refused rather than supported, as above. The message that tells a buyer their order has shipped is waiting on template approval at Meta, and until it clears, a dispatch days after payment usually cannot be delivered at all, because the service window has closed.
That last one is a genuine limitation of the channel, and we would rather name it than let you find it.
What a shop should get from this is boring. Your software should not ask a customer for money they have already sent, should not sell a ring twice, and should not tell you an order is paid when a fifth of it is. You should not have to check. That is the same standard we are trying to hold with our own customers, and it is built the same way: by refusing to let small unspoken gaps accumulate.
Get the next one on WhatsApp
Message the line and type NOTES. The latest one comes straight back, in the same thread, from the number that sends everything else. Reply STOP whenever you like and it stops.
Send NOTES on WhatsAppAsk our WhatsApp number what it costs
The most honest demo we have is the purchase itself. Message the line and type pricing: you will get our live catalog as a WhatsApp list, the real number of slots left, and a payment link on whatever you tap. Every guard described above is running on that conversation while you have it.
+91 97893 77634 · you message first, so nothing reaches you without your say-so.
Questions people ask about this
Can WhatsApp automation request a payment automatically?
Yes. In Klaros a drip sequence step, a behaviour rule, a milestone, a completed Flow, or a customer asking how to pay can all trigger a payment request in the thread. All five surfaces call one code path, so a guard added in that path applies everywhere rather than to whichever surface the author remembered.
What stops automated payment requests from becoming spam?
Unattended payment requests are gated by a separate capability from ordinary commerce, and it is off by default. Beyond that the send path checks consent and fails closed, meaning a database error results in no message rather than a message sent without a consent record. It also refuses to ask the same contact twice for the same item while an earlier request is still open.
Does Klaros take a percentage of payments collected over WhatsApp?
No. A workspace connects its own payment gateway credentials and the money settles directly into its own account. Klaros never sits in the payment path. The adapter that would settle a customer's sale through the vendor's own gateway is explicitly refused for every workspace other than the vendor's own.
Can a payment request be sent outside the WhatsApp 24-hour service window?
Yes, using an approved template. Meta fixes a template's button URL at approval time, with the exception of a single trailing variable, so a template cannot carry an arbitrary gateway checkout link. Klaros sends a stable redirect URL ending in a per-order token, and resolves that token to whichever gateway link the order actually has. Inside an open window it sends the native interactive payment card instead.
What happens if a customer pays only part of what they owe?
The order is not completed. Klaros does not offer partial payments, so an underpayment is treated as an anomaly rather than a sale: the order stays open, the shortfall is recorded in the order ledger, the shop is alerted with the outstanding balance and told not to dispatch, and the buyer receives no receipt. The order is also protected from being chased as an abandoned cart or expired by a deadline, because money has already arrived against it.
Written 4 August 2026. We append when the facts change. Related: the belief this comes from, the WhatsApp payments implementation details, what we found in our own billing, the consent check this note's first guard depends on, all build notes.