> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://infonite.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://infonite.dev/_mcp/server.

# 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](/flows/overview), the widget has this conversation for you.

---

## The loop

#### You are told

The [`action_required` webhook](/direct-executions/webhooks) fires, or a poll answers `423`. Both mean the same thing.

#### You read the form

[Execution State](api:GET/executions/handler/v1/\{execution_id}) 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](api:PUT/executions/handler/v1/\{execution_id}). 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`.

```json
{
  "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-meta`                                                       | What it tells you                                                                                                                            |
| :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| `reason`                                                          | `MFA_REQUIRED` — a strong-authentication factor. `INPUT_REQUIRED` — anything else the engine needs.                                          |
| `mfa_type`                                                        | `otp-sms`, `otp-email`, `push-notification`, `totp` or `generic`. It is what lets you say *"check your phone"* instead of *"enter a value"*. |
| `timeout`                                                         | Seconds 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_text` | Copy 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

```bash
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**

| Status | Meaning                                                                               |
| :----- | :------------------------------------------------------------------------------------ |
| `202`  | Accepted. The execution is queued again.                                              |
| `400`  | This execution is not waiting for anything — it is already closed, or it never asked. |
| `410`  | The window expired. The execution is aborted; there is nothing to resume.             |
| `503`  | The 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 sent | If the source raises a challenge                                                                        |
| :------- | :------------------------------------------------------------------------------------------------------ |
| `true`   | The execution parks and waits for you, for as long as the form allows.                                  |
| `false`  | The 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](api:GET/config/engines/\{engine_reference}) 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](/flows/overview): 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](mailto:support@infonite.tech) — 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**.

#### [The webhook that announces it](/direct-executions/webhooks)

Subscribing to `action_required` is what turns this from a polling loop into a notification your product reacts to.