Skip to content

The decision object

One object, the same in your app and in your event stream. Your app branches on one field and shows one message; your fraud team, your analysts and your auditors read the rest. Nobody sees an action without the reasons behind it.

Names and fields are illustrative and agreed in the sandbox before any contract.

On this page

Annotated

The decision at Ananya’s payment: ₹96,000 to a payee added 3 minutes earlier, 41 minutes into a call, with her screen shared. The object below is what the SDK returned to the app; the same object reached the FRM as an event.

The decision

decision.json

{
  "decision_id": "dec_7q2_118",
  "event": "payment.initiate",
  "session": "ses_8f2c91ab",
  "action": "hold",
  "reasons": [
    { "code": "ACTIVE_CALL", "module": "scam" },
    { "code": "SCREEN_SHARED", "module": "device" },
    { "code": "DICTATION_PAUSES", "module": "behaviour" },
    { "code": "PAYEE_MULE_LINKED", "module": "graph" }
  ],
  "message": {
    "key": "payment_paused_callback",
    "text": "Payment paused. Your money hasn’t left your account. We’ll call you on +91 •••• 3210."
  },
  "queue": "scam-callback",
  "policy": { "id": "retail", "version": 14 },
  "created_at": "2026-10-22T12:53:34Z"
}
decision_id
Match the app, the FRM event and the log on this.
event
The payment, called before the instruction went to the rail.
session
Read since the app opened: the 41-minute call began before login.
action
Hold. The app shows the message and sends nothing.
reasons
Four codes from four modules: scam, device, behaviour, graph. Risk first.
message
What Ananya reads. Logged as shown.
queue
Your call-back queue, by the name your FRM already uses. The call goes to +91 •••• 3210, her registered number.
policy
Retail, version 14: the version that held a new payee to ₹10,000 for 12 hours.

Fields

Who reads each field: the app, which needs the action and the message; the FRM, which needs the reasons and the queue; and audit, which needs the policy version and the message as it was shown.

Every field of the decision object, with its type and who reads it
FieldTypeWhat it carriesRead by
decision_idstringThe decision’s id. The same id reaches your app, the event your FRM receives and the decision log, so the three can be matched.App, FRM, Audit
eventstringThe moment your app called at: signup.start, login.attempt, device.change, payee.add or payment.initiate.FRM, Audit
sessionstringThe app session the SDK has read since the app opened. Several decisions can share one session, and the call at a payment is read against everything earlier in it.FRM, Audit
actionenumWhat to do now: allow, warn, step_up, cool_off, hold or block. Your app branches on this one field and handles all six.App, FRM, Audit
reasonsobject[]Why, as reason codes, risk first and then what held. The same codes reach your FRM and the log, so nobody sees an action without the signals behind it.FRM, Audit
reasons[].codestringOne code from the catalogue, in capitals: ACTIVE_CALL, PAYEE_MULE_LINKED.FRM, Audit
reasons[].modulestringThe module that read it: scam, device, behaviour, graph, access, binding or one of the verification modules.FRM, Audit
messageobjectThe words for the customer. Show the text as it is, or map the key to your own copy and languages. Whatever is shown is logged with the decision.App, Audit
message.keystringA stable key for the message, such as payment_paused_callback, so you can translate it or replace it with your own wording.App
message.textstringPlain words written for the customer, for this action at this moment. Never written for an analyst.App, Audit
queuestring | nullThe queue in your FRM or case manager a person should work, such as scam-callback, or null when nobody needs to act. The names are yours, set in the policy.FRM
policyobjectThe policy and version that made the decision, so any decision can be traced to the rule that applied at the time.Audit
policy.idstringThe policy’s name, per journey and market: retail here.Audit
policy.versionnumberThe version that applied. Every version is kept.Audit
created_attimestampWhen the decision was made, in UTC.FRM, Audit

The six actions

From the least to the most restrictive. Each comes with a message for the customer and a record for the FRM; most customers only ever see the first.

The six actions: when each applies, what the customer sees and what the FRM receives
ActionWhenThe customer seesYour FRM receives
allowEverything holds.Payment sent.ALLOW · known device · bound SIM · usual payee
warnSomething is unusual, but the customer is in control.This payee is new to you. Check the name before you send.WARN · first-time payee
step_upIdentity is uncertain.Confirm it’s you with a quick face check.STEP_UP · new device · face check requested
cool_offA new device, SIM or payee.You can send up to the starter limit now. You get the full limit in 12 hours.COOL_OFF · new device 4 min · limit applied
holdSigns of coercion or a mule payee.Payment paused. Your money hasn’t left your account. We’ll call you on +91 •••• 3210.HOLD · active call · screen share · mule-linked payee
blockClear attack.We couldn’t verify this session. Your money is safe.BLOCK · virtual camera · emulator

Branching on the action

Handle all six actions explicitly, so a new policy rule never falls through to a default you did not intend. The SDK has read the session since the app opened; the call passes only your own identifiers and the payment’s details.

PaymentViewModel.kt

// The SDK has read the session since launch.
val decision = DeepID.decide(
  Event.PaymentInitiate(
    customer = user.id,
    payee = payee.vpa,
    amount = Money(96_000, "INR"),
    rail = "UPI",
  )
)

when (decision.action) {
  Action.ALLOW    -> sendPayment()
  Action.WARN     -> showWarning(decision.message)
  Action.STEP_UP  -> stepUp(decision.message)
  Action.COOL_OFF -> applyLimit(decision.message)
  Action.HOLD     -> showPaused(decision.message)
  Action.BLOCK    -> endSession(decision.message)
}

Customer messages

message.text is written for the customer, for this action at this moment: what happened to the money and what happens next, never an accusation. Show it as it is, or map message.key to your own copy and languages. The message shown is logged with the decision, so a complaint or an audit can see exactly what the customer read.

Which message a decision carries is set in the policy, rule by rule. The reasons it carries come from the catalogue, and the same object reaches your FRM through webhooks.

Wire one moment in the sandbox, before any contract.

Sign-up or login end to end, with the decision back in the app and as an event, so your engineers see the shape of it before anyone signs anything.