The execution lifecycle
An execution is asynchronous from the first millisecond: the call that starts it answers 202 and hands you an execution_id, and the work happens behind that. Everything on this page answers the question that follows — where is it now, and what do I do about it?
Three fields, three different questions
This is the part worth reading slowly, because the three are easy to confuse and they do not mean the same thing.
Plus one convenience field that sits above all of them:
is_closed is the only signal that means finished. true — the run is over and nothing about it will change. false — it is alive, whatever the other fields say. It is in every state response, and it is what Execution Status turns into a 200.
A reason always implies its code, never the other way round. INCORRECT_CREDENTIALS is always AUTH_ERROR; AUTH_ERROR might be any of nine reasons. Branch on the code, and reach for the reason when the code is not specific enough to decide — which, for the failures, it usually is not.
The ten families
PARTIAL is a finished execution, not a failing one. It is what you get when a bank answers for accounts and times out for investments. What arrived is real and final; what did not will not appear later. Read features to see which is which, and treat the gap as a business decision — is this enough to act on? — rather than as an error to retry blindly.
Every reason, by family
Each heading below is a status_code — one of the ten families in the table above — and the rows under it are the status_reason values that belong to it. That is the relationship in one sentence: the family is the situation, the reason is the granular cause.
So a state reading status_code: AUTH_ERROR will always carry one of the reasons in the AUTH_ERROR section, and never a reason from another one. If your integration only handles the ten families it will work; it will just be less specific than it could be, and the failures are exactly where that specificity is worth having.
status_code: ONGOING — it is working
status_code: ACTION_REQUIRED — it is waiting for you
Both publish a form_schema describing exactly what to collect. See Challenges & MFA.
status_code: AUTH_ERROR — we could not get in
The most actionable family, because almost every value here means a person has to do something, and none of them is fixed by retrying.
A tokenized execution that ends in this family also locks the token. The stored credentials are the ones that were rejected, so check Token Status and mint a new token from a fresh parametrized execution rather than retrying the locked one.
UNHANDLED_AUTH_ERROR is its own family, not a member of this one, and it has a single reason of the same name: the login failed in a way the engine could not classify. Treat it as transient, and if the same access keeps producing it, send us the execution_id.
status_code: CONFIGURATION_ERROR — the request was not valid
The state carries configurations_errors and parameters_errors naming the offending fields, which is where to look before changing anything.
status_code: ABORTED — it was cancelled
status_code: TEMPORARY_ERROR — try again later
Nothing in this family means your request was wrong. A sensible retry — minutes for the setup ones, longer for OUT_OF_SERVICE — is the right response, with a cap so a source that is down all day does not become an infinite loop.
status_code: COMPLETED · PARTIAL · FAILED
One reason each, carrying the same name as its family — COMPLETED, PARTIAL, FAILED. When the run ends well there is nothing more to say at this level; the detail lives one level down, in the features.
The fourth axis: authentication_status
Reported separately from everything above, and it answers a question the execution status cannot: did we get into the source?
The distinction is the one that decides what you do next:
- We could not get in → a conversation with your customer. Their credentials, their bank, their action.
- We got in and the extraction fell short (
PARTIAL,FAILED,TEMPORARY_ERRORafterAUTH_OK) → nothing to ask anyone. Retry later, or take what arrived.
The auth_ended webhook delivers this the moment it is known, which is usually well before the execution ends — that is what lets you start the “ask for the credentials again” conversation early.
One level down: the features
features carries one entry per feature you requested, each with its own status_reason from a parallel vocabulary. This is where a PARTIAL explains itself.
A results endpoint for a feature that never finished answers 202; one for a feature you never requested answers 204. Both are described in Reading the results.
How to follow one
Four events: started, action required, authentication ended, ended. You do nothing until there is something to do.
HEAD on the execution answers 202, 423 or 200 with no body. Cheap enough for a loop; ask every few seconds, not every few milliseconds.
An engine talks to a real institution over a real network: seconds to minutes is normal, and a source that produces its report on its own schedule can take considerably longer. Size your own timeouts on the slowest feature you requested, not on the average.
The clocks
Three timings come back on the state, and they answer three different questions:
And three limits end an execution on their own:
execution_timeout— active processing, 60 to 1200 seconds, chosen per execution (default 1200). Waiting for you does not count against it.action_required_expires— the deadline to answer a challenge. Past it,ABORTED/ACTION_TIMEOUT.- The unattended window — an execution started with
customer_interaction_available: falsehas up to three hours to find a moment when the source cooperates, then gives up.
Running the same engine twice
An application cannot have two live executions for the same customer and engine — nor two carrying the same external_execution_id for the same customer, nor two on the same token. The second is refused with 409, and the body names the one already running so you can follow it instead of guessing. It is a guard against a double-submitted form running two logins against a bank at the same time, which some institutions treat as an attack.
If the parallel run is deliberate, give each one a different external_execution_id: your own identifier makes them distinguishable and both are accepted. It travels through every event and every state, which also makes it the natural place for your case number.
Ending it yourself
Stops a live execution and closes it as CLIENT_CANCELLED — a cancellation, not a failure. Whatever was extracted stays readable, and the slot is freed immediately.
Purges a closed execution and everything under it — records and documents — permanently. This is the one to build into your flow.
Delete as soon as you have stored what you need. Everything an execution retrieved is somebody’s financial life; the strongest protection available is not to be holding it. Executions nobody deletes are purged after the retention period, but that is a safety net, not a data-minimisation policy.