Challenges & MFA
Challenges & MFA
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
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.
You ask your customer
Render the form, collect the values. This is the only part nobody can do for you.
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.
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
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
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.
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.