Errors
What this page covers
Two groups of operations answer errors with the envelope described here:
- the automated recipe operations under
/api/v3.1/automated-recipe*; GET /api/v3.1/connections/{connection_id}/accounts/.
Every other operation of this API answers with {"detail": "..."}, as described in the
Introduction. The deprecated v3 recipe operations are in that older group too:
see Moving recipes to v3.1.
This page also lists the build error codes, which are not HTTP errors at all: they arrive
inside a successful 200 response, in the error field of a recipe.
The error envelope
Every error response of the operations above has the same three keys:
Code
codeis machine-readable and stable. Branch on it, never on the message.messageis human-readable text meant for a log or a support ticket. Its wording can change at any time and is not part of the contract.detailsis always present and always an object. Its keys depend on the code; codes that carry no extra data return{}. Read it only after you have matched the code.
The HTTP status matches the code, so a client that does not know a code can still fall back to the status. See New values without a new version.
Validation errors
validation_error is the only code with a list inside details. It reports every field of the
request body that failed the schema, in details.fields:
Code
Each entry has three keys:
| Key | Meaning |
|---|---|
field | Where in the body the problem is. |
code | Why it failed, for example required, empty, max_length, invalid. |
message | Human-readable text for that one field. |
The field path is written the way you would index into the JSON body you sent: a dot for an
object key and square brackets for a list position. connections[0].accounts is the accounts
list of the first connection, and connections[1].accounts[2].id is the id of the third account
of the second connection. A problem with the body as a whole, one that belongs to no single field,
is reported with an empty string: "field": "".
The field codes are the codes of the validation layer, not the codes of the catalogue below. They are a separate, open set; treat an unfamiliar one as "this field is invalid".
HTTP error codes
How the status is chosen
| Status | Meaning |
|---|---|
400 | The request is wrong by itself: it breaks the schema, or the rules of the configuration. Fix the request. |
401 | Authentication failed. |
403 | Authentication succeeded, but this operation is not available to you. |
404 | The resource is not there for you. |
405 | The method is not supported on this path. |
409 | The request is well formed, but contradicts the current state of the resource. Read the resource, then decide. |
500 | Our fault. The request was not served. |
501 | The destination of your workspace cannot answer the question. Only destination_check_unsupported. |
503 | Infrastructure is temporarily unavailable. Retry - see Temporary failures and retries. |
bad_request is the exception to the table: it is the catch-all for a 4xx that has no code of
its own, so it usually arrives with 400 but can arrive with another 4xx status, for example
415 for an unsupported content type. Read the status of the response, not the code, to know what
happened.
Codes any operation can return
code | Status | When | details |
|---|---|---|---|
validation_error | 400 | The request body does not match the schema. | {fields: [{field, code, message}]} |
malformed_request | 400 | The request body is not valid JSON. | {} |
invalid_query_parameter | 400 | A query parameter has a value that is not allowed. | {parameter, allowed_values} - allowed_values is filled in only where the values can be enumerated, for example state; otherwise it is []. |
bad_request | 4xx | Any other 4xx refusal that has no code of its own. | {} |
authentication_failed | 401 | The token is missing, invalid or expired, or the session has expired. | {} |
permission_denied | 403 | The operation is not available to this user or agency. | {} |
not_found | 404 | No such recipe, template or connection in this workspace - it may belong to another workspace, be deleted, or not be managed through this API. Also an unknown path under the recipes prefix. The message names what was not found, for example Recipe not found. | {} |
method_not_allowed | 405 | The HTTP method is not supported on this path. | {} |
internal_error | 500 | Something failed on our side. The request was not served. | {} |
service_unavailable | 503 | A service this operation depends on did not answer in time. | {} |
Codes of specific operations
code | Status | Operations | When | details |
|---|---|---|---|---|
template_not_found | 400 | create | There is no such template for your agency, or it cannot be deployed through the API. | {template_id} |
template_not_deployable | 409 | create | The template exists but carries no configuration that can be deployed. | {template_id} |
recipe_already_exists | 409 | create | This workspace already has a recipe from this template; one template gives one recipe per workspace. | {existing_recipe_ids} - the ids worth reusing, best first. It can be empty, when two creates raced; then re-read GET /api/v3.1/automated-recipes/?template_id=.... |
view_name_taken | 409 | create, build, resetup | Another recipe already holds the output table name this template produces. | {view_name, conflicting_recipe_ids} |
no_active_destination | 409 | create, delete, extractions | The workspace has no active destination to build into. | {} |
destination_not_supported | 409 | create, delete, extractions | The destination of this workspace is not one recipes can be built into. | {} |
data_table_reservation_failed | 503 | create | The output table could not be reserved right now. Nothing was created. | {} |
data_source_not_allowed | 400 | configure | A data source in the body is not allowed by the template of this recipe. | {data_sources} - the ones that are not allowed |
connection_data_source_not_selected | 400 | configure | A connection's connection_data_source is not in the data_sources list of the same body. | {connections: [{index, connection_id, connection_data_source}]} |
duplicate_connection | 400 | configure | The same (connection_id, connection_data_source) pair is listed twice. | {connections: [{index, connection_id, connection_data_source}]} |
data_source_without_connection | 400 | configure | A newly added data source has no connection in the body. | {data_sources} - the ones added without a connection |
account_unavailable | 400 | configure | An account id is unknown, is not active, or belongs to another connection or workspace. Take ids from the v3.1 accounts endpoint, and only those with is_active: true. | {connections: [{connection_id, connection_data_source, accounts: [{id}]}]} |
data_source_removal_not_allowed | 409 | configure | A data source that the recipe already has is missing from the body. Configuration is append-only. | {data_sources} - the ones that went missing |
connection_removal_not_allowed | 409 | configure | A connection the recipe already has is missing from the body. | {connections: [{connection_id, connection_data_source}]} |
account_removal_not_allowed | 409 | configure | An account the recipe already has is missing from the body. | {connections: [{connection_id, connection_data_source, accounts: [{id}]}]} |
account_check_unavailable | 503 | configure | The accounts in the body could not be checked right now. Nothing was saved. | {} |
recipe_not_configured | 409 | build, resetup | The recipe has no data sources or connections yet: configure it first. | {} |
build_already_running | 409 | build, resetup | A build of this recipe is already running. | {} on build. On resetup it can carry {recipe_updated}: true means the steps were already recreated from the template and only the build did not start, so repeat the call once the running build finishes; false, or no key at all, means nothing was changed. |
recipe_template_unavailable | 409 | resetup | The template of this recipe can no longer be deployed. | {} |
delete_incomplete | 500 | delete | The recipe itself could not be deleted. Read it again to see whether it is still there. | {recipe_id} |
destination_unavailable | 503 | extractions | The destination of the workspace could not be queried right now. | {} |
destination_check_unsupported | 501 | extractions | The destination of this workspace cannot report whether a table exists. Retrying will not help. | {} |
Only one rule fails at a time: a body that breaks several of the configuration rules above is answered with the first problem found, so fixing one can reveal the next.
The catalogue has no code of its own for throttling; rate limits still apply to these paths as they do to the rest of the API, see Rate limits.
Temporary failures and retries
A 503 means "not now", not "no". The response carries the header Retry-After: 30, so wait
30 seconds before repeating the request. The header is part of the response even though it is not
shown in the operation reference on this site.
On a read - listing templates, reading a recipe, listing accounts - simply repeat the request.
On a writing operation - create, configure, delete, build, resetup - the outcome is unknown: the request may have gone through before the connection broke. Re-read the resource first, then decide whether to repeat it. The same applies after a timeout on your side. Repeating is safe once you have looked:
- create answers
409 recipe_already_existsif the first attempt did land, and the ids indetails.existing_recipe_idstell you which recipe to keep; - configure sends the full configuration every time, so sending it twice changes nothing;
- build and
resetup answer
409 build_already_runningif a build is already going; - a repeated delete answers
404.
account_check_unavailable and data_table_reservation_failed are the two 503 codes that
promise nothing was saved, so they can be repeated as they are. destination_unavailable is a
503 about your destination rather than about us; it too changes nothing.
501 destination_check_unsupported is the opposite case: it is permanent for this workspace, and
repeating the call will keep returning it.
Build errors
A build failure is not an HTTP error. The call that starts a build answers 202 and the build runs
in the background; when it fails, the failure arrives inside a normal 200 response, in the
error field of the recipe and in error of an entry of sources:
Code
A recipe carries error if and only if its state is failed; an entry of sources carries one
if and only if its status is failed.
Build error codes are a namespace of their own, separate from the HTTP codes above. Two names
appear in both lists on purpose - view_name_taken and build_already_running - because the same
situation can be caught either when a request comes in or later, during the build. Match a build
code against this catalogue only, and an HTTP code against the catalogue above only.
code | When | details |
|---|---|---|
extractions_not_created | The extractions this recipe needs could not be created. The detailed reason stays on our side. | {} |
no_data_delivered | Nothing had arrived to build from: no extraction of the recipe, of that data source, or of that template delivered data. | {} |
all_sources_failed | Progressive build: every data source failed, so nothing was left to build. | {} |
nothing_to_build | The recipe has no steps to build - it has no configuration. | {} |
view_name_taken | Another recipe took the output table name while this build was running. | {} |
data_table_registration_failed | The output table could not be registered once it had been built. | {} |
build_already_running | Another build of this recipe held the lock. | {} |
sql_object_not_found | A table or a column the recipe reads was not found. | {missing_column} - the column name, or null when the missing object was not a column |
sql_type_mismatch | A column type does not match the data in the source. | {column, source_type, target_type, mixed_types} - all four keys are always present, any of the first three can be null, and mixed_types is always a list (empty when there is nothing to list) |
sql_permission_denied | The query did not have the privileges it needed. | {} |
sql_resource_limit | The query exceeded a time or memory limit. | {} |
sql_syntax_error | The query has a syntax error. | {} |
sql_error | Some other database failure. | {} |
build_failed | Everything else, including a failure recorded without a code. | {} |
details here follows the same rule as in the HTTP envelope: always an object, {} when the code
carries nothing extra.
The other fields of a build error:
stage-extraction,transformation_build,final_build, ornullwhen the failure was recorded without a phase.data_source- the data source the failure belongs to, ornullwhen it belongs to no single one. Insidesourcesit always matches that entry'sdata_source.occurred_at- when the failure was recorded, ornull.message- public text, never raw internal output. For our own codes it is a fixed sentence chosen by the code, so it adds nothing the code does not already tell you. For thesql_*codes it is the message of the database with internal names and paths stripped out, which is the one case where the message is worth reading. Its wording can still change, so do not parse it.
A failure that was recorded before v3.1 existed has no code stored with it. Such a failure is
classified from the database text that was kept: if it is recognised, it arrives under an sql_*
code with the cleaned-up text; if it is not recognised, or nothing was kept, it arrives as
build_failed with the generic message.
Data that did not arrive
A build can succeed while part of the data is missing. That is not an error and has no code: the
recipe is ready_to_work with degraded: true, and the reasons are reported per data source in
not_delivered_reasons and not_delivered. The table of reasons and what to do about each of them
is in Deploying recipes.
New values without a new version
These fields are open sets and can gain new values without a change of API version:
code, both in the HTTP envelope and in a build error;reasoninnot_delivered_reasonsandnot_delivered;statusof an entry ofsources;stageof a build error;build_modeandoriginof a template.
Write your client so that an unfamiliar value does not break it:
- an unfamiliar HTTP
codeis handled by the HTTP status of the response, using the table in How the status is chosen - retry a503, fix the request on a400, look at the resource on a409, and report a500; - an unfamiliar value anywhere else is treated as "unknown" and shown as it is, rather than mapped onto a value you do know.
Existing values are not removed or renamed without a new version of the API.
Getting help
Every response carries an x-im-trace-id header. Keep it - for a single failing request, and for
the poll that first showed a failed build - and quote it when you contact support. It is the
identifier that lets us find what happened to that exact request.

