Challenges & MFA

What to do when the institution asks for something only your customer can give
View as Markdown

Not every login finishes in one step. A bank sends a code to a phone; a public administration asks the person to confirm a push notification; a portal wants a field nobody could know in advance. When that happens the execution parks — it is not stuck, and it is not finished — publishes the exact form it needs, and waits for you.

This is the part of a direct execution where your product has to be a participant. With a flow, the widget has this conversation for you.


The loop

1

You are told

The action_required webhook fires, or a poll answers 423. Both mean the same thing.

2

You read the form

Execution State answers 423 with an Action Required State: form_schema describes the fields, and action_required_expires is your deadline.

3

You ask your customer

Render the form, collect the values. This is the only part nobody can do for you.

4

You send them back

A flat JSON object to Resume an Execution. It answers 202, and the execution goes back to ONGOING.

An execution can go round this loop more than once — a login factor first, then something a specific feature needs later.


Reading form_schema

It is an OpenAPI 3.0.3 Schema Object, so a form renderer that speaks JSON Schema already understands it: properties, required, types and formats. What is specific to this platform lives under x-meta.

{
"type": "object",
"format": "cg-action-required",
"properties": {
"otp": { "type": "string", "title": "Verification code", "maxLength": 6 }
},
"required": ["otp"],
"x-meta": {
"reason": "MFA_REQUIRED",
"mfa_type": "otp-sms",
"timeout": 120,
"instructions_title": "Enter the code",
"instructions_text": "We sent a code to the phone registered with your bank.",
"submit_button_text": "Continue"
}
}
In x-metaWhat it tells you
reasonMFA_REQUIRED — a strong-authentication factor. INPUT_REQUIRED — anything else the engine needs.
mfa_typeotp-sms, otp-email, push-notification, totp or generic. It is what lets you say “check your phone” instead of “enter a value”.
timeoutSeconds this form is valid for, from the moment it was raised. The same figure lands on the state as action_required_expires.
instructions_title · instructions_text · submit_button_textCopy written for the end user, in context. Use it: it is more specific than anything generic you would write.

Some challenges ask for no value at all. A push notification is confirmed on the person’s own device, so the form may carry instructions and no fields. properties and required are the authority on what, if anything, you have to send — show the instructions, and submit what the form declares.


Answering

curl -s -X PUT "$INFONITE_API/executions/handler/v1/$EXECUTION_ID" \
-H "X-APP-SECRET: $INFONITE_SECRET" \
-H "Content-Type: application/json" \
-d '{"otp": "418205"}'

The values may be encrypted exactly like the initial credentials — field by field as rsa::… / hybrid::…, or the whole body as one encrypted string. A one-time code is a credential for the seconds it lives, and this is where that matters most.

What the answers mean

StatusMeaning
202Accepted. The execution is queued again.
400This execution is not waiting for anything — it is already closed, or it never asked.
410The window expired. The execution is aborted; there is nothing to resume.
503The engine became unavailable while the execution was parked.

A wrong value is not an HTTP error. 202 means we forwarded it, not it was right. The source rejects it and the execution either asks again or ends with an authentication reason — watch the state, not the status code of your PUT.


The deadline is real

action_required_expires is not advisory. Past that instant the execution is aborted with ACTION_TIMEOUT, the answer is refused with 410, and the run has to be started again from scratch.

The window is usually a couple of minutes, because that is how long the institution’s own code lives. Design for it:

  • Ask before you start. Have the customer present, or a channel to reach them, before launching an execution against an engine that will challenge.
  • Do not queue the collection. A form the customer sees three minutes later is a form that fails.
  • Fail forward. If the code did not arrive, start a new execution rather than waiting on a dead one.

When nobody can answer

base_configurations.customer_interaction_available is what tells the platform whether this conversation is possible at all.

You sentIf the source raises a challenge
trueThe execution parks and waits for you, for as long as the form allows.
falseThe execution ends with CUSTOMER_INTERVENTION_REQUIRED. It never waits, because nobody is coming.

And it is checked before anything runs: starting an execution with false against an engine whose action_required_frequency is always is rejected on the spot, with that same reason in the body. Show Engine Details is where you find that out in advance.

false is also what stops your customer’s phone from ringing.

A challenge is not a passive question: the institution SENDS something — an SMS, a push notification, an e-mail — the moment the engine asks for it. With customer_interaction_available: false the engine never asks, so nothing is sent and the execution ends with CUSTOMER_INTERVENTION_REQUIRED instead.

That is the difference between a nightly batch nobody notices and one that wakes a thousand people at 03:00 with a code they did not request. Declare it honestly per execution: true only when somebody really is there.

This is what makes some engines unattendable and others perfectly schedulable. If your product runs unattended batches, filter the catalogue by action_required_frequency and build your schedule from the engines that can answer without a person.


Delegating the pause, later

Everything above assumes your product asks the question. There is a second answer on the way: a minimal hosted interface where your customer resolves the pause themselves — the code, the extra field, the state of the run — authenticated by the session_token that comes back when you start the execution.

Coming soon 🚀 It is not a flow: no journey, no consent step, no branding, just the screens a pause needs over an execution you already started. Until it ships there is nothing to present that token to, and the loop above is the way. If you would rather not build those screens, tell support — it helps us prioritise.


Practise it in the sandbox

The sandbox engines raise real challenges — the full pause, the form, the deadline and the resume — with no phone and no institution involved. Build the loop there, including the two paths that are easy to forget: the wrong code, and the expired window.