{
  "openapi": "3.1.0",
  "info": {
    "title": "Ember API",
    "version": "0.1.0-preview",
    "x-status": "preview",
    "summary": "The family office you own, as an API.",
    "description": "Ember consolidates every custodian, entity, and private investment a family holds onto one balance sheet and turns questions into plans the owner executes themselves. This API exposes the same flow the app runs: link an institution with a read-only connection, read the balance sheet, entities, and holdings down to the tax lot, send statements and fund documents for parsing, and ask for a plan. Connecting accounts and running the plan is a single call each, so an owner's AI agent can do it on their behalf.\n\nThis specification describes a pre-release API. Paths, schemas, and field names may change before general availability.\n\nNot a registered investment adviser. Regulated services are delivered by licensed professionals. Steps in a plan that are regulated (formal investment advice, trade execution, tax filing) are marked as such and are routed to a licensed professional as a discrete, priced step that the owner approves.",
    "termsOfService": "https://emberhold.co/",
    "contact": {
      "name": "Ember",
      "url": "https://emberhold.co/"
    }
  },
  "servers": [
    {
      "url": "https://api.emberhold.co",
      "description": "Production (preview)"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Connections", "description": "Read-only links to custodians, banks, and fund portals." },
    { "name": "Balance sheet", "description": "One balance sheet across every account, entity, and asset class." },
    { "name": "Entities", "description": "Trusts, LLCs, holding companies, foundations, and individuals, with ownership mapped between them." },
    { "name": "Holdings", "description": "Positions across every custodian, down to the tax lot." },
    { "name": "Documents", "description": "Statements, K-1s, and capital calls, parsed on arrival." },
    { "name": "Plans", "description": "Ask a question, get a plan with the exact steps to execute it." }
  ],
  "paths": {
    "/v1/connections": {
      "get": {
        "tags": ["Connections"],
        "operationId": "listConnections",
        "summary": "List connected institutions",
        "responses": {
          "200": {
            "description": "Every institution linked to the family, with its connection status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Connection" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Connections"],
        "operationId": "createConnection",
        "summary": "Connect an institution",
        "description": "Links a custodian, bank, or fund portal with a read-only connection. Ember never receives a credential that can move money. Once the connection is `connected`, statements, K-1s, capital calls, and cost basis stream into the ledger and are parsed automatically. This is the whole connect flow in one call.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ConnectionRequest" },
              "examples": {
                "brokerage": {
                  "summary": "Link a brokerage account to a trust",
                  "value": { "institution": "Charles Schwab", "entity_id": "ent_family_trust" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The connection was created. Poll `GET /v1/connections` or wait for `status` to become `connected`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Connection" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/balance-sheet": {
      "get": {
        "tags": ["Balance sheet"],
        "operationId": "getBalanceSheet",
        "summary": "Read the consolidated balance sheet",
        "description": "One balance sheet across every account, entity, and asset class, public and private. Filter to a single entity to see what it owns directly and through the entities beneath it.",
        "parameters": [
          { "$ref": "#/components/parameters/AsOf" },
          { "$ref": "#/components/parameters/EntityFilter" }
        ],
        "responses": {
          "200": {
            "description": "The balance sheet as of the requested date.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BalanceSheet" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/entities": {
      "get": {
        "tags": ["Entities"],
        "operationId": "listEntities",
        "summary": "List entities and the ownership map",
        "description": "Every entity in the family structure and who owns what, through what. Each entity carries its parent ownership edges so an agent can walk the map from a principal down to a single account.",
        "responses": {
          "200": {
            "description": "The entity map.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Entity" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/holdings": {
      "get": {
        "tags": ["Holdings"],
        "operationId": "listHoldings",
        "summary": "List holdings across every custodian",
        "description": "Positions across every connected account, optionally expanded to the tax lot. The canonical question, \"I hold Apple in five places, what's my blended cost basis, and where do I take the highest lots off the table?\", is `GET /v1/holdings?symbol=AAPL&include_lots=true`.",
        "parameters": [
          { "$ref": "#/components/parameters/EntityFilter" },
          {
            "name": "symbol",
            "in": "query",
            "description": "Restrict to one public security by ticker.",
            "schema": { "type": "string", "examples": ["AAPL"] }
          },
          {
            "name": "asset_class",
            "in": "query",
            "schema": { "$ref": "#/components/schemas/AssetClass" }
          },
          {
            "name": "include_lots",
            "in": "query",
            "description": "Include tax lots for each holding.",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "Holdings matching the filters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Holding" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/documents": {
      "post": {
        "tags": ["Documents"],
        "operationId": "createDocument",
        "summary": "Send a document for parsing",
        "description": "Upload a statement, K-1, capital call, distribution notice, or any other document an institution issues. It is parsed on arrival and its contents are written to the ledger: positions and cost basis from statements, capital calls and distributions to the fund position they belong to. Documents Ember retrieves from connected fund portals arrive through the same path without an upload.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "contentMediaType": "application/pdf", "description": "The document. PDF is the common case." },
                  "entity_id": { "type": "string", "description": "The entity the document belongs to, if known. Otherwise Ember infers it from the document." },
                  "kind": { "$ref": "#/components/schemas/DocumentKind" }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted for parsing.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Document" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/plans": {
      "post": {
        "tags": ["Plans"],
        "operationId": "createPlan",
        "summary": "Ask a question, get a plan",
        "description": "Ask what an advisor would tell you, in plain language. The response is guidance with the exact steps to implement it yourself at whichever custodian is best. Steps are marked `executor: owner` when the owner can carry them out directly, or `executor: licensed_professional` when the step is regulated. Regulated steps are not carried out by the API; they are routed to a licensed professional as a discrete, priced step the owner approves. Ember does not place trades and does not give formal investment advice.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PlanRequest" },
              "examples": {
                "concentration": {
                  "summary": "Trim a concentrated position tax-efficiently",
                  "value": { "question": "Where do I take the highest-cost-basis Apple lots off the table across all five accounts?" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The plan.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plan" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/plans/{id}": {
      "get": {
        "tags": ["Plans"],
        "operationId": "getPlan",
        "summary": "Read a plan and its step status",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "The plan, including which steps are done, which are waiting on the owner, and which are with a licensed professional.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plan" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A family-scoped API token. Every token is bound to one family; data is isolated per family."
      }
    },
    "parameters": {
      "AsOf": {
        "name": "as_of",
        "in": "query",
        "description": "Value the balance sheet as of this date. Defaults to today.",
        "schema": { "type": "string", "format": "date" }
      },
      "EntityFilter": {
        "name": "entity_id",
        "in": "query",
        "description": "Restrict to one entity and everything it owns beneath it.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing or invalid bearer token.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such resource in this family.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Money": {
        "type": "object",
        "required": ["amount", "currency"],
        "properties": {
          "amount": { "type": "string", "description": "Decimal amount as a string to avoid floating-point loss.", "examples": ["1250000.00"] },
          "currency": { "type": "string", "default": "USD" }
        }
      },
      "AssetClass": {
        "type": "string",
        "enum": ["cash", "public_equity", "fixed_income", "private_fund", "private_company", "real_estate", "retirement", "other"]
      },
      "ConnectionRequest": {
        "type": "object",
        "required": ["institution"],
        "properties": {
          "institution": { "type": "string", "description": "The institution to connect, as you would type it into the search box: a custodian, bank, or fund portal.", "examples": ["Charles Schwab", "Morgan Stanley", "Fidelity"] },
          "entity_id": { "type": "string", "description": "The entity that holds the accounts at this institution. If omitted, accounts are attached to the principal and can be re-mapped later." }
        }
      },
      "Connection": {
        "type": "object",
        "required": ["id", "institution", "status", "read_only", "created_at"],
        "properties": {
          "id": { "type": "string", "examples": ["con_01j8..."] },
          "institution": { "type": "string" },
          "entity_id": { "type": "string" },
          "status": { "type": "string", "enum": ["connecting", "authorizing", "connected", "error"] },
          "read_only": { "type": "boolean", "const": true, "description": "Always true. Connections cannot move money." },
          "accounts": { "type": "integer", "description": "Number of accounts found at this institution once connected." },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "Entity": {
        "type": "object",
        "required": ["id", "name", "kind"],
        "properties": {
          "id": { "type": "string", "examples": ["ent_family_trust"] },
          "name": { "type": "string" },
          "kind": { "type": "string", "enum": ["individual", "trust", "llc", "holding_company", "foundation", "partnership", "next_gen_account"] },
          "owned_by": {
            "type": "array",
            "description": "Ownership edges into this entity.",
            "items": {
              "type": "object",
              "required": ["entity_id", "percent"],
              "properties": {
                "entity_id": { "type": "string" },
                "percent": { "type": "number", "minimum": 0, "maximum": 100 }
              }
            }
          },
          "net_worth": { "$ref": "#/components/schemas/Money" }
        }
      },
      "BalanceSheet": {
        "type": "object",
        "required": ["as_of", "net_worth", "assets", "liabilities"],
        "properties": {
          "as_of": { "type": "string", "format": "date" },
          "entity_id": { "type": "string" },
          "net_worth": { "$ref": "#/components/schemas/Money" },
          "assets": { "$ref": "#/components/schemas/Money" },
          "liabilities": { "$ref": "#/components/schemas/Money" },
          "by_asset_class": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["asset_class", "value"],
              "properties": {
                "asset_class": { "$ref": "#/components/schemas/AssetClass" },
                "value": { "$ref": "#/components/schemas/Money" }
              }
            }
          },
          "by_entity": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["entity_id", "value"],
              "properties": {
                "entity_id": { "type": "string" },
                "value": { "$ref": "#/components/schemas/Money" }
              }
            }
          }
        }
      },
      "TaxLot": {
        "type": "object",
        "required": ["acquired_on", "quantity", "cost_basis"],
        "properties": {
          "acquired_on": { "type": "string", "format": "date" },
          "quantity": { "type": "string" },
          "cost_basis": { "$ref": "#/components/schemas/Money" },
          "market_value": { "$ref": "#/components/schemas/Money" },
          "term": { "type": "string", "enum": ["short", "long"] }
        }
      },
      "Holding": {
        "type": "object",
        "required": ["id", "name", "asset_class", "connection_id", "entity_id", "market_value"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "symbol": { "type": "string" },
          "asset_class": { "$ref": "#/components/schemas/AssetClass" },
          "connection_id": { "type": "string" },
          "entity_id": { "type": "string" },
          "quantity": { "type": "string" },
          "market_value": { "$ref": "#/components/schemas/Money" },
          "cost_basis": { "$ref": "#/components/schemas/Money" },
          "lots": { "type": "array", "items": { "$ref": "#/components/schemas/TaxLot" }, "description": "Present when `include_lots=true`." },
          "private_fund": {
            "type": "object",
            "description": "Present for private fund positions.",
            "properties": {
              "committed": { "$ref": "#/components/schemas/Money" },
              "called": { "$ref": "#/components/schemas/Money" },
              "distributed": { "$ref": "#/components/schemas/Money" },
              "irr": { "type": "number", "description": "Annualised, as a fraction." },
              "moic": { "type": "number" }
            }
          }
        }
      },
      "DocumentKind": {
        "type": "string",
        "enum": ["statement", "k1", "capital_call", "distribution", "subscription", "other"]
      },
      "Document": {
        "type": "object",
        "required": ["id", "status", "received_at"],
        "properties": {
          "id": { "type": "string" },
          "kind": { "$ref": "#/components/schemas/DocumentKind" },
          "entity_id": { "type": "string" },
          "status": { "type": "string", "enum": ["received", "parsing", "parsed", "needs_review"] },
          "received_at": { "type": "string", "format": "date-time" }
        }
      },
      "PlanRequest": {
        "type": "object",
        "required": ["question"],
        "properties": {
          "question": { "type": "string", "description": "What you want to know or do, in plain language." },
          "entity_id": { "type": "string", "description": "Scope the question to one entity." }
        }
      },
      "PlanStep": {
        "type": "object",
        "required": ["id", "description", "executor", "regulated", "status"],
        "properties": {
          "id": { "type": "string" },
          "description": { "type": "string", "description": "The exact action, including which custodian or account to do it at." },
          "executor": {
            "type": "string",
            "enum": ["owner", "licensed_professional"],
            "description": "`owner`: the owner (or their agent) does it themselves at the custodian. `licensed_professional`: the step is regulated and is handled by a licensed professional as a discrete, priced step."
          },
          "regulated": { "type": "boolean" },
          "requires_owner_approval": { "type": "boolean", "description": "True for every regulated step. Nothing regulated proceeds without the owner's approval." },
          "status": { "type": "string", "enum": ["proposed", "awaiting_approval", "with_professional", "done", "skipped"] },
          "professional": {
            "type": "object",
            "description": "Who handled a regulated step and why, once assigned.",
            "properties": {
              "name": { "type": "string" },
              "credential": { "type": "string" },
              "rationale": { "type": "string" }
            }
          }
        }
      },
      "Plan": {
        "type": "object",
        "required": ["id", "question", "guidance", "steps", "created_at", "disclaimer"],
        "properties": {
          "id": { "type": "string" },
          "question": { "type": "string" },
          "guidance": { "type": "string", "description": "The answer, in plain language." },
          "steps": { "type": "array", "items": { "$ref": "#/components/schemas/PlanStep" } },
          "created_at": { "type": "string", "format": "date-time" },
          "disclaimer": {
            "type": "string",
            "const": "Not a registered investment adviser. Regulated services are delivered by licensed professionals."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": { "type": "string" },
          "message": { "type": "string" }
        }
      }
    }
  }
}
