{
  "openapi": "3.1.0",
  "info": {
    "title": "Index One REST API",
    "version": "2026-09-04",
    "summary": "Build, run, query and deliver custom financial indices.",
    "description": "The Index One REST API covers two generations of the product.\n\n**The workflow plane (`idx_...` ids)** is the current system: a workflow is a DAG of operations that is scheduled, executed and persisted by the execution coordinator. Workflow data (values, holdings, weightings, universes, corporate actions) is read through `GET /query`.\n\n**The legacy index plane (uuid ids such as `348fb318-b1a4-42f8-9c8b-b618edf2832f-0`, marked `legacy`)** is the original index engine backed by the `{stage}-index` tables. It is still live and still serving production indices, but no new features are being added to it.\n\nOperations marked `planned` do **not** exist yet. They are documented here because they are the intended shape of the API and, in several cases, because the console already calls them.\n\n### Base URL\n\n`https://api.indexone.io` — there is no stage prefix. Requests to `https://api.indexone.io/prod/...` return `Missing Authentication Token`.\n\n### Authentication\n\nMost routes require **both** an API key and a Cognito id token. The API key is validated by API Gateway (and carries the usage-plan rate limit); the Cognito id token identifies the user and is what every handler uses for ownership checks. Sending only one of the two on a both-auth route fails: no key gives `403 Forbidden` from the gateway, no token gives `401 Unauthorized`.\n\n### Error shapes\n\nEvery error response uses one shape: `{\"error\": \"<message>\"}`, with the HTTP status carrying the category. Two optional fields appear where they help: `code` (a machine-readable discriminator, e.g. the Cognito exception class on a sign-in failure) and `issues` (an array of structured problems on validation 422s). See the Errors guide for the status-code list.",
    "license": {
      "name": "Proprietary",
      "url": "https://indexone.io/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.indexone.io",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Cognito-backed sign-up, sign-in, token refresh and password reset. These are the only authenticated-plane routes that need just an API key."
    },
    {
      "name": "API Keys",
      "description": "Create, list and revoke the team's API Gateway keys. Keys carry the usage-plan rate limit."
    },
    {
      "name": "Workflows",
      "description": "The current index plane: workflows (`idx_...`) are operation DAGs executed by the coordinator. Creation over REST is still a gap — see the `planned` operations."
    },
    {
      "name": "Data Query",
      "description": "`GET /query` is the generic read path for all workflow-plane data: values, holdings, weightings, universes and corporate actions."
    },
    {
      "name": "Executions",
      "description": "Execution history for workflows and deliveries. One execution row per operation group per fire."
    },
    {
      "name": "Deliveries",
      "description": "Scheduled distribution of index data over email, webhook, SFTP, S3 or a partner integration. A delivery is an ordinary workflow — a plain `operations` list — stored in `{stage}-deliveries` and run by the same coordinator as workflows."
    },
    {
      "name": "Datasets",
      "description": "Team-owned tabular data (`dst_...`) stored as parquet in S3 and consumable from a workflow via the `load_dataset` operation."
    },
    {
      "name": "Execution Engine",
      "description": "Direct access to the execution engine: run an operation DAG, simulate a workflow, and read the live operation catalog. Unauthenticated at the gateway."
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Team API key, created with `POST /teams/{id}/keys`. Validated by API Gateway before the request reaches any handler, and bound to the usage plan that enforces the per-key rate limit. A missing or unknown key returns `403 Forbidden` with `{\"message\": \"Forbidden\"}`; an over-limit key returns `429`."
      },
      "CognitoAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "The **raw Cognito id token** (JWT) from `POST /signin` or `POST /refresh_token`, sent with **no `Bearer ` prefix** — the value is the token itself. This is why the scheme is modelled as `apiKey` rather than `http`/`bearer`: adding a `Bearer ` prefix makes the API Gateway Cognito authorizer reject the request with `401 Unauthorized`. Handlers read the caller identity from `requestContext.authorizer.claims.sub`."
      }
    },
    "schemas": {
      "ErrorMessage": {
        "type": "object",
        "description": "The standard API error shape (see `ErrorField`). Returned by the auth, users, teams, notifications and admin routes.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error text."
          }
        },
        "required": [
          "error"
        ],
        "example": {
          "error": "Not authorized to perform this operation"
        }
      },
      "ErrorField": {
        "type": "object",
        "description": "The standard API error shape. Every error response — from the Lambda handlers, the FastAPI worker and the API gateway — uses it: an `error` string, an optional machine-readable `code`, and an optional `issues` array on validation failures.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "code": {
            "type": "string",
            "description": "Optional machine-readable discriminator, present on some 401/402 responses (e.g. `NotAuthorizedException`, `billing_required`)."
          },
          "issues": {
            "type": "array",
            "description": "Optional structured validation problems (`severity`, `code`, `message`, `path`, `operation_id`), present on workflow-validation 422s.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "required": [
          "error"
        ],
        "example": {
          "error": "workflow not found"
        }
      },
      "SignInError": {
        "type": "object",
        "description": "Standard error shape for `POST /signin`, with a `code` carrying the Cognito exception class so a client can branch on the reason without parsing the message.",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Cognito exception class name, e.g. `NotAuthorizedException`, `UserNotConfirmedException`."
          }
        },
        "required": [
          "error"
        ],
        "example": {
          "error": "An error occurred (NotAuthorizedException) when calling the InitiateAuth operation: Incorrect username or password.",
          "code": "NotAuthorizedException"
        }
      },
      "FastApiError": {
        "type": "object",
        "description": "Standard error shape for the FastAPI worker behind `/execute`, `/simulate`, `/validate` and `/schema`. Workflow-validation failures also include an `issues` array.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "issues": {
            "type": "array",
            "description": "Structured validation problems, present on 422s.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "required": [
          "error"
        ],
        "example": {
          "error": "trigger_id is required"
        }
      },
      "ApiKey": {
        "type": "object",
        "description": "An API Gateway key belonging to a team.",
        "properties": {
          "id": {
            "type": "string",
            "description": "API Gateway key id — the value passed to `DELETE /teams/{id}/keys/{key_id}`."
          },
          "value": {
            "type": "string",
            "description": "The key itself, in plaintext. Send it as the `x-api-key` header."
          },
          "created_at": {
            "type": "string",
            "format": "date",
            "description": "`YYYY-MM-DD`."
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "example": {
          "id": "k3n8qz1p42",
          "value": "8Kd0Wq3mZa7Yr1Xu5Tv9Bc2Ne6Lp4Hs0Gj8Fd3R",
          "created_at": "2026-03-11",
          "enabled": true
        }
      },
      "IndexValue": {
        "type": "object",
        "description": "A single point on a legacy index value series.",
        "properties": {
          "time": {
            "type": "string",
            "description": "UTC timestamp, format `YYYY-MM-DD HH:MM:SS`."
          },
          "value": {
            "type": "number"
          }
        },
        "example": {
          "time": "2026-07-18 20:00:00",
          "value": 1184.37
        }
      },
      "QueryResponse": {
        "type": "object",
        "description": "Response envelope of `GET /query`.",
        "properties": {
          "items": {
            "type": "array",
            "description": "The matching rows, in sort-key order (ascending unless `order=descending`).",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "count": {
            "type": "integer",
            "description": "Number of rows in `items` (not the total matching count)."
          },
          "cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque base64 continuation token. Present only when `limit` was reached and more rows exist; pass it back as `cursor` to resume. `null` means the partition was exhausted."
          },
          "figi_symbols": {
            "type": "object",
            "description": "Only present when `map_symbols=true`: FIGI to ticker-symbol lookup covering every identifier found anywhere in `items`.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "DeliveryOperationNode": {
        "type": "object",
        "description": "One node of a delivery's `operations` list — the same operation object a workflow uses. A delivery is an ordinary workflow: a trigger, then either an `index_panel` payload node and a `send_email` / `send_webhook` / `send_sftp` / `send_s3` channel node, or a single partner node (`send_alphabot`, `send_alphathena`, `send_stratifi`, `send_refinitiv`) that reads, formats and transports the data itself. Every operation and its parameters are listed in the live catalog at `GET /schema`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Node id, unique within the list. Downstream nodes wire to it by this id.",
            "example": "delivery_trigger"
          },
          "operation": {
            "type": "string",
            "description": "Operation name from the catalog, e.g. `index_event_trigger`, `trigger`, `manual_trigger`, `index_panel`, `send_email`."
          },
          "use_cache": {
            "type": "boolean",
            "description": "Whether the engine may reuse a cached result for this node. Delivery nodes are written with `false`."
          },
          "input": {
            "type": "array",
            "description": "Upstream nodes. Each entry is an object whose single key is `$ref`, set to an upstream node's `id` — see the request example. A trigger has no input.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "parameters": {
            "type": "object",
            "description": "Parameters for that operation. A parameter may take an upstream node's output instead of a literal value by using the same `$ref` form against that node's output path, e.g. the trigger's `fired_index_ids`.",
            "additionalProperties": true
          }
        },
        "required": [
          "id",
          "operation"
        ]
      }
    }
  },
  "paths": {
    "/dataset": {
      "post": {
        "operationId": "createDataset",
        "summary": "Create a dataset",
        "description": "Creates an empty dataset record (`dst_...`) and reserves its S3 prefix, then links it into the team's `datasets` map. The dataset holds no data yet: upload it with `POST /dataset/{dataset_id}/presigned_url` followed by `POST /dataset/{dataset_id}/mutation`.\n\nOnce populated, the dataset is readable from a workflow through the `load_dataset` operation.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "default": "Untitled Dataset"
                  },
                  "description": {
                    "type": "string"
                  },
                  "team_id": {
                    "type": "string",
                    "description": "Owning team."
                  },
                  "get_presigned_url": {
                    "type": "boolean",
                    "description": "Also return an upload slot under `presigned_url` in the same call, skipping `POST /dataset/{dataset_id}/presigned_url`. Send `file_type` (and optionally `file_name`) alongside it; `file_type` defaults to `text/csv`."
                  }
                }
              },
              "example": {
                "name": "Custom ESG scores",
                "description": "Quarterly vendor ESG scores keyed by FIGI",
                "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created dataset, wrapped alongside the updated team record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dataset": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "team_data": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "presigned_url": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Only when `get_presigned_url` was true."
                    }
                  }
                },
                "example": {
                  "dataset": {
                    "id": "dst_R4mVnQ8xL27p",
                    "name": "Custom ESG scores",
                    "description": "Quarterly vendor ESG scores keyed by FIGI",
                    "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                    "created_at": "2026-07-21 08:45:10",
                    "updated_at": "2026-07-21 08:45:10",
                    "s3_url": "s3://prod-core-i1-datasets/dst_R4mVnQ8xL27p",
                    "url": "https://prod-core-i1-datasets.s3.amazonaws.com/dst_R4mVnQ8xL27p"
                  },
                  "team_data": {
                    "id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                    "datasets": {
                      "dst_R4mVnQ8xL27p": {
                        "id": "dst_R4mVnQ8xL27p"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "get": {
        "operationId": "listDatasets",
        "summary": "List datasets",
        "description": "Lists the datasets belonging to a team, newest first.\n\nThe datasets table is keyed on `id` alone with no secondary index, so it cannot be queried by team. This reads the `datasets` map held on the team record — the same source the console list and the MCP `list_datasets` tool use — which is why `team_id` is required rather than inferred.\n\nAdded 2026-07-21. Before that there was no collection route and callers had to read the team record themselves via `GET /teams/{id}`.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Team to list for. Would default to the caller's primary team.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the team's datasets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                },
                "example": [
                  {
                    "id": "dst_R4mVnQ8xL27p",
                    "name": "Custom ESG scores",
                    "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                    "updated_at": "2026-07-21 08:50:42",
                    "row_count": 4812
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller is not a member of that team."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/dataset/{dataset_id}": {
      "get": {
        "operationId": "getDataset",
        "summary": "Get a dataset",
        "description": "Returns the dataset record. With `parsed=true` the current parquet file is read from S3 and its rows are attached as `data` — convenient for previews, but it loads the whole dataset into the response, so avoid it for large files.\n\nNote there is no ownership check on this route: any authenticated caller who knows a `dst_...` id can read it.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset id.",
            "schema": {
              "type": "string"
            },
            "example": "dst_R4mVnQ8xL27p"
          },
          {
            "name": "parsed",
            "in": "query",
            "required": false,
            "description": "Also read and return the dataset rows as `data`.",
            "schema": {
              "type": "boolean"
            },
            "example": false
          }
        ],
        "responses": {
          "200": {
            "description": "The dataset record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "dst_R4mVnQ8xL27p",
                  "name": "Custom ESG scores",
                  "description": "Quarterly vendor ESG scores keyed by FIGI",
                  "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                  "created_at": "2026-07-21 08:45:10",
                  "updated_at": "2026-07-21 08:50:42",
                  "s3_url": "s3://prod-core-i1-datasets/dst_R4mVnQ8xL27p",
                  "url": "https://prod-core-i1-datasets.s3.amazonaws.com/dst_R4mVnQ8xL27p"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "404": {
            "description": "No such dataset. The handler indexes the DynamoDB `Item` key unguarded, so an unknown id surfaces as a `500`-class gateway error rather than a clean 404."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "patch": {
        "operationId": "patchDataset",
        "summary": "Update dataset metadata",
        "description": "Updates a dataset's metadata. Only `name` and `description` may be changed; everything else on the record is an identifier or derived from one (`s3_url` and `url` embed the dataset id), so changing them would orphan the stored objects.\n\nThe team record holds a full copy of each dataset, written once at creation. This endpoint rewrites that copy too, so a rename is reflected everywhere the team map is read. Data changes still go through `POST /dataset/{dataset_id}/mutation`.\n\nAdded 2026-07-21.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset id.",
            "schema": {
              "type": "string"
            },
            "example": "dst_R4mVnQ8xL27p"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "Custom ESG scores (v2)",
                "description": "Now including governance sub-scores"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Intended response: the updated dataset record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "dst_R4mVnQ8xL27p",
                  "name": "Custom ESG scores (v2)",
                  "description": "Now including governance sub-scores",
                  "updated_at": "2026-07-21 09:01:00"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this dataset."
          },
          "404": {
            "description": "No such dataset."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "delete": {
        "operationId": "deleteDataset",
        "summary": "Delete a dataset",
        "description": "Deletes a dataset: every S3 object under its prefix, its DynamoDB record, and its entry in the team's `datasets` map. Not reversible.\n\nAdded 2026-07-21. The same purge remains available as `POST /dataset/{dataset_id}/mutation` with `{\"mutation\": \"delete\"}`, which is how deletion worked before this route existed — an overload that made a destructive action look like a data append. Prefer `DELETE`.\n\nWorkflows referencing the dataset are not updated and will fail on their next run.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset id.",
            "schema": {
              "type": "string"
            },
            "example": "dst_R4mVnQ8xL27p"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the deletion summary (same shape the `delete` mutation returns today).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dataset_id": {
                      "type": "string"
                    },
                    "s3_deleted": {
                      "type": "integer"
                    },
                    "record_deleted": {
                      "type": "boolean"
                    },
                    "reference_removed": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "dataset_id": "dst_R4mVnQ8xL27p",
                  "s3_deleted": 12,
                  "record_deleted": true,
                  "reference_removed": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this dataset."
          },
          "404": {
            "description": "No such dataset."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/dataset/{dataset_id}/mutation": {
      "post": {
        "operationId": "mutateDataset",
        "summary": "Apply an upload to a dataset",
        "description": "Step two of the upload flow. Reads the file you uploaded and folds it into the dataset's current parquet.\n\n- `append` — concatenate the new rows onto the existing data. Columns are unioned, so a schema mismatch produces nulls rather than an error.\n- `replace` — discard the existing data and use the upload as the new dataset.\n- `delete` — **deletes the entire dataset**: every S3 object under its prefix, the DynamoDB record and the team link. `mutation_data_url` is ignored. `DELETE /dataset/{dataset_id}` does exactly the same thing and says so in its name; prefer it.\n\n`mutation_data_url` is the presigned URL with its query string removed (or any `s3://` or `https://<bucket>.s3.amazonaws.com/<key>` URL). `.csv`, `.tsv` and `.parquet` are recognised by extension.\n\nOn success a `dataset_update` trigger is posted so workflows consuming this dataset can react.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset id.",
            "schema": {
              "type": "string"
            },
            "example": "dst_R4mVnQ8xL27p"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mutation": {
                    "type": "string",
                    "enum": [
                      "append",
                      "replace",
                      "delete"
                    ]
                  },
                  "mutation_data_url": {
                    "type": "string",
                    "description": "URL of the uploaded file. Required for `append` and `replace`."
                  }
                },
                "required": [
                  "mutation"
                ]
              },
              "example": {
                "mutation": "append",
                "mutation_data_url": "https://prod-core-i1-datasets.s3.amazonaws.com/dst_R4mVnQ8xL27p/fil_7YtQ2nWx4Kp8/esg_scores_2026q2.csv"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated dataset plus the trigger result. For `delete`, a `deletion` summary instead.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dataset": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "trigger_notification": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "deletion": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Only for `mutation: delete`."
                    }
                  }
                },
                "example": {
                  "dataset": {
                    "id": "dst_R4mVnQ8xL27p",
                    "name": "Custom ESG scores",
                    "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                    "updated_at": "2026-07-21 08:50:42",
                    "url": "https://prod-core-i1-datasets.s3.amazonaws.com/dst_R4mVnQ8xL27p"
                  },
                  "trigger_notification": {
                    "sent": true,
                    "status_code": 200,
                    "response": {
                      "status": "accepted",
                      "trigger_id": "trg_3Kw8PnBv52Hj"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`mutation type not specified`, or `unsupported mutation type: <value>`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "mutation type not specified"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "404": {
            "description": "No such dataset."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/dataset/{dataset_id}/presigned_url": {
      "post": {
        "operationId": "createDatasetPresignedUrl",
        "summary": "Get an upload URL for dataset data",
        "description": "Returns a presigned S3 `PUT` URL. Step one of the two-step upload: `PUT` your CSV or parquet bytes to the returned `url` with `Content-Type` set to exactly the `file_type` you requested (a mismatch makes S3 reject the signature), then call `POST /dataset/{dataset_id}/mutation` with the URL stripped of its query string to fold the upload into the dataset.\n\nThe upload lands at a unique per-file key, so an upload alone never overwrites the current data.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "dataset_id",
            "in": "path",
            "required": true,
            "description": "Dataset id.",
            "schema": {
              "type": "string"
            },
            "example": "dst_R4mVnQ8xL27p"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "file_type": {
                    "type": "string",
                    "description": "MIME type of the upload; becomes the required `Content-Type` of the PUT.",
                    "example": "text/csv"
                  },
                  "file_name": {
                    "type": "string",
                    "description": "Optional filename. Defaults to a generated `fil_...` name."
                  }
                },
                "required": [
                  "file_type"
                ]
              },
              "example": {
                "file_type": "text/csv",
                "file_name": "esg_scores_2026q2.csv"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The presigned upload URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Presigned PUT URL."
                    },
                    "key": {
                      "type": "string"
                    },
                    "bucket": {
                      "type": "string"
                    },
                    "dataset_id": {
                      "type": "string"
                    },
                    "content_type": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "url": "https://prod-core-i1-datasets.s3.amazonaws.com/dst_R4mVnQ8xL27p/fil_7YtQ2nWx4Kp8/esg_scores_2026q2.csv?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=abc123",
                  "key": "dst_R4mVnQ8xL27p/fil_7YtQ2nWx4Kp8/esg_scores_2026q2.csv",
                  "bucket": "prod-core-i1-datasets",
                  "dataset_id": "dst_R4mVnQ8xL27p",
                  "content_type": "text/csv"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/deliveries": {
      "post": {
        "operationId": "createDelivery",
        "summary": "Create a delivery",
        "description": "Creates a delivery rule: a schedule plus a payload plus a destination. Deliveries are workflow-engine rows in their own table and are picked up by the same execution coordinator as workflows, so a delivery behaves exactly like a workflow at runtime.\n\nA delivery **is** an ordinary workflow: `operations` is a plain list of operation nodes, exactly as a workflow stores them, and nothing is expanded at run time. Two shapes cover everything:\n\n- **Standard** — `trigger -> index_panel -> send_<channel>`, where the channel is `send_email`, `send_webhook`, `send_sftp` or `send_s3`. The trigger is `index_event_trigger` (fires when a target index publishes `value_eod`, `universe`, `weighting` or `holdings`), `trigger` (cron) or `manual_trigger`. The console's one-click form writes and reads back exactly these nodes, so the workflow builder shows what actually runs.\n- **Partner** — `trigger -> send_<partner>` (`send_alphabot`, `send_alphathena`, `send_stratifi`, `send_refinitiv`). A partner operation is the whole flow — it reads the data, formats it the one way that partner accepts and sends it over the one transport that partner speaks — so there is no `index_panel` node; you supply only the per-index account binding, such as `ric_map` for LSEG.\n\n`operations` is required. Every operation and its parameters are listed in the live catalog at `GET /schema`.\n\nThe new delivery is linked into the team's `deliveries` map and the coordinator is notified immediately, so a `live` delivery with an `index_event` trigger can fire the same day. Note the success status is **200, not 201**.",
        "tags": [
          "Deliveries"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "team_id": {
                    "type": "string",
                    "description": "Owning team. Must be a team the caller belongs to."
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "stage": {
                    "type": "string",
                    "enum": [
                      "live",
                      "draft"
                    ],
                    "default": "live",
                    "description": "`live` starts scheduling immediately."
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC"
                  },
                  "index_id": {
                    "type": "string",
                    "description": "Single target index. Use `index_ids` for several."
                  },
                  "index_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Target indices. Both `idx_...` and legacy uuid ids are accepted."
                  },
                  "operations": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/DeliveryOperationNode"
                    },
                    "description": "The delivery's operations, in execution order: `trigger -> index_panel -> send_<channel>` for a standard delivery, `trigger -> send_<partner>` for a partner one. Required."
                  }
                },
                "required": [
                  "team_id",
                  "name",
                  "operations"
                ]
              },
              "x-indexone-example": "{\n  \"team_id\": \"33a36e74-b36a-4a50-9a99-edeff5144a43\",\n  \"name\": \"Daily values to ops\",\n  \"stage\": \"live\",\n  \"timezone\": \"America/New_York\",\n  \"index_ids\": [\n    \"idx_bh7fgXWJMaa3\"\n  ],\n  \"operations\": [\n    {\n      \"id\": \"delivery_trigger\",\n      \"operation\": \"index_event_trigger\",\n      \"use_cache\": false,\n      \"parameters\": {\n        \"index_id\": \"idx_bh7fgXWJMaa3\",\n        \"event_type\": \"value_eod\"\n      }\n    },\n    {\n      \"id\": \"delivery_payload\",\n      \"operation\": \"index_panel\",\n      \"use_cache\": false,\n      \"input\": [\n        {\n          \"$ref\": \"delivery_trigger\"\n        }\n      ],\n      \"parameters\": {\n        \"payload_type\": \"values\",\n        \"history\": \"latest\",\n        \"index_ids\": {\n          \"$ref\": \"delivery_trigger.output.fired_index_ids\"\n        },\n        \"filename_template\": \"{index_id}_{date}_values.csv\"\n      }\n    },\n    {\n      \"id\": \"delivery_send\",\n      \"operation\": \"send_email\",\n      \"use_cache\": false,\n      \"input\": [\n        {\n          \"$ref\": \"delivery_payload\"\n        }\n      ],\n      \"parameters\": {\n        \"recipients\": [\n          \"ops@example.com\"\n        ],\n        \"subject\": \"Daily index values\",\n        \"body\": \"Attached are today's closing values.\"\n      }\n    }\n  ]\n}"
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created delivery, plus the coordinator notification result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "dlv_c8KtQ1rPxZ92",
                  "type": "delivery",
                  "name": "Daily values to ops",
                  "stage": "live",
                  "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                  "user_id": "7dcd221e-4cf4-4cb5-845e-32efc1803ee6",
                  "timezone": "America/New_York",
                  "index_ids": [
                    "idx_bh7fgXWJMaa3"
                  ],
                  "created_at": "2026-07-21 08:35:02",
                  "updated_at": "2026-07-21 08:35:02",
                  "trigger_notification": {
                    "sent": true,
                    "status_code": 200,
                    "response": {
                      "status": "accepted",
                      "trigger_id": "trg_5Xn2QpLw81Zk"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name required`, `operations required`, or `no team resolved for this caller`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "operations required"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller is not a member of `team_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/deliveries/{delivery_id}/artifacts": {
      "get": {
        "operationId": "listDeliveryArtifacts",
        "summary": "Retrieve files a delivery produced",
        "description": "**PLANNED — this route does not exist.** Today a delivery's output only leaves the system through its configured channel: email attachment, webhook body, or SFTP upload. There is no way to fetch what was sent afterwards, which makes a failed send unrecoverable without re-running the delivery, and makes pull-based integrations impossible.\n\nThe intent is to list the artifacts each execution generated — filename, target index, size, generation time — with a short-lived presigned URL to download each one.",
        "tags": [
          "Deliveries"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "delivery_id",
            "in": "path",
            "required": true,
            "description": "Delivery id.",
            "schema": {
              "type": "string"
            },
            "example": "dlv_c8KtQ1rPxZ92"
          },
          {
            "name": "execution_id",
            "in": "query",
            "required": false,
            "description": "Restrict to one execution. Defaults to the most recent.",
            "schema": {
              "type": "string"
            },
            "example": "exn_dQ4vTnRy73Lc"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the artifacts and their download URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "artifacts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "filename": {
                            "type": "string"
                          },
                          "index_id": {
                            "type": "string"
                          },
                          "execution_id": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "size_bytes": {
                            "type": "integer"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Short-lived presigned download URL."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "artifacts": [
                    {
                      "filename": "idx_bh7fgXWJMaa3_2026-07-18_values.csv",
                      "index_id": "idx_bh7fgXWJMaa3",
                      "execution_id": "exn_dQ4vTnRy73Lc",
                      "created_at": "2026-07-18 20:06:35",
                      "size_bytes": 2184,
                      "url": "https://prod-core-i1-export.s3.eu-west-1.amazonaws.com/dlv_c8KtQ1rPxZ92/idx_bh7fgXWJMaa3_2026-07-18_values.csv?X-Amz-Expires=900"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this delivery."
          },
          "404": {
            "description": "No such delivery or execution."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/deliveries/{id}": {
      "get": {
        "operationId": "listTeamDeliveries",
        "summary": "List a team's deliveries",
        "description": "Returns the full delivery records for every delivery the team owns.\n\n**`{id}` is the TEAM id, not a delivery id** — the same convention as `GET /workflows/{id}`, and the reason `PATCH`/`DELETE` on this same path mean something different (those take the delivery id). There is no route that fetches a single delivery by its own id.\n\nReturns `[]` when the team has no deliveries.",
        "tags": [
          "Deliveries"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "**Team** id — not a delivery id.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          }
        ],
        "responses": {
          "200": {
            "description": "The team's deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                },
                "x-indexone-example": "[\n  {\n    \"id\": \"dlv_c8KtQ1rPxZ92\",\n    \"type\": \"delivery\",\n    \"name\": \"Daily values to ops\",\n    \"stage\": \"live\",\n    \"team_id\": \"33a36e74-b36a-4a50-9a99-edeff5144a43\",\n    \"index_ids\": [\n      \"idx_bh7fgXWJMaa3\"\n    ],\n    \"timezone\": \"America/New_York\",\n    \"created_at\": \"2026-06-12 15:41:55\",\n    \"updated_at\": \"2026-07-14 10:02:33\",\n    \"operations\": [\n      {\n        \"id\": \"delivery_trigger\",\n        \"operation\": \"index_event_trigger\",\n        \"use_cache\": false,\n        \"parameters\": {\n          \"index_id\": \"idx_bh7fgXWJMaa3\",\n          \"event_type\": \"value_eod\"\n        }\n      },\n      {\n        \"id\": \"delivery_payload\",\n        \"operation\": \"index_panel\",\n        \"use_cache\": false,\n        \"input\": [\n          {\n            \"$ref\": \"delivery_trigger\"\n          }\n        ],\n        \"parameters\": {\n          \"payload_type\": \"values\",\n          \"history\": \"latest\",\n          \"index_ids\": {\n            \"$ref\": \"delivery_trigger.output.fired_index_ids\"\n          },\n          \"filename_template\": \"{index_id}_{date}_values.csv\"\n        }\n      },\n      {\n        \"id\": \"delivery_send\",\n        \"operation\": \"send_email\",\n        \"use_cache\": false,\n        \"input\": [\n          {\n            \"$ref\": \"delivery_payload\"\n          }\n        ],\n        \"parameters\": {\n          \"recipients\": [\n            \"ops@example.com\"\n          ],\n          \"subject\": \"Daily index values\",\n          \"body\": \"Attached are today's closing values.\"\n        }\n      }\n    ]\n  },\n  {\n    \"id\": \"dlv_Q4mWs7Ld20Bk\",\n    \"type\": \"delivery\",\n    \"name\": \"Closing values to LSEG\",\n    \"stage\": \"live\",\n    \"team_id\": \"33a36e74-b36a-4a50-9a99-edeff5144a43\",\n    \"index_ids\": [\n      \"idx_bh7fgXWJMaa3\"\n    ],\n    \"timezone\": \"UTC\",\n    \"created_at\": \"2026-07-02 09:12:40\",\n    \"updated_at\": \"2026-07-02 09:12:40\",\n    \"operations\": [\n      {\n        \"id\": \"delivery_trigger\",\n        \"operation\": \"index_event_trigger\",\n        \"use_cache\": false,\n        \"parameters\": {\n          \"index_id\": \"idx_bh7fgXWJMaa3\",\n          \"event_type\": \"value_eod\"\n        }\n      },\n      {\n        \"id\": \"delivery_send\",\n        \"operation\": \"send_refinitiv\",\n        \"use_cache\": false,\n        \"input\": [\n          {\n            \"$ref\": \"delivery_trigger\"\n          }\n        ],\n        \"parameters\": {\n          \"index_ids\": {\n            \"$ref\": \"delivery_trigger.output.fired_index_ids\"\n          },\n          \"ric_map\": {\n            \"idx_bh7fgXWJMaa3\": \".SPLTPR\"\n          }\n        }\n      }\n    ]\n  }\n]"
              }
            }
          },
          "400": {
            "description": "`team id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller is not a member of that team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "patch": {
        "operationId": "patchDelivery",
        "summary": "Update a delivery",
        "description": "Partially updates a delivery. **Here `{id}` is the DELIVERY id**, unlike the `GET` on this same path, which takes the team id.\n\n`id`, `type`, `team_id` and `created_at` are blacklisted and dropped. Sending a field with the value `null` **removes** that attribute from the row rather than setting it to null — this is how you clear an optional config block.\n\nEvery patch notifies the coordinator to reload the delivery, because any field can change execution behaviour. Pausing a delivery is done by patching `stage`.",
        "tags": [
          "Deliveries"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "**Delivery** id.",
            "schema": {
              "type": "string"
            },
            "example": "dlv_c8KtQ1rPxZ92"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any delivery field except `id`, `type`, `team_id` and `created_at`. `null` removes an attribute.",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "stage": {
                    "type": "string",
                    "enum": [
                      "live",
                      "draft"
                    ],
                    "description": "Set to `draft` to pause the delivery."
                  },
                  "index_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "operations": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/DeliveryOperationNode"
                    },
                    "description": "Replaces the delivery's operations. Send the whole list — a partial list is stored as-is and is what will run."
                  }
                },
                "additionalProperties": true
              },
              "example": {
                "stage": "draft"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated delivery (`ALL_NEW`) plus `trigger_notification`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "dlv_c8KtQ1rPxZ92",
                  "name": "Daily values to ops",
                  "stage": "draft",
                  "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                  "updated_at": "2026-07-21 08:40:19",
                  "trigger_notification": {
                    "sent": true,
                    "status_code": 200,
                    "response": {
                      "status": "accepted",
                      "trigger_id": "trg_7Bz4RmKt29Qd"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id required`, or `no editable fields`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "no editable fields"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "delete": {
        "operationId": "deleteDelivery",
        "summary": "Delete a delivery",
        "description": "Deletes the delivery row and unlinks it from the team, after telling the coordinator to stop scheduling it. **Here `{id}` is the DELIVERY id.**\n\nUnlike deleting a workflow, this purges no data — a delivery owns no value or holdings history, only its own definition and its execution rows. Files already sent are unaffected.",
        "tags": [
          "Deliveries"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "**Delivery** id.",
            "schema": {
              "type": "string"
            },
            "example": "dlv_c8KtQ1rPxZ92"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "string"
                    },
                    "trigger_notification": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "deleted": "dlv_c8KtQ1rPxZ92",
                  "trigger_notification": {
                    "sent": true,
                    "status_code": 200,
                    "response": {
                      "status": "accepted",
                      "trigger_id": "trg_7Bz4RmKt29Qd"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/deliveries/{id}/executions": {
      "get": {
        "operationId": "listDeliveryExecutions",
        "summary": "List a delivery's executions",
        "description": "Returns the delivery's entire execution history, from the same `{stage}-executions` table as workflow executions and with the same row shape — deliveries and workflows are indistinguishable to the coordinator. **Here `{id}` is the DELIVERY id.**\n\nThis is how you confirm a send actually happened: a `completed` state means the send operation returned without error.",
        "tags": [
          "Executions",
          "Deliveries"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "**Delivery** id.",
            "schema": {
              "type": "string"
            },
            "example": "dlv_c8KtQ1rPxZ92"
          }
        ],
        "responses": {
          "200": {
            "description": "Every execution recorded for this delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                },
                "example": [
                  {
                    "index_id": "dlv_c8KtQ1rPxZ92",
                    "execution_id": "exn_dQ4vTnRy73Lc",
                    "state": "completed",
                    "updated_at": "2026-07-18 20:06:41",
                    "request_context": {
                      "registered_time": "2026-07-18 20:05:02",
                      "execution_time": "2026-07-18 20:06:30",
                      "execution_mode": "live"
                    }
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/execute": {
      "post": {
        "operationId": "executeOperations",
        "summary": "Execute an operation DAG",
        "description": "Runs an arbitrary operation DAG on the execution engine and returns each operation's output. This is what the workflow builder calls to preview a node graph — nothing is persisted unless the DAG itself contains a persisting operation.\n\n**This route is unauthenticated at the API Gateway** (`AuthorizationType: NONE`, no API key required): it is proxied straight to the workflow service over a VPC link. Treat it accordingly.\n\nPass `websocket_id` to stream per-node results as they complete over the WebSocket API; the HTTP response still returns the full result, truncated to 20 rows per output. Without it, outputs are returned untruncated.\n\n**The API Gateway integration timeout is 29 seconds.** A DAG that runs longer will have its HTTP connection cut even though the engine keeps going — which is why long runs must use `websocket_id`.\n\nAs of 2026-07-21 this endpoint requires a team API key. It was previously reachable without any credential.",
        "tags": [
          "Execution Engine"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "operations": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "The DAG to run."
                  },
                  "websocket_id": {
                    "type": "string",
                    "description": "WebSocket connection id to stream node results to. Also accepted as `connection_id`."
                  },
                  "team_id": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "x-indexone-example": "{\n  \"operations\": [\n    {\n      \"id\": \"trigger_1\",\n      \"operation\": \"manual_trigger\",\n      \"parameters\": {\n        \"time\": \"2026-07-18 20:00:00\"\n      },\n      \"input\": []\n    },\n    {\n      \"id\": \"eod_1\",\n      \"operation\": \"i1_core_eod\",\n      \"parameters\": {},\n      \"input\": [\n        {\n          \"$ref\": \"trigger_1\"\n        }\n      ]\n    }\n  ]\n}"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-operation results. Outputs are truncated to 20 rows when streaming.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "operations": [
                    {
                      "id": "trigger_1",
                      "operation": "manual_trigger",
                      "state": {
                        "error": false,
                        "suspended": false
                      },
                      "output": {
                        "trigger_time": "2026-07-18 20:00:00"
                      }
                    },
                    {
                      "id": "eod_1",
                      "operation": "i1_core_eod",
                      "state": {
                        "error": false,
                        "suspended": false
                      },
                      "output": [
                        {
                          "id": "BBG000B9XRY4",
                          "time": "2026-07-18",
                          "close": 233.18
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "No `operations` array in the request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "Forbidden"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation before the DAG was built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          },
          "500": {
            "description": "The DAG raised. `detail` is the exception message, or `{error, traceback}` when the service runs with tracebacks enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          }
        }
      }
    },
    "/forgot_password": {
      "post": {
        "operationId": "forgotPassword",
        "summary": "Start a password reset",
        "description": "Emails a password-reset confirmation code. Complete the reset with `POST /forgot_password/confirm`.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "format": "email"
                  }
                },
                "required": [
                  "username"
                ]
              },
              "example": {
                "username": "quant@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reset code sent. The body is the raw Cognito `ForgotPassword` response, which describes the delivery medium.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "CodeDeliveryDetails": {
                    "Destination": "q***@e***.com",
                    "DeliveryMedium": "EMAIL",
                    "AttributeName": "email"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown user, or Cognito rate-limited the reset. The Cognito exception is uncaught, so the body is a generic gateway error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/forgot_password/confirm": {
      "post": {
        "operationId": "confirmForgotPassword",
        "summary": "Complete a password reset",
        "description": "Sets a new password using the code emailed by `POST /forgot_password`.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "description": "The new password."
                  },
                  "confirmation_code": {
                    "type": "string"
                  }
                },
                "required": [
                  "username",
                  "password",
                  "confirmation_code"
                ]
              },
              "example": {
                "username": "quant@example.com",
                "password": "N3w-Passw0rd!",
                "confirmation_code": "482913"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed. The body is the raw (empty) Cognito response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {}
              }
            }
          },
          "400": {
            "description": "Wrong or expired code, or the new password violates the Cognito policy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/query": {
      "get": {
        "operationId": "queryTable",
        "summary": "Query workflow-plane data",
        "description": "The main data-read path for `idx_...` indices. It exposes the underlying DynamoDB tables directly as a keyed query: you pick a table, give a partition key (`pk` — the workflow or delivery id) and get back rows in sort-key order.\n\n**Tables** (`table`): `index-parameters` (the workflow definition itself, no sort key), `index-holdings`, `index-values-eod`, `index-weightings`, `index-universes`, `index-corporate-actions` (all sorted by `time`), and `deliveries` (no sort key).\n\n**Narrowing the read.** `sk` pins one exact sort-key value — the fastest way to fetch a single snapshot. `attributes` is a projection: only the named top-level fields come back, which matters because holdings and universe rows are large. `order=descending` with `limit` is the idiom for \"most recent N\".\n\n**Paging.** `cursor` is an opaque base64-encoded DynamoDB `LastEvaluatedKey`; never parse or construct it. It is returned only when `limit` was reached and more rows exist. Without `limit` the handler pages internally and returns the whole partition in one response — safe for values, expensive for holdings.\n\n**Symbols.** `map_symbols=true` adds a `figi_symbols` map covering every identifier that appears anywhere in the payload, so you can render tickers without a second lookup.\n\n**Access control.** `pk` must be a workflow or delivery id belonging to one of the caller's teams (site admins bypass this). This is the only authorization check — there is no per-table permission.",
        "tags": [
          "Data Query"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "table",
            "in": "query",
            "required": true,
            "description": "Which table to read.",
            "schema": {
              "type": "string",
              "enum": [
                "index-parameters",
                "index-holdings",
                "index-values-eod",
                "index-weightings",
                "index-universes",
                "index-corporate-actions",
                "deliveries"
              ]
            },
            "example": "index-values-eod"
          },
          {
            "name": "pk",
            "in": "query",
            "required": true,
            "description": "Partition key: the workflow (`idx_...`) or delivery (`dlv_...`) id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "sk",
            "in": "query",
            "required": false,
            "description": "Exact sort-key value (`time`). Ignored for tables without a sort key.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-18 20:00:00"
          },
          {
            "name": "attributes",
            "in": "query",
            "required": false,
            "description": "Comma-separated top-level fields to project. Omit for whole rows.",
            "schema": {
              "type": "string"
            },
            "example": "time,value"
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort-key direction.",
            "schema": {
              "type": "string",
              "enum": [
                "ascending",
                "descending"
              ],
              "default": "ascending"
            },
            "example": "descending"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return. Also enables cursor paging.",
            "schema": {
              "type": "integer"
            },
            "example": 250
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque continuation token from a previous response.",
            "schema": {
              "type": "string"
            },
            "example": "eyJpZCI6IHsiUyI6ICJpZHhfYmg3Zmc..."
          },
          {
            "name": "map_symbols",
            "in": "query",
            "required": false,
            "description": "Add a `figi_symbols` FIGI-to-ticker map.",
            "schema": {
              "type": "boolean"
            },
            "example": true
          }
        ],
        "responses": {
          "200": {
            "description": "The matching rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResponse"
                },
                "example": {
                  "items": [
                    {
                      "id": "idx_bh7fgXWJMaa3",
                      "time": "2026-07-18 20:00:00",
                      "value": 1184.37
                    },
                    {
                      "id": "idx_bh7fgXWJMaa3",
                      "time": "2026-07-17 20:00:00",
                      "value": 1178.42
                    }
                  ],
                  "count": 2,
                  "cursor": "eyJpZCI6IHsiUyI6ICJpZHhfYmg3Zmd..."
                }
              }
            }
          },
          "400": {
            "description": "`table and pk required`, or `unknown table <alias>` when `table` is not one of the seven allowed aliases.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "unknown table index-values"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "`pk` is not a workflow or delivery owned by one of the caller's teams.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/refresh_token": {
      "post": {
        "operationId": "refreshToken",
        "summary": "Exchange a refresh token for a new id token",
        "description": "Returns a fresh id token from the refresh token issued by `POST /signin`. No new refresh token is issued — keep using the original.\n\nAn expired, revoked or malformed refresh token returns **401** with `{\"error\": …}` — treat it as a sign-out and re-authenticate.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "refresh_token": {
                    "type": "string"
                  }
                },
                "required": [
                  "refresh_token"
                ]
              },
              "example": {
                "refresh_token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.refresh"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A fresh id token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id_token": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "success"
                      ]
                    }
                  },
                  "required": [
                    "id_token"
                  ]
                },
                "example": {
                  "id_token": "eyJraWQiOiJ4WGZQK0k9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI3ZGNkMjIxZS00Y2Y0LTRjYjUtODQ1ZS0zMmVmYzE4MDNlZTYifQ.sig",
                  "expires_in": 3600,
                  "status": "success"
                }
              }
            }
          },
          "401": {
            "description": "The refresh token is expired, revoked or invalid — re-authenticate with `POST /signin`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/schema": {
      "get": {
        "operationId": "getOperationSchema",
        "summary": "Get the operation catalog",
        "description": "Returns the live operation catalog: every operation the execution engine can run, with its parameter schema, input and output contracts, and documentation. There are currently **68 operations**, covering triggers, core data (EOD prices, fundamentals, FX, corporate actions, quotes), dataset loading, transformation, statistics, optimisation, index construction, storage and delivery.\n\nThis is the authoritative source when building a DAG — operation names, parameter names and enum values must come from here, not from memory, because the catalog changes as operations are added.\n\nThe response is `{\"operation_manifest\": {<name>: {...}}}`. It is a few hundred KB. **This route is unauthenticated** and takes no parameters.",
        "tags": [
          "Execution Engine"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The operation manifest, keyed by operation name. Truncated here to two entries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "operation_manifest": {
                      "type": "object",
                      "description": "Operation name to definition.",
                      "additionalProperties": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                },
                "example": {
                  "operation_manifest": {
                    "trigger": {
                      "description": "Fire the flow on a cron schedule, optionally aligned to an exchange calendar session.",
                      "category": "trigger",
                      "parameters": {
                        "cron": {
                          "type": "string",
                          "description": "5-field cron expression.",
                          "required": true
                        },
                        "timezone": {
                          "type": "string",
                          "default": "UTC"
                        },
                        "exchange_calendar": {
                          "type": "string",
                          "description": "xcals calendar code."
                        },
                        "alignment_enabled": {
                          "type": "boolean",
                          "default": false
                        },
                        "align_time": {
                          "type": "string",
                          "enum": [
                            "session_open",
                            "session_time",
                            "session_close"
                          ]
                        }
                      }
                    },
                    "i1_core_eod": {
                      "description": "Core end-of-day prices, market caps and volumes for the securities master.",
                      "category": "core_data",
                      "parameters": {
                        "as_of": {
                          "type": "string",
                          "description": "Point-in-time selection date."
                        },
                        "columns": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The engine failed to build the catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                },
                "example": {
                  "error": "Failed to generate schema: registry not initialised"
                }
              }
            }
          }
        },
        "x-indexone-no-4xx": "This endpoint takes no parameters and requires no credentials, so there is no client-error branch."
      }
    },
    "/signin": {
      "post": {
        "operationId": "signIn",
        "summary": "Sign in and get tokens",
        "description": "Authenticates against Cognito using the `USER_PASSWORD_AUTH` flow and returns an id token plus a refresh token.\n\nSend `id_token` as the `Authorization` header on every authenticated request — **raw, with no `Bearer ` prefix**. It expires after `expires_in` seconds (3600 by default); use `POST /refresh_token` to get a new one without re-prompting for the password.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "format": "email",
                    "description": "Lower-cased server-side, so case does not matter."
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "username",
                  "password"
                ]
              },
              "example": {
                "username": "quant@example.com",
                "password": "S3cure-Passw0rd!"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id_token": {
                      "type": "string",
                      "description": "Cognito id token (JWT). Send as `Authorization`, no prefix."
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "Long-lived; exchange at `POST /refresh_token`."
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Lifetime of `id_token` in seconds."
                    }
                  }
                },
                "example": {
                  "id_token": "eyJraWQiOiJ4WGZQK0k9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI3ZGNkMjIxZS00Y2Y0LTRjYjUtODQ1ZS0zMmVmYzE4MDNlZTYiLCJlbWFpbCI6InF1YW50QGV4YW1wbGUuY29tIiwiZXhwIjoxNzg0NTUyMDAwfQ.sig",
                  "refresh_token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.refresh",
                  "expires_in": 3600
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed. The `code` carries the Cognito exception class: `NotAuthorizedException` for bad credentials, `UserNotConfirmedException` when `POST /signup/confirm` was never called, `UserNotFoundException` for an unknown address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignInError"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/signup": {
      "post": {
        "operationId": "signUp",
        "summary": "Create a user account",
        "description": "Registers a new Cognito user and creates the backing user record. If the email address has a pending team invite the user joins that team; otherwise a new team is created and the user becomes its admin. A confirmation code is emailed — call `POST /signup/confirm` next.\n\nOptional profile fields (`firstname`, `lastname`, `company`, `use_case`, `industry`, `position`, `country`, `company_type`) are stored on the user record and forwarded to the CRM.\n\nThe same route doubles as the marketing contact form: posting `{\"hubspot_only\": true, ...}` creates only a CRM contact and no user.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address. Lower-cased server-side."
                  },
                  "password": {
                    "type": "string",
                    "description": "Must satisfy the Cognito password policy."
                  },
                  "firstname": {
                    "type": "string"
                  },
                  "lastname": {
                    "type": "string"
                  },
                  "company": {
                    "type": "string"
                  },
                  "hubspot_only": {
                    "type": "boolean",
                    "description": "Create only a CRM contact, no user account."
                  }
                },
                "required": [
                  "username",
                  "password"
                ]
              },
              "example": {
                "username": "quant@example.com",
                "password": "S3cure-Passw0rd!",
                "firstname": "Ada",
                "lastname": "Lovelace",
                "company": "Example Capital"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account created. The body is the new user's Cognito `sub`, JSON-encoded as a bare string.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                },
                "example": "7dcd221e-4cf4-4cb5-845e-32efc1803ee6"
              }
            }
          },
          "400": {
            "description": "`User already exists, please login or reset password instead.` when the email is taken, otherwise `Unknown error.` — Cognito policy violations (weak password, invalid email) are collapsed into that generic message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "User already exists, please login or reset password instead."
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/signup/confirm": {
      "post": {
        "operationId": "confirmSignUp",
        "summary": "Confirm a new account",
        "description": "Completes registration with the confirmation code emailed by `POST /signup`. After this the user can sign in.",
        "tags": [
          "Authentication"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "format": "email"
                  },
                  "confirmation_code": {
                    "type": "string"
                  }
                },
                "required": [
                  "username",
                  "confirmation_code"
                ]
              },
              "example": {
                "username": "quant@example.com",
                "confirmation_code": "482913"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account confirmed. The body is the literal string `success` (not JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                },
                "example": "success"
              }
            }
          },
          "400": {
            "description": "Wrong or expired code, or the user is already confirmed. The Cognito exception is not caught, so the gateway surfaces a generic error body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/simulate": {
      "post": {
        "operationId": "simulateWorkflow",
        "summary": "Backtest a workflow definition",
        "description": "Launches a historical simulation of an index definition and returns a pollable backtest id immediately — the same launch-now/fetch-later shape as the MCP `run_backtest` tool. The run replays the DAG over every scheduled date from `start_time` to now; its value series, holdings and statistics are persisted to a backtest blob. Nothing live is created and no schedule is registered.\n\nThe body is `{\"index_parameters\": {...}}` — the same object `POST /workflows` would take.\n\nFetch the result from `GET /backtests/{backtest_id}`, which waits server-side for the in-flight window so polling straight after this call is fine. Optionally pass `websocket_id` (or `connection_id`) to *also* stream progress frames over the WebSocket API as the run computes.\n\nBecause the id comes back at once, the 29-second API Gateway integration timeout can never sever the launch. This route runs a backtest only — `stage` is forced to `backtest`; creating a live index is `POST /workflows`. Requires a team API key.",
        "tags": [
          "Execution Engine"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "index_parameters": {
                    "type": "object",
                    "description": "The workflow definition: `name`, `start_time`, `exchange_calendar`, `operations`.",
                    "additionalProperties": true
                  },
                  "websocket_id": {
                    "type": "string",
                    "description": "Stream results to this WebSocket connection and return immediately. Also accepted as `connection_id`."
                  }
                },
                "required": [
                  "index_parameters"
                ]
              },
              "x-indexone-example": "{\n  \"index_parameters\": {\n    \"name\": \"Example Momentum Index\",\n    \"start_time\": \"2020-01-02 00:00:00\",\n    \"exchange_calendar\": \"XNYS\",\n    \"operations\": [\n      {\n        \"id\": \"trigger_1\",\n        \"operation\": \"trigger\",\n        \"parameters\": {\n          \"cron\": \"0 16 * * MON-FRI\",\n          \"exchange_calendar\": \"XNYS\"\n        },\n        \"input\": []\n      },\n      {\n        \"id\": \"eod_1\",\n        \"operation\": \"i1_core_eod\",\n        \"parameters\": {},\n        \"input\": [\n          {\n            \"$ref\": \"trigger_1\"\n          }\n        ]\n      },\n      {\n        \"id\": \"holdings_1\",\n        \"operation\": \"create_index_holdings\",\n        \"parameters\": {},\n        \"input\": [\n          {\n            \"$ref\": \"eod_1\"\n          }\n        ]\n      }\n    ]\n  },\n  \"websocket_id\": \"Yq3TneQMDoECJfw=\"\n}"
            }
          }
        },
        "responses": {
          "200": {
            "description": "The backtest was accepted and is running. Poll `GET /backtests/{backtest_id}` for the result; `connection_id` is echoed when `websocket_id` was supplied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "backtest_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "running"
                      ]
                    },
                    "hint": {
                      "type": "string"
                    },
                    "connection_id": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "backtest_id": "bkt_9Fq2LmVt41Xe",
                  "status": "running",
                  "hint": "Poll GET /backtests/bkt_9Fq2LmVt41Xe until status is 'completed' or 'failed'."
                }
              }
            }
          },
          "400": {
            "description": "No `index_parameters` object in the request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "Forbidden"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation before the backtest was built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          },
          "500": {
            "description": "The backtest raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FastApiError"
                }
              }
            }
          }
        }
      }
    },
    "/teams/{id}/keys": {
      "post": {
        "operationId": "createApiKey",
        "summary": "Create an API key",
        "description": "Creates an API Gateway key for the team and attaches it to the usage plan that carries the rate limit. The plaintext key is in the `value` field of the response — but it is also retrievable later via `GET /teams/{id}/keys`, so it is not a show-once secret.\n\nThe caller must be a member of the team. **A non-member gets HTTP 200** with `{\"message\": \"Not authorized to perform this operation\"}`, not a 403.",
        "tags": [
          "API Keys"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Team id.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          }
        ],
        "responses": {
          "200": {
            "description": "The new key — or, if the caller is not a team member, `{\"message\": \"Not authorized to perform this operation\"}` with this same 200 status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                },
                "example": {
                  "id": "k3n8qz1p42",
                  "value": "8Kd0Wq3mZa7Yr1Xu5Tv9Bc2Ne6Lp4Hs0Gj8Fd3R",
                  "created_at": "2026-07-21",
                  "enabled": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "get": {
        "operationId": "listApiKeys",
        "summary": "List a team's API keys",
        "description": "Lists every API Gateway key registered against the team.\n\n**Key values are returned in plaintext** (the handler passes `includeValues=True`), so this response is credential material: never log it, cache it or render it outside an authenticated screen.\n\nAs with key creation, a non-member gets HTTP 200 carrying `{\"message\": \"Not authorized to perform this operation\"}` instead of a 403.",
        "tags": [
          "API Keys"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Team id.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          }
        ],
        "responses": {
          "200": {
            "description": "The team's keys, values included.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKey"
                  }
                },
                "example": [
                  {
                    "id": "k3n8qz1p42",
                    "value": "8Kd0Wq3mZa7Yr1Xu5Tv9Bc2Ne6Lp4Hs0Gj8Fd3R",
                    "created_at": "2026-03-11",
                    "enabled": true
                  },
                  {
                    "id": "m7ta4bx915",
                    "value": "2Qp6Rn8Vs1Mj4Ck7Zw0Ye5Ta3Bu9Hd6Lx2Pg4W",
                    "created_at": "2026-06-30",
                    "enabled": false
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/teams/{id}/keys/{key_id}": {
      "delete": {
        "operationId": "deleteApiKey",
        "summary": "Revoke an API key",
        "description": "Deletes the key at API Gateway, immediately invalidating it everywhere. Deleting a key that does not exist is not an error — the handler swallows the exception and returns `{}`.",
        "tags": [
          "API Keys"
        ],
        "x-indexone-status": "stable",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Team id.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          },
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "description": "The key's `id` (not its `value`).",
            "schema": {
              "type": "string"
            },
            "example": "k3n8qz1p42"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked. Returns `{\"id\": ...}` on success, or `{}` if the key did not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "k3n8qz1p42"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Missing or invalid `x-api-key`."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/validate": {
      "post": {
        "operationId": "validate",
        "summary": "Validate a workflow or a dataset without running it",
        "description": "Two things can be validated, told apart by whether the body carries `dataset_id`.\n\n**Workflow** (no `dataset_id`) — checks the operation graph against the operation manifest: unknown operations, missing required parameters, broken `$ref` wiring, structural rules.\n\n**Dataset** (`dataset_id` present) — checks a dataset's data against its optional semantic-type schema. Pass `source` to check a file you have uploaded but not yet committed; omit it to report on the dataset as it stands. Reports unresolvable symbols (against the securities reference), unparseable dates, columns that differ from the stored data only by case, duplicate keys, out-of-range numbers and which dates are new — i.e. what will fire a `dataset_trigger`.\n\n**Advisory only.** This never blocks a commit and changes nothing; `ok: false` is information. Both shapes answer the same issues array (`severity`, `code`, `message`, `scope`). Column meanings come from the semantic type registry — `GET /schema` returns it as `semantic_manifest`.",
        "tags": [
          "Datasets"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dataset_id": {
                    "type": "string",
                    "description": "Validate this dataset. Omit to validate a workflow instead.",
                    "example": "dst_R4mVnQ8xL27p"
                  },
                  "source": {
                    "type": "string",
                    "description": "s3 uri/key of an uploaded file to check BEFORE committing it. Omit to check the dataset's current data.",
                    "example": "s3://prod-core-i1-datasets/dst_R4mVnQ8xL27p/fil_9c2/holdings.csv"
                  },
                  "mutation": {
                    "type": "string",
                    "enum": [
                      "append",
                      "replace"
                    ],
                    "default": "append",
                    "description": "How `source` would be committed. `append` compares against the stored rows, so new dates and duplicates are visible; `replace` judges the file on its own."
                  },
                  "schema": {
                    "type": "object",
                    "description": "Column meanings to check against; defaults to the dataset's own. Omit on a dataset with no schema to get a `suggested_schema` back."
                  },
                  "resolve": {
                    "type": "boolean",
                    "default": true,
                    "description": "Confirm identifiers against the securities reference. `false` checks shape only and is much faster."
                  },
                  "preview_rows": {
                    "type": "number",
                    "default": 10,
                    "description": "Rows of the incoming data to return with the report."
                  },
                  "index_parameters": {
                    "type": "object",
                    "description": "Workflow to validate, when no `dataset_id` is given."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "No errors were found. Never means \"you may not commit\"."
                    },
                    "type": {
                      "type": "string",
                      "description": "`dataset` for a dataset report."
                    },
                    "row_count": {
                      "type": "number"
                    },
                    "state": {
                      "type": "object",
                      "description": "Aggregate flags: error / warning / info."
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "severity, code, message, scope, and for a dataset the `column` and example values."
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Per column: dtype, declared semantic type, invalid counts and reference-resolution results."
                    },
                    "dates": {
                      "type": "object",
                      "description": "Parsed/unparsed counts, min, max, and `new_dates` — the ones that will fire a dataset trigger."
                    },
                    "suggested_schema": {
                      "type": "object",
                      "description": "Returned when no schema was supplied. A suggestion to pre-fill a form, never applied on its own."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown dataset, unreadable source file, or no dataset_id and no workflow."
          }
        }
      }
    },
    "/workflows": {
      "post": {
        "operationId": "createWorkflow",
        "summary": "Create a live workflow",
        "description": "Creates a live index: validates the workflow, persists it as `stage: \"pending\"`, and runs the backtest and live registration in the background. Returns immediately so the call is never held against the gateway's 29-second ceiling.\n\nPoll `GET /workflows/{workflow_id}` and watch `stage`: `pending` becomes `live` once history is written and the index is registered for continuous calculation, or `failed` with a `failure_reason` if validation, execution or the empty-index check rejected it. Failed records are kept, not deleted, so the reason is readable.\n\nPass `websocket_id` to stream progress instead of polling, the same way the console does.\n\nThis replaces the current go-live path, which is `POST /simulate` with `stage: \"live\"` buried in `index_parameters` — a call whose name says simulation and whose effect is a production index.",
        "tags": [
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string",
                    "description": "Datetime the index history begins at, `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS`."
                  },
                  "exchange_calendar": {
                    "type": "string",
                    "description": "`xcals` calendar code driving trading days.",
                    "example": "XNYS"
                  },
                  "team_id": {
                    "type": "string",
                    "description": "Owning team. Would default to the caller's primary team."
                  },
                  "operations": {
                    "type": "array",
                    "description": "The DAG. Every flow must start with a trigger operation, must include `create_index_holdings`, and must persist its result. Operations reference upstream outputs with `{\"$ref\": \"<op_id>.output#column\"}` and must also list that operation in `input`.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "operation": {
                          "type": "string",
                          "description": "An operation name from `GET /schema`."
                        },
                        "parameters": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "input": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "$ref": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      },
                      "required": [
                        "id",
                        "operation"
                      ]
                    }
                  }
                },
                "required": [
                  "name",
                  "start_time",
                  "operations"
                ]
              },
              "x-indexone-example": "{\n  \"name\": \"Example Momentum Index\",\n  \"start_time\": \"2020-01-02 00:00:00\",\n  \"exchange_calendar\": \"XNYS\",\n  \"operations\": [\n    {\n      \"id\": \"trigger_1\",\n      \"operation\": \"trigger\",\n      \"parameters\": {\n        \"cron\": \"0 16 * * MON-FRI\",\n        \"exchange_calendar\": \"XNYS\"\n      },\n      \"input\": []\n    },\n    {\n      \"id\": \"eod_1\",\n      \"operation\": \"i1_core_eod\",\n      \"parameters\": {},\n      \"input\": [\n        {\n          \"$ref\": \"trigger_1\"\n        }\n      ]\n    },\n    {\n      \"id\": \"holdings_1\",\n      \"operation\": \"create_index_holdings\",\n      \"parameters\": {},\n      \"input\": [\n        {\n          \"$ref\": \"eod_1\"\n        }\n      ]\n    }\n  ]\n}"
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. The workflow exists as pending; the deploy runs in the background.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "backtest_id": {
                      "type": "string"
                    },
                    "stage": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "created_at": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "backtest_id": "bkt_7Hs2Qa",
                  "stage": "pending",
                  "created_at": "2026-07-21 09:14:02"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "Forbidden"
                }
              }
            }
          },
          "422": {
            "description": "The workflow failed validation. The body lists the issues.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "every flow must start with a trigger operation"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listWorkflows",
        "summary": "List a team's workflows",
        "description": "Lists the workflows a team owns.\n\nAdded 2026-07-21, replacing `GET /workflows/{id}`, where `{id}` confusingly meant a *team* id while the same path template under PATCH, DELETE and /executions meant a *workflow* id.",
        "tags": [
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "team_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "tem_4Kd9Xa",
            "description": "The owning team."
          }
        ],
        "responses": {
          "200": {
            "description": "The team's workflows.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "idx_bh7fgXWJMaa3",
                    "name": "US Momentum 50",
                    "stage": "live",
                    "created_at": "2026-01-14 09:02:11"
                  }
                ]
              }
            }
          },
          "403": {
            "description": "Missing or invalid credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "Forbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "team not found"
                }
              }
            }
          }
        }
      }
    },
    "/workflows/{workflow_id}": {
      "get": {
        "operationId": "getWorkflow",
        "summary": "Get a workflow",
        "description": "Returns one workflow's whole record — metadata, calendar settings, stage, and its `operations` DAG.\n\nThis is what you poll after `POST /workflows`: watch `stage` move from `pending` to `live`, or to `failed` with a `failure_reason`.\n\nAdded 2026-07-21. Before that this path took a *team* id and returned that team's whole list, while the same path under PATCH and DELETE took a workflow id — so there was no way to fetch a single workflow at all. The list moved to `GET /workflows?team_id=`.",
        "tags": [
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "**Team** id — not a workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "33a36e74-b36a-4a50-9a99-edeff5144a43"
          }
        ],
        "responses": {
          "200": {
            "description": "The workflow.",
            "content": {
              "application/json": {
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "name": "US Momentum 50",
                  "stage": "live",
                  "status": "live",
                  "team_id": "tem_4Kd9Xa",
                  "exchange_calendar": "XNYS",
                  "timezone": "US/Eastern",
                  "created_at": "2026-01-14 09:02:11",
                  "updated_at": "2026-07-20 16:00:04",
                  "operations": []
                }
              }
            }
          },
          "403": {
            "description": "The workflow is not in one of your teams — or does not exist. Ownership is checked before existence, so an unknown id is indistinguishable from someone else's, which keeps ids non-enumerable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "404": {
            "description": "The workflow is referenced by your team but its row is missing — a data inconsistency, not a normal outcome. A plain unknown id returns 403.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "workflow not found"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchWorkflow",
        "summary": "Update a workflow",
        "description": "Partially updates a workflow row.\n\nThe fields `id`, `team_id`, `stage` and `created_at` are blacklisted and silently dropped from the patch — in particular you cannot promote a draft to live this way — going live is what the planned `POST /workflows` does.\n\nWhen the patch touches `operations`, `updated_at` is refreshed and a `workflow_updated` trigger is posted to the coordinator so the running schedule reloads; the outcome of that notification is reported back in `trigger_notification`.\n\nThe caller must belong to a team that owns the workflow, or be a site admin.",
        "tags": [
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any workflow field except `id`, `team_id`, `stage` and `created_at`.",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "visibility": {
                    "type": "string",
                    "description": "`public` is readable by anyone and listed in the Index Directory; `unlisted` is readable by anyone with the link but not listed.",
                    "enum": [
                      "private",
                      "public",
                      "unlisted"
                    ]
                  },
                  "operations": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "The full operation DAG. Replaces the existing one wholesale."
                  },
                  "exchange_calendar": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "example": {
                "name": "Example Momentum Index v2",
                "visibility": "public"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated workflow record (`ALL_NEW`), plus `trigger_notification` when `operations` changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "name": "Example Momentum Index v2",
                  "visibility": "public",
                  "team_id": "33a36e74-b36a-4a50-9a99-edeff5144a43",
                  "stage": "live",
                  "updated_at": "2026-07-21 08:15:02"
                }
              }
            }
          },
          "400": {
            "description": "`id required`, or `no editable fields` when the body was empty or contained only blacklisted keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "no editable fields"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "The caller's teams do not own this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      },
      "delete": {
        "operationId": "deleteWorkflow",
        "summary": "Delete a workflow and all its data",
        "description": "Permanently deletes the workflow **and every row it produced**. This is not reversible and there is no soft-delete window.\n\nThe sequence is: notify the coordinator (`workflow_deleted`) so it stops scheduling; mark the row `stage=deleting`; purge every partition of `index-holdings`, `index-values-eod`, `index-weightings`, `index-universes` and `index-corporate-actions` in parallel; delete the workflow row; unlink it from the caller's teams.\n\nThe response reports how many rows were purged per table; `-1` means that table's purge raised and its data may be partially left behind.",
        "tags": [
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "string"
                    },
                    "purged": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Rows deleted per table; `-1` on failure."
                    },
                    "trigger_notification": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "deleted": "idx_bh7fgXWJMaa3",
                  "purged": {
                    "prod-index-holdings": 1482,
                    "prod-index-values-eod": 1663,
                    "prod-index-weightings": 44,
                    "prod-index-universes": 44,
                    "prod-index-corporate-actions": 219
                  },
                  "trigger_notification": {
                    "sent": true,
                    "status_code": 200,
                    "response": {
                      "status": "accepted",
                      "trigger_id": "trg_5Xn2QpLw81Zk"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "The caller's teams do not own this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/executions": {
      "get": {
        "operationId": "listWorkflowExecutions",
        "summary": "List a workflow's executions",
        "description": "Returns the workflow's **entire** execution history from the `{stage}-executions` table — there is no limit or cursor parameter, so for a long-running live index this response can be large.\n\nEach row is one execution group: its `state`, the `request_context` timings (`registered_time`, `execution_time`, `execution_mode`), `next_execution_time`, and `last_error` when it failed. Note that per-group durations are not stored — they are inferred by comparing a group's `execution_time` to its successor's `registered_time`.",
        "tags": [
          "Executions",
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id (here it really is the workflow, not the team).",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          }
        ],
        "responses": {
          "200": {
            "description": "Every execution recorded for this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                },
                "example": [
                  {
                    "index_id": "idx_bh7fgXWJMaa3",
                    "execution_id": "exn_bhE9JoGPHwAi",
                    "execution_group_id": "grp_2",
                    "state": "completed",
                    "updated_at": "2026-07-18 20:04:12",
                    "next_execution_time": "2026-07-21 20:00:00",
                    "request_context": {
                      "registered_time": "2026-07-18 20:00:03",
                      "execution_time": "2026-07-18 20:00:15",
                      "execution_mode": "live"
                    }
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/executions/{execution_id}": {
      "get": {
        "operationId": "getWorkflowExecution",
        "summary": "Get one execution",
        "description": "Returns a single execution row in full, including the resolved `operations` DAG as it was executed and each operation's output state. This is the endpoint to use when debugging a failed run — `last_error` on the list endpoint tells you *that* it broke, this one tells you *where*.\n\nThe execution is returned wrapped: `{\"execution\": {...}}`.",
        "tags": [
          "Executions",
          "Workflows"
        ],
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "execution_id",
            "in": "path",
            "required": true,
            "description": "Execution id from the list endpoint.",
            "schema": {
              "type": "string"
            },
            "example": "exn_bhE9JoGPHwAi"
          }
        ],
        "responses": {
          "200": {
            "description": "The execution.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "execution": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                },
                "example": {
                  "execution": {
                    "index_id": "idx_bh7fgXWJMaa3",
                    "execution_id": "exn_bhE9JoGPHwAi",
                    "execution_group_id": "grp_2",
                    "state": "completed",
                    "updated_at": "2026-07-18 20:04:12",
                    "request_context": {
                      "registered_time": "2026-07-18 20:00:03",
                      "execution_time": "2026-07-18 20:00:15",
                      "execution_mode": "live"
                    },
                    "operations": [
                      {
                        "id": "trigger_1",
                        "operation": "trigger",
                        "state": {
                          "error": false,
                          "suspended": false
                        }
                      },
                      {
                        "id": "eod_1",
                        "operation": "i1_core_eod",
                        "state": {
                          "error": false,
                          "suspended": false
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "`workflow id and execution_id required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "404": {
            "description": "No execution with that id under this workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorField"
                },
                "example": {
                  "error": "execution not found"
                }
              }
            }
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/holdings": {
      "get": {
        "operationId": "getWorkflowHoldings",
        "summary": "Get a workflow's holdings",
        "description": "**PLANNED — this route does not exist.** Today: `GET /query?table=index-holdings&pk=idx_...` (or the MCP `get_index_holdings` tool).\n\nThe intent is a holdings snapshot with resolved identifiers — shares, weight, price and value per constituent for a given date — rather than the raw stored row.",
        "tags": [
          "Workflows"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "description": "Snapshot timestamp. Defaults to the latest.",
            "schema": {
              "type": "string"
            },
            "example": "2026-07-18 20:00:00"
          },
          {
            "name": "map_symbols",
            "in": "query",
            "required": false,
            "description": "Resolve FIGIs to ticker symbols.",
            "schema": {
              "type": "boolean"
            },
            "example": true
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the holdings snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "time": "2026-07-18 20:00:00",
                  "holdings": [
                    {
                      "id": "BBG000B9XRY4",
                      "symbol": "AAPL",
                      "shares": 1204.55,
                      "weight": 0.0731,
                      "price": 233.18,
                      "value": 280837.5
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow."
          },
          "404": {
            "description": "No such workflow, or no holdings at that time."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/stats": {
      "get": {
        "operationId": "getWorkflowStats",
        "summary": "Get a workflow's performance statistics",
        "description": "**PLANNED — this route does not exist.** Today the only workflow-plane equivalent is the MCP `get_index_stats` tool.\n\nThe intent is the same statistics surface (returns, volatility, Sharpe, Sortino, max drawdown, period returns) computed over an `idx_...` index's value series.",
        "tags": [
          "Workflows"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "returns",
            "in": "query",
            "required": false,
            "description": "Comma-separated windows to compute returns over.",
            "schema": {
              "type": "string"
            },
            "example": "itd,1y,ytd,30d"
          },
          {
            "name": "volatility",
            "in": "query",
            "required": false,
            "description": "Comma-separated windows for annualized volatility.",
            "schema": {
              "type": "string"
            },
            "example": "1y,ytd"
          },
          {
            "name": "sharpe",
            "in": "query",
            "required": false,
            "description": "Comma-separated windows for the Sharpe ratio.",
            "schema": {
              "type": "string"
            },
            "example": "itd,1y"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the requested statistics.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "returns": {
                    "itd": 0.1843,
                    "1y": 0.0912,
                    "ytd": 0.0471,
                    "30d": 0.0128
                  },
                  "volatility": {
                    "1y": 0.1633,
                    "ytd": 0.1502
                  },
                  "sharpe": {
                    "itd": 0.94,
                    "1y": 0.56
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow."
          },
          "404": {
            "description": "No such workflow."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/universe": {
      "get": {
        "operationId": "getWorkflowUniverse",
        "summary": "Get a workflow's universe",
        "description": "**PLANNED — this route does not exist.** Today: `GET /query?table=index-universes&pk=idx_...` (or the MCP `get_index_universe` tool).\n\nThe universe is the selection-stage output: every security that passed the filters at a given reconstitution, before weighting.",
        "tags": [
          "Workflows"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "description": "Reconstitution timestamp. Defaults to the latest.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-30 20:00:00"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the universe snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "time": "2026-06-30 20:00:00",
                  "universe": [
                    "BBG000B9XRY4",
                    "BBG000BPH459",
                    "BBG000BVPV84"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow."
          },
          "404": {
            "description": "No such workflow."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/values": {
      "get": {
        "operationId": "getWorkflowValues",
        "summary": "Get a workflow's index values",
        "description": "**PLANNED — this route does not exist.** Today the equivalent read is `GET /query?table=index-values-eod&pk=idx_...` (or the MCP `get_index_values` tool).\n\nThe intent is a first-class, chart-shaped read for the workflow plane: duration windows, increment down-sampling and a `[{time, value}]` payload, instead of making callers page raw DynamoDB rows through the generic query endpoint.",
        "tags": [
          "Workflows"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "duration",
            "in": "query",
            "required": false,
            "description": "Window, e.g. `1d`, `30d`, `1y`, `ytd`, `itd`.",
            "schema": {
              "type": "string"
            },
            "example": "1y"
          },
          {
            "name": "increment",
            "in": "query",
            "required": false,
            "description": "Down-sampling interval, e.g. `1d`, `1w`, `1mo`, `eod`.",
            "schema": {
              "type": "string"
            },
            "example": "1d"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Return only the most recent N points.",
            "schema": {
              "type": "integer"
            },
            "example": 250
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the value series.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/IndexValue"
                  }
                },
                "example": [
                  {
                    "time": "2026-07-17 20:00:00",
                    "value": 1178.42
                  },
                  {
                    "time": "2026-07-18 20:00:00",
                    "value": 1184.37
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow."
          },
          "404": {
            "description": "No such workflow."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    },
    "/workflows/{workflow_id}/weightings": {
      "get": {
        "operationId": "getWorkflowWeightings",
        "summary": "Get a workflow's target weightings",
        "description": "**PLANNED — this route does not exist.** Today: `GET /query?table=index-weightings&pk=idx_...` (or the MCP `get_index_weightings` tool).\n\nWeightings are the targets set at each rebalance, as distinct from holdings, which drift with prices between rebalances.",
        "tags": [
          "Workflows"
        ],
        "x-indexone-status": "planned",
        "security": [
          {
            "ApiKeyAuth": [],
            "CognitoAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "description": "Workflow id.",
            "schema": {
              "type": "string"
            },
            "example": "idx_bh7fgXWJMaa3"
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "description": "Rebalance timestamp. Defaults to the latest.",
            "schema": {
              "type": "string"
            },
            "example": "2026-06-30 20:00:00"
          }
        ],
        "responses": {
          "200": {
            "description": "Intended response: the weighting snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "example": {
                  "id": "idx_bh7fgXWJMaa3",
                  "time": "2026-06-30 20:00:00",
                  "weighting": {
                    "BBG000B9XRY4": 0.0731,
                    "BBG000BPH459": 0.0654
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid id token."
          },
          "403": {
            "description": "Caller's teams do not own this workflow."
          },
          "404": {
            "description": "No such workflow."
          },
          "429": {
            "description": "API key usage-plan rate limit exceeded."
          }
        }
      }
    }
  }
}