← All articles

Voice AI engineering

Stochastic prompts, rigid banking APIs: close the IFSC and UPI payload gap

Turn messy spoken payment requests into validated, auditable banking API calls without handing the model the transaction boundary.

A voice model hands a proposed banking payload through a deterministic validation bridge before an API call

Take it with you

MarkdownFree and ungated

Banking tool-call contract template

Copy the contract, validation rules, confirmation text, and test cases into your own voice-agent review.

banking-tool-call-contract-template.md

Download Markdown

“Send five thousand to Rakesh, HDFC, that Pune branch. You have it already.”

The model hears a payment request. The banking API sees missing fields, a name collision, and an amount that should never move without a fresh confirmation. Those are not the same object. Pretending they are is how a pleasant demo turns into an incident review.

Voxeval has a blunt view here: an LLM may propose a transaction. It does not get to define one.

This article shows where to put the hard boundary, how to handle IFSC and UPI entities, what to log, and which failures should block a release. The payoff is boring in the best possible way. A caller can speak naturally while the money-moving system stays exact.

The prompt did its job. The payload still failed.

Language models vary. That is part of why they can handle a caller saying “five K,” “paanch hazaar,” or “the same amount as last month.” A bank API does not want that flexibility. It wants an integer in paise, a known beneficiary ID, an allowed rail, and a request that can be safely retried.

Here is a fictional example, not a real customer story.

A caller in Pune says, “Rakesh ko five thousand kar do, HDFC Baner wala.” The agent produces this:

{
  "beneficiary": "Rakesh",
  "amount": "five thousand",
  "bank": "HDFC Baner"
}

Looks reasonable. It is unusable.

Which Rakesh? Is “five thousand” rupees or something the model inferred from prior context? Does the beneficiary already exist? Is the IFSC attached to the saved beneficiary, or did the model make one up from a branch name? Can this request be replayed if the network drops after the debit?

The agent needs another layer between conversation and action.

A deterministic boundary between spoken intent and a banking API

The model proposes. Code disposes.

We use five stages for a money-moving tool call.

  1. Extract a proposal. The model can return beneficiary_reference, amount_text, currency, rail, and whatever evidence came from the caller’s words. No API-ready claim yet.
  2. Resolve against owned data. Code looks up saved beneficiaries, permitted accounts, bank records, limits, and the current session. A fuzzy branch phrase may produce candidates. It must not silently pick one.
  3. Validate. Exact types, allowed values, checksum or format rules where applicable, account state, transaction limits, fraud controls, and policy checks all run outside the prompt.
  4. Read back the resolved instruction. “₹5,000 to Rakesh Sharma, beneficiary ending 1842, by IMPS. Shall I continue?” The confirmation is built from the resolved object, not from the model’s earlier prose.
  5. Commit once. The backend accepts a short-lived confirmation token and an idempotency key. A retry returns the prior result instead of sending the money again.

That last point gets missed. Mobile calls break. SIP legs reset. A tool result may arrive after the agent has already retried. If the payment endpoint is not idempotent, a voice glitch can become a duplicate debit.

IFSC is not a creative-writing prompt

An IFSC is eleven characters with a defined structure. UPI IDs have their own shape. Amounts need a currency and a fixed numeric representation. Phone numbers, account suffixes, and beneficiary nicknames each need separate treatment.

Do not ask one giant prompt to “normalize all Indian payment details.” Split the work.

  • IFSC. Read it back in chunks, with explicit letters and digits.

  • A caller says “S as in Surat,” then changes it to “F as in Faridabad.” Keep both observations and the correction event. The final field should come from a deterministic resolver or a verified re-entry, not from whichever token had the highest confidence first.

  • “Same UPI as before?” Maybe. Only if the signed-in customer has one matching saved handle and policy allows reuse.

A schema helps, but schema conformance is only the outer fence. Structured output can make sure amount_minor is an integer. It cannot prove the amount is what the caller meant. It can require an IFSC-shaped string. It cannot tell you whether that branch exists or belongs to the intended payee.

Shape first. Meaning next.

Build two objects, not one

Keep the model’s observation separate from the transaction command.

{
  "observed": {
    "amount_phrase": "five thousand",
    "beneficiary_phrase": "Rakesh HDFC Baner",
    "language_sequence": ["en", "hi", "en"]
  },
  "resolved": {
    "amount_minor": 500000,
    "currency": "INR",
    "beneficiary_id": "ben_1842",
    "rail": "IMPS"
  },
  "evidence": {
    "beneficiary_match": "single_saved_match",
    "confirmation_state": "pending"
  }
}

The observed object is evidence. The resolved object is a candidate instruction. Only a server-side payment service can turn that candidate into a committed action.

Why keep both? Because when the caller complains, “I said fifteen, not fifty,” you need to inspect what was heard, how it was normalized, what was read back, and what the customer confirmed. A final JSON blob cannot tell that story.

Confirmation should be awkward on purpose

Fast conversation feels good until money is involved. Then a little friction is useful.

The read-back needs the amount, recipient identity, rail when it affects timing or fees, and a clear action phrase. Avoid “Should I do that?” after four conversational turns. “That” has become foggy.

Also avoid letting an interruption count as approval. If the agent asks for confirmation and the caller says, “Wait, which Rakesh?”, that is a question. It is not a yes with extra words.

At Voxeval, we would test confirmation as its own state machine:

  • “Yes.”

  • “Yes, but make it four thousand.” The amount changed. Start confirmation again.

  • Background speech from a television says “haan.” No commit.

  • “Do it tomorrow.” That is scheduling intent, not approval for a transfer now.

  • Silence?

Nothing moves.

Logs can become a second breach

Teams often secure the bank endpoint and then pour raw transcripts, UPI handles, tokens, and tool arguments into a general logging system. That creates a copy of sensitive data with weaker access rules.

OWASP’s logging guidance calls out data that should usually be removed, masked, sanitized, hashed, or encrypted. For a voice payment path, we would log state transitions and evidence references, not a full dump of secrets.

Useful:

  • beneficiary_match=single_saved_match
  • confirmation_version=3
  • policy_result=allowed
  • tool_call_id and idempotency_key_hash
  • the final outcome code

Risky by default:

  • full account numbers
  • raw authentication secrets
  • an OTP spoken into the call
  • complete tool payloads copied into an analyst dashboard

Debugging matters. So does not creating a searchable pile of customer credentials.

Test the seam where certainty changes

Most teams test the model and the API separately. The dangerous part is the seam between them.

Run cases where:

  • the model returns valid JSON with the wrong beneficiary;
  • the IFSC has the right shape but does not match the saved bank record;
  • the caller corrects one digit during read-back;
  • the payment service times out after committing;
  • the same tool call arrives twice;
  • an old confirmation token is replayed;
  • the caller switches from UPI to IMPS halfway through approval;
  • a policy rule changes between proposal and commit.

And inspect the business state after every run. A polite final sentence is not proof that the debit happened once, to the right person, for the confirmed amount.

The release rule

Do not ship a voice payment flow because its prompts are “consistent.” Ship when malformed, ambiguous, stale, duplicated, and adversarial payloads fail closed at the server boundary.

The downloadable banking tool-call contract template gives your product, engineering, risk, and QA teams one document to mark up together. Replace the fictional fields with your real rails and policies. Then try hard to break it.

The model gets a suggestion box. The transaction service keeps the keys.

Reference list

Sources

  1. OpenAI Structured Outputs
  2. OWASP Logging Cheat Sheet
  3. Twilio SIGNAL 2026 product announcements
  4. RBI payment systems information