# Get Account Balances Source: https://docs.wellapp.ai/api-reference/endpoint/account-balances/get GET /v1/account-balances/{id} Fetch one `account_balance` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Account Balances Source: https://docs.wellapp.ai/api-reference/endpoint/account-balances/get-all GET /v1/account-balances Workspace-scoped, cursor-paginated list of `account_balance` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------- | --------------------- | ----------------------------------------------------------------- | ----------------------------------- | | `account` | to-one (Account) | `{ "account": { "field_name": { "_eq": … } } }` | `"field": "account.field_name"` ✅ | | `workspace` | to-one (Workspace) | `{ "workspace": { "field_name": { "_eq": … } } }` | `"field": "workspace.field_name"` ✅ | | `transactions` | to-many (Transaction) | `{ "transactions": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "account_balances", "whereClause": { "account": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "account.field_name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "account_balances", "whereClause": { "transactions": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Get Accounts Source: https://docs.wellapp.ai/api-reference/endpoint/accounts/get GET /v1/accounts/{id} Fetch one `account` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Accounts Source: https://docs.wellapp.ai/api-reference/endpoint/accounts/get-all GET /v1/accounts Workspace-scoped, cursor-paginated list of `account` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------------------ | ----------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------- | | `workspace` | to-one (Workspace) | `{ "workspace": { "field_name": { "_eq": … } } }` | `"field": "workspace.field_name"` ✅ | | `company` | to-one (Company) | `{ "company": { "field_name": { "_eq": … } } }` | `"field": "company.field_name"` ✅ | | `people` | to-one (People) | `{ "people": { "field_name": { "_eq": … } } }` | `"field": "people.field_name"` ✅ | | `bank_company` | to-one (Company) | `{ "bank_company": { "field_name": { "_eq": … } } }` | `"field": "bank_company.field_name"` ✅ | | `source_workspace_connector` | to-one (WorkspaceConnector) | `{ "source_workspace_connector": { "field_name": { "_eq": … } } }` | `"field": "source_workspace_connector.field_name"` ✅ | | `workspace_connector` | to-one (WorkspaceConnector) | `{ "workspace_connector": { "field_name": { "_eq": … } } }` | `"field": "workspace_connector.field_name"` ✅ | | `account_workspace_connectors` | to-many (AccountWorkspaceConnector) | `{ "account_workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "accounts", "whereClause": { "workspace": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.field_name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "accounts", "whereClause": { "account_workspace_connectors": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Create Api-Key Source: https://docs.wellapp.ai/api-reference/endpoint/api-key/create POST /v1/api-key # Delete Api-Key Source: https://docs.wellapp.ai/api-reference/endpoint/api-key/delete DELETE /v1/api-key/{id} # Create Balance Source: https://docs.wellapp.ai/api-reference/endpoint/balances/create POST /v1/balances Create a new balance record with local and accounting currency information, foreign exchange rates, and account relationships. # Delete Balance Source: https://docs.wellapp.ai/api-reference/endpoint/balances/delete DELETE /v1/balances/{id} Permanently delete a specific balance record by its unique ID. This action is irreversible and will remove all associated balance data including local balance, accounting balance, and foreign exchange information. # Get Blueprint Runs Source: https://docs.wellapp.ai/api-reference/endpoint/blueprint-runs/get GET /v1/blueprint-runs/{id} Fetch one `blueprint_run` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Blueprint Runs Source: https://docs.wellapp.ai/api-reference/endpoint/blueprint-runs/get-all GET /v1/blueprint-runs Workspace-scoped, cursor-paginated list of `blueprint_run` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------------- | ---------------------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `steps` | to-many (blueprint\_step) | `{ "steps": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "blueprint_runs", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "blueprint_runs", "whereClause": { "steps": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Get Cards Source: https://docs.wellapp.ai/api-reference/endpoint/cards/get GET /v1/cards/{id} Fetch one `card` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Cards Source: https://docs.wellapp.ai/api-reference/endpoint/cards/get-all GET /v1/cards Workspace-scoped, cursor-paginated list of `card` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | --------------------------------------------- | ------------------------------- | | `company` | to-one (company) | `{ "company": { "name": { "_eq": … } } }` | `"field": "company.name"` ✅ | | `people` | to-one (people) | `{ "people": { "full_name": { "_eq": … } } }` | `"field": "people.full_name"` ✅ | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "cards", "whereClause": { "company": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "company.name", "direction": "asc" } } ``` # Get Categories Source: https://docs.wellapp.ai/api-reference/endpoint/categories/get GET /v1/categories/{id} Fetch one `category` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Categories Source: https://docs.wellapp.ai/api-reference/endpoint/categories/get-all GET /v1/categories Workspace-scoped, cursor-paginated list of `category` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). # Get Chat Conversations Source: https://docs.wellapp.ai/api-reference/endpoint/chat-conversations/get GET /v1/chat-conversations/{id} Fetch one `chat_conversation` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Chat Conversations Source: https://docs.wellapp.ai/api-reference/endpoint/chat-conversations/get-all GET /v1/chat-conversations Workspace-scoped, cursor-paginated list of `chat_conversation` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "chat_conversations", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Get Checks Source: https://docs.wellapp.ai/api-reference/endpoint/checks/get GET /v1/checks/{id} Fetch one `check` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Checks Source: https://docs.wellapp.ai/api-reference/endpoint/checks/get-all GET /v1/checks Workspace-scoped, cursor-paginated list of `check` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | --------------- | ------------------------ | ------------------------------------------------------------------ | ------------------------------- | | `company` | to-one (company) | `{ "company": { "name": { "_eq": … } } }` | `"field": "company.name"` ✅ | | `people` | to-one (people) | `{ "people": { "full_name": { "_eq": … } } }` | `"field": "people.full_name"` ✅ | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `payment_means` | to-many (payment\_means) | `{ "payment_means": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "checks", "whereClause": { "company": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "company.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "checks", "whereClause": { "payment_means": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Create Company Source: https://docs.wellapp.ai/api-reference/endpoint/companies/create POST /v1/companies Create a new company record with comprehensive business information, registration details, tax information, and various relationships including people, workspaces, and other entities. # Delete Company Source: https://docs.wellapp.ai/api-reference/endpoint/companies/delete DELETE /v1/companies/{id} Delete a company record # Get Company by id Source: https://docs.wellapp.ai/api-reference/endpoint/companies/get GET /v1/companies/{id} Retrieve a specific company by ID # Get Companies Source: https://docs.wellapp.ai/api-reference/endpoint/companies/getAll GET /v1/companies Retrieve a paginated list of companies. By default, only basic company data is returned with relationship IDs. Use the 'include' parameter to get detailed relationship data. ## Complex Usage Example ### Advanced Company Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, relationship inclusion, business entity filtering, sorting, and pagination for retrieving companies: ```bash theme={null} curl -X GET "https://api.well.com/v1/companies?include=peoples,workspaces,emails,phones,web_links,locations,documents,media,categories,parents,subsidiaries&filter[workspace_id]=550e8400-e29b-41d4-a716-446655440000&filter[name]=TechCorp&filter[business_entity]=GmbH&filter[registration_tax_id]=12345678901&filter[registration_registered_value]=123456789&filter[country]=DE&filter[people_id]=550e8400-e29b-41d4-a716-446655440001&filter[email]=contact@techcorp.com&filter[phone_number]=+491234567890&filter[has_logo]=true&filter[created_at_from]=2024-01-01T00:00:00Z&filter[created_at_to]=2024-12-31T23:59:59Z&filter[updated_at_from]=2024-06-01T00:00:00Z&sort=-created_at&page[limit]=25&page[cursor]=eyJjcmVhdGVkX2F0IjoiMjAyNS0xMS0wMlQxMDozMDowMFoiLCJpZCI6ImNvbXBhbnk5LXV1aWQifQ==" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` This comprehensive query will: * **Include relationships**: Retrieve complete data for people, workspaces, contact information, and documents * **Filter by workspace**: Limit to companies in a specific workspace * **Search by name**: Find companies with "TechCorp" in their name * **Filter by business entity**: Only show German GmbH companies * **Registration filtering**: Filter by tax ID and registration numbers * **Geographic filtering**: Limit to companies in Germany * **Contact filtering**: Find companies with specific email or phone * **Media filtering**: Only show companies with logos * **Date range filtering**: Limit to companies created/updated in specific periods * **Sorting**: Order by creation date (newest first) * **Pagination**: Get 25 results per page with cursor-based pagination ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------------------ | --------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `source_workspace_connector` | to-one (workspace\_connector) | `{ "source_workspace_connector": { "name": { "_eq": … } } }` | `"field": "source_workspace_connector.name"` ✅ | | `relations` | to-many (company\_relation) | `{ "relations": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `people` | to-many (company\_person) | `{ "people": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `social_links` | to-many (company\_web\_link) | `{ "social_links": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `locations` | to-many (company\_location) | `{ "locations": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `emails` | to-many (company\_email) | `{ "emails": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `phones` | to-many (company\_phone) | `{ "phones": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `categories` | to-many (company\_category) | `{ "categories": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `media` | to-many (company\_media) | `{ "media": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `company_financial` | to-one (company\_financial) | `{ "company_financial": { "field_name": { "_eq": … } } }` | `"field": "company_financial.field_name"` ✅ | | `company_workspace_connectors` | to-many (company\_workspace\_connector) | `{ "company_workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "companies", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "companies", "whereClause": { "relations": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Remove Workspace from Company Source: https://docs.wellapp.ai/api-reference/endpoint/companies/remove-workspace-from-company DELETE /v1/companies/{companyId}/relationships/workspace/{workspaceId} Remove a workspace relationship from a company. This operation soft-deletes the relationship between the company and workspace. # Update Company Source: https://docs.wellapp.ai/api-reference/endpoint/companies/update PATCH /v1/companies/{id} Update an existing company record # Create Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/connectors/create POST /v1/connectors Create a new connector record with integration details, configuration, and various relationships including published by workspace and media assets. # Delete Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/connectors/delete DELETE /v1/connectors/{id} Permanently delete a specific connector by its unique ID. This action is irreversible and will remove all associated connector data including configuration, relationships, and integration settings. # Get Connectors by id Source: https://docs.wellapp.ai/api-reference/endpoint/connectors/get GET /v1/connectors/{id} Retrieve a specific connector by its unique identifier. This endpoint supports: **Include functionality**: Use the `include` parameter to get detailed relationship data including organization, category, and media information. **Relationship data**: Returns complete connector information with all attributes and relationships. # Get Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/connectors/get-all GET /v1/connectors Retrieve a list of connector records with advanced filtering, sorting, and include capabilities. This endpoint supports: **Default behavior**: Returns all connectors associated with the workspace_id and organization extracted from the authentication token. **Include functionality**: Use the `include` parameter to get full details of related resources in the response. **Date filtering**: Filter by creation or update date ranges using `created_at_from/to` and `updated_at_from/to`. **Relationship filtering**: Filter by related entities using `workspace_id`, `published_by`, etc. **Category filtering**: Filter by connector categories like `crm`, `api`, `database`, etc. **Status filtering**: Filter by connector status and data synchronization state. **Sorting**: Sort results by `created_at`, `updated_at`, `name`, or `category` in ascending or descending order. ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------------- | ---------------------------- | ------------------------------------------------------------------------- | -------------------------------------------- | | `workspace_connectors` | to-many (WorkspaceConnector) | `{ "workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `connector_filters` | to-many (ConnectorFilter) | `{ "connector_filters": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `provider_connector` | to-one (ProviderConnector) | `{ "provider_connector": { "field_name": { "_eq": … } } }` | `"field": "provider_connector.field_name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "connectors", "whereClause": { "provider_connector": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "provider_connector.field_name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "connectors", "whereClause": { "workspace_connectors": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Update Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/connectors/update PATCH /v1/connectors/{id} Update a connector record with integration details, configuration, and various relationships. All fields are optional for partial updates. # Create Document Source: https://docs.wellapp.ai/api-reference/endpoint/documents/create POST /v1/documents Create a new document record with file and metadata # Delete Document Source: https://docs.wellapp.ai/api-reference/endpoint/documents/delete DELETE /v1/documents/{id} # Get Document by id Source: https://docs.wellapp.ai/api-reference/endpoint/documents/get GET /v1/documents/{id} # Get Document Source: https://docs.wellapp.ai/api-reference/endpoint/documents/get-all GET /v1/documents Retrieve a list of documents with optional filtering, sorting, and relationship inclusion. By default, only document IDs are returned for relationships. ## Complex Usage Example ### Advanced Document Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, relationship inclusion, file type filtering, and sorting for retrieving documents: ```bash theme={null} curl -X GET "https://api.well.com/v1/documents?include=media,invoice&filter[workspace_id]=550e8400-e29b-41d4-a716-446655440000&filter[status]=completed&filter[file_type]=pdf&filter[uploaded_at][from]=2024-01-01T00:00:00Z&filter[uploaded_at][to]=2024-12-31T23:59:59Z&filter[processed_at][from]=2024-06-01T00:00:00Z&sort=-uploaded_at&limit=25" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------------------- | ------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `collect` | to-one (collect) | `{ "collect": { "field_name": { "_eq": … } } }` | `"field": "collect.field_name"` ✅ | | `source_workspace_connector` | to-one (workspace\_connector) | `{ "source_workspace_connector": { "name": { "_eq": … } } }` | `"field": "source_workspace_connector.name"` ✅ | | `invoices` | to-many (invoice) | `{ "invoices": { "_some": { "grand_total": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `transaction_documents` | to-many (transaction\_document) | `{ "transaction_documents": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "documents", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "documents", "whereClause": { "invoices": { "_some": { "grand_total": { "_gt": 0 } } } } } ``` # Create Email Source: https://docs.wellapp.ai/api-reference/endpoint/emails/create-email POST /v1/emails Create a new email address with relationships to people, companies, or workspaces # Delete Email Source: https://docs.wellapp.ai/api-reference/endpoint/emails/delete-email DELETE /v1/emails/{id} Remove an email address from a person, company, or workspace # Get Email by ID Source: https://docs.wellapp.ai/api-reference/endpoint/emails/get-email-by-id GET /v1/emails/{id} Retrieve a specific email by its UUID or email address with optional relationship details # Get Emails Source: https://docs.wellapp.ai/api-reference/endpoint/emails/get-emails GET /v1/emails Retrieve a list of email addresses with advanced filtering, sorting, and include capabilities. This endpoint supports: **Default behavior**: Returns email addresses with basic information (IDs only for relationships). **Trailing slashes**: Works with or without trailing slashes '/'. **Include functionality**: Use the `include` parameter to get detailed relationship data. **Date filtering**: Filter by creation, update, or deletion date ranges. **Relationship filtering**: Filter by related entities using various identifiers. **Contact filtering**: Filter by contact information and verification status. **Company filtering**: Filter by company registration details. **Email filtering**: Filter by primary status, verification, and label. **Sorting**: Sort results by date fields in ascending or descending order. ## Complex Usage Example ### Advanced Email Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, relationship inclusion, sorting, and pagination: ```bash theme={null} curl -X GET "https://api.well.com/v1/emails?include=persons,companies,workspaces&filter[company_id]=550e8400-e29b-41d4-a716-446655440000&filter[is_verified]=true&filter[label]=work&filter[created_at_from]=2023-01-01T00:00:00Z&filter[created_at_to]=2023-12-31T23:59:59Z&sort=-created_at&page[limit]=10&page[cursor]=eyJjcmVhdGVkX2F0IjoiMjAyMy0xMS0wMlQxMDozMDowMFoiLCJpZCI6ImVtYWlsOS11dWlkIn0=" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------- | ------------------------ | ------------------------------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `company_emails` | to-many (company\_email) | `{ "company_emails": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `person_emails` | to-many (person\_email) | `{ "person_emails": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "emails", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "emails", "whereClause": { "company_emails": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Get Exchange Rates Source: https://docs.wellapp.ai/api-reference/endpoint/exchange-rates/get GET /v1/exchange-rates/{id} Fetch one `exchange_rate` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Exchange Rates Source: https://docs.wellapp.ai/api-reference/endpoint/exchange-rates/get-all GET /v1/exchange-rates Workspace-scoped, cursor-paginated list of `exchange_rate` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "exchange_rates", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Delete Invoice items Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-items/delete DELETE /v1/invoice-items/{id} Permanently delete a specific invoice item by ID. This action cannot be undone. # Get Invoice Items Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-items/get GET /v1/invoice-items/{id} Fetch one `invoice_item` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Invoice Items Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-items/get-all GET /v1/invoice-items Workspace-scoped, cursor-paginated list of `invoice_item` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------ | ------------------------ | -------------------------------------------------------- | ------------------------------------------ | | `invoice` | to-one (invoice) | `{ "invoice": { "grand_total": { "_eq": … } } }` | `"field": "invoice.grand_total"` ✅ | | `ledger_account` | to-one (ledger\_account) | `{ "ledger_account": { "name": { "_eq": … } } }` | `"field": "ledger_account.name"` ✅ | | `applied_tax_rate` | to-one (tax\_rate) | `{ "applied_tax_rate": { "field_name": { "_eq": … } } }` | `"field": "applied_tax_rate.field_name"` ✅ | | `media` | to-one (media) | `{ "media": { "file_name": { "_eq": … } } }` | `"field": "media.file_name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "invoice_items", "whereClause": { "invoice": { "grand_total": { "_ilike": "%acme%" } } }, "orderBy": { "field": "invoice.grand_total", "direction": "asc" } } ``` # Patch Invoice items Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-items/patch PATCH /v1/invoice-items/{id} Create a new invoice item with detailed product information, pricing, taxes, and period specifications. # Create Invoice items Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-items/post POST /v1/invoice-items Create a new invoice item with detailed product information, pricing, taxes, and period specifications. # Get Invoice Transactions Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-transactions/get GET /v1/invoice-transactions/{id} Fetch one `invoice_transaction` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Invoice Transactions Source: https://docs.wellapp.ai/api-reference/endpoint/invoice-transactions/get-all GET /v1/invoice-transactions Workspace-scoped, cursor-paginated list of `invoice_transaction` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | --------------- | ----------------------- | ---------------------------------------------------------- | -------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `invoice` | to-one (invoice) | `{ "invoice": { "grand_total": { "_eq": … } } }` | `"field": "invoice.grand_total"` ✅ | | `transaction` | to-one (transaction) | `{ "transaction": { "instructed_amount": { "_eq": … } } }` | `"field": "transaction.instructed_amount"` ✅ | | `subscription` | to-one (subscription) | `{ "subscription": { "status": { "_eq": … } } }` | `"field": "subscription.status"` ✅ | | `exchange_rate` | to-one (exchange\_rate) | `{ "exchange_rate": { "rate_date": { "_eq": … } } }` | `"field": "exchange_rate.rate_date"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "invoice_transactions", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Delete Invoice Source: https://docs.wellapp.ai/api-reference/endpoint/invoices/delete DELETE /v1/invoices/{id} Remove an invoice by its unique ID. This action is irreversible. # Get Invoice Source: https://docs.wellapp.ai/api-reference/endpoint/invoices/get GET /v1/invoices/{id} Fetch detailed information about a single invoice by ID. # Get Invoices Source: https://docs.wellapp.ai/api-reference/endpoint/invoices/get-all GET /v1/invoices Retrieve a list of invoices filtered by issuer, receiver, status or date range. Supports pagination following JSON:API specification. ## Complex Usage Example ### Advanced Invoice Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, status filtering, company relationships, and pagination for retrieving invoices: ```bash theme={null} curl -X GET "https://api.well.com/v1/invoices?issuer_id=550e8400-e29b-41d4-a716-446655440001&receiver_id=550e8400-e29b-41d4-a716-446655440002&status=sent&date_from=2025-01-01&date_to=2025-12-31&page[limit]=20&page[cursor]=eyJkYXRlIjoiMjAyNS0xMS0wMiIsImlkIjoiaW52b2ljZTktdXVpZCJ9" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------------------ | --------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------- | | `issuer` | to-one (company) | `{ "issuer": { "name": { "_eq": … } } }` | `"field": "issuer.name"` ✅ | | `receiver` | to-one (company) | `{ "receiver": { "name": { "_eq": … } } }` | `"field": "receiver.name"` ✅ | | `document` | to-one (document) | `{ "document": { "file_name": { "_eq": … } } }` | `"field": "document.file_name"` ✅ | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `source_workspace_connector` | to-one (workspace\_connector) | `{ "source_workspace_connector": { "name": { "_eq": … } } }` | `"field": "source_workspace_connector.name"` ✅ | | `exchange_rate` | to-one (exchange\_rate) | `{ "exchange_rate": { "rate_date": { "_eq": … } } }` | `"field": "exchange_rate.rate_date"` ✅ | | `subscription` | to-one (subscription) | `{ "subscription": { "status": { "_eq": … } } }` | `"field": "subscription.status"` ✅ | | `invoice_items` | to-many (invoice\_item) | `{ "invoice_items": { "_some": { "amount": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `invoice_transactions` | to-many (invoice\_transaction) | `{ "invoice_transactions": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `payment_means` | to-many (invoice\_payment\_means) | `{ "payment_means": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `invoice_workspace_connectors` | to-many (invoice\_workspace\_connector) | `{ "invoice_workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "invoices", "whereClause": { "issuer": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "issuer.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "invoices", "whereClause": { "invoice_items": { "_some": { "amount": { "_gt": 0 } } } } } ``` # Patch Invoice Source: https://docs.wellapp.ai/api-reference/endpoint/invoices/patch PATCH /v1/invoices/{id} Modify an existing invoice by its unique ID. # Create Invoices Source: https://docs.wellapp.ai/api-reference/endpoint/invoices/post POST /v1/invoices Create a new invoice with comprehensive details including issuer/receiver relationships, payment terms, line items, and document attachments. Supports both basic invoice creation and complex scenarios with detailed billing contexts. # Get Journal Entries Source: https://docs.wellapp.ai/api-reference/endpoint/journal-entries/get GET /v1/journal-entries/{id} Fetch one `journal_entry` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Journal Entries Source: https://docs.wellapp.ai/api-reference/endpoint/journal-entries/get-all GET /v1/journal-entries Workspace-scoped, cursor-paginated list of `journal_entry` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------------------- | ------------------------------ | ----------------------------------------------------------- | --------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `journal` | to-one (journal) | `{ "journal": { "name": { "_eq": … } } }` | `"field": "journal.name"` ✅ | | `validated_by` | to-one (people) | `{ "validated_by": { "full_name": { "_eq": … } } }` | `"field": "validated_by.full_name"` ✅ | | `invoice_transaction` | to-one (invoice\_transaction) | `{ "invoice_transaction": { "field_name": { "_eq": … } } }` | `"field": "invoice_transaction.field_name"` ✅ | | `lines` | to-many (journal\_entry\_line) | `{ "lines": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `sourceWorkspaceConnector` | to-one (workspace\_connector) | `{ "sourceWorkspaceConnector": { "name": { "_eq": … } } }` | `"field": "sourceWorkspaceConnector.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "journal_entries", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "journal_entries", "whereClause": { "lines": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Get Journals Source: https://docs.wellapp.ai/api-reference/endpoint/journals/get GET /v1/journals/{id} Fetch one `journal` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Journals Source: https://docs.wellapp.ai/api-reference/endpoint/journals/get-all GET /v1/journals Workspace-scoped, cursor-paginated list of `journal` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------------------- | ----------------------------- | ------------------------------------------------------------ | ---------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `default_debit_account` | to-one (ledger\_account) | `{ "default_debit_account": { "name": { "_eq": … } } }` | `"field": "default_debit_account.name"` ✅ | | `default_credit_account` | to-one (ledger\_account) | `{ "default_credit_account": { "name": { "_eq": … } } }` | `"field": "default_credit_account.name"` ✅ | | `source_workspace_connector` | to-one (workspace\_connector) | `{ "source_workspace_connector": { "name": { "_eq": … } } }` | `"field": "source_workspace_connector.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "journals", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Get Ledger Accounts Source: https://docs.wellapp.ai/api-reference/endpoint/ledger-accounts/get GET /v1/ledger-accounts/{id} Fetch one `ledger_account` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Ledger Accounts Source: https://docs.wellapp.ai/api-reference/endpoint/ledger-accounts/get-all GET /v1/ledger-accounts Workspace-scoped, cursor-paginated list of `ledger_account` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------------------- | ----------------------------- | ------------------------------------------------------------- | ---------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `parent_account` | to-one (ledger\_account) | `{ "parent_account": { "name": { "_eq": … } } }` | `"field": "parent_account.name"` ✅ | | `child_accounts` | to-many (ledger\_account) | `{ "child_accounts": { "_some": { "name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `source_workspace_connector` | to-one (workspace\_connector) | `{ "source_workspace_connector": { "name": { "_eq": … } } }` | `"field": "source_workspace_connector.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "ledger_accounts", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "ledger_accounts", "whereClause": { "child_accounts": { "_some": { "name": { "_gt": 0 } } } } } ``` # Create Location Source: https://docs.wellapp.ai/api-reference/endpoint/location/create POST /v1/locations Create a new address/location with geographic coordinates and associate it with people, companies, or workspaces. # Delete Location Source: https://docs.wellapp.ai/api-reference/endpoint/location/delete DELETE /v1/locations/{id} Remove a location from a person, company, or workspace # Get Location Source: https://docs.wellapp.ai/api-reference/endpoint/location/get GET /v1/locations Workspace-scoped, cursor-paginated list of `location` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Complex Usage Example ### Advanced Location Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, relationship inclusion, status filtering, and sorting for retrieving locations: ```bash theme={null} curl -X GET "https://api.well.com/v1/locations?include=persons,companies,workspaces&workspace_id=550e8400-e29b-41d4-a716-446655440000&company_id=550e8400-e29b-41d4-a716-446655440001&is_registered=true&is_primary=true&created_at_from=2023-01-01T00:00:00Z&created_at_to=2023-12-31T23:59:59Z&updated_at_from=2023-06-01T00:00:00Z&sort=-created_at&page[limit]=20&page[cursor]=eyJjcmVhdGVkX2F0IjoiMjAyMy0xMS0wMlQxMDozMDowMFoiLCJpZCI6ImxvY2F0aW9uOS11dWlkIn0=" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` # Get Location by id Source: https://docs.wellapp.ai/api-reference/endpoint/location/get-by-id GET /v1/locations/{id} Fetch one `location` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # Get Locations Source: https://docs.wellapp.ai/api-reference/endpoint/locations/get GET /v1/locations/{id} Fetch one `location` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Locations Source: https://docs.wellapp.ai/api-reference/endpoint/locations/get-all GET /v1/locations Workspace-scoped, cursor-paginated list of `location` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------- | --------------------------- | ---------------------------------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `company_locations` | to-many (company\_location) | `{ "company_locations": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `person_locations` | to-many (person\_location) | `{ "person_locations": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "locations", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "locations", "whereClause": { "company_locations": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Create Media Source: https://docs.wellapp.ai/api-reference/endpoint/media/create POST /v1/medias Create a new media record with relationships to people, companies, or workspaces # Delete Media Source: https://docs.wellapp.ai/api-reference/endpoint/media/delete DELETE /v1/medias/{id} Delete an existing media record and its associated file # Get Media Source: https://docs.wellapp.ai/api-reference/endpoint/media/get GET /v1/media/{id} Fetch one `media` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Media Source: https://docs.wellapp.ai/api-reference/endpoint/media/get-all GET /v1/media Workspace-scoped, cursor-paginated list of `media` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "media", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Create Membership Source: https://docs.wellapp.ai/api-reference/endpoint/memberships/create POST /v1/memberships Create a new membership linking a person to a workspace with a specific role # Delete Membership Source: https://docs.wellapp.ai/api-reference/endpoint/memberships/delete DELETE /v1/memberships/{id} Remove a person from a workspace # Get Membership Source: https://docs.wellapp.ai/api-reference/endpoint/memberships/get GET /v1/memberships/{id} Retrieve a specific membership by ID # Get All Memberships Source: https://docs.wellapp.ai/api-reference/endpoint/memberships/get-all GET /v1/memberships Retrieve memberships with optional filtering by person ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------------- | ----------------------------------- | | `person` | to-one (people) | `{ "person": { "full_name": { "_eq": … } } }` | `"field": "person.full_name"` ✅ | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `invited_by` | to-one (people) | `{ "invited_by": { "full_name": { "_eq": … } } }` | `"field": "invited_by.full_name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "memberships", "whereClause": { "person": { "full_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "person.full_name", "direction": "asc" } } ``` # Update Membership Source: https://docs.wellapp.ai/api-reference/endpoint/memberships/update PATCH /v1/memberships/{id} Update an existing membership role # Create Payment Mean Source: https://docs.wellapp.ai/api-reference/endpoint/payment-means/create POST /v1/payment-means Create a new payment method or account for financial transactions. This endpoint supports various payment means types including bank accounts (IBAN/SWIFT), credit/debit cards, digital wallets, and other payment instruments. # Delete Payment Mean Source: https://docs.wellapp.ai/api-reference/endpoint/payment-means/delete DELETE /v1/payment-means/{id} Delete a specific payment method or account by its unique ID. # Get Payment Means Source: https://docs.wellapp.ai/api-reference/endpoint/payment-means/get GET /v1/payment-means/{id} Fetch one `payment_means` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Payment Means Source: https://docs.wellapp.ai/api-reference/endpoint/payment-means/get-all GET /v1/payment-means Workspace-scoped, cursor-paginated list of `payment_means` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------------------- | --------------------------- | ---------------------------------------------------------------- | -------------------------------------------------- | | `workspace` | to-one (Workspace) | `{ "workspace": { "field_name": { "_eq": … } } }` | `"field": "workspace.field_name"` ✅ | | `account` | to-one (Account) | `{ "account": { "field_name": { "_eq": … } } }` | `"field": "account.field_name"` ✅ | | `card` | to-one (Card) | `{ "card": { "field_name": { "_eq": … } } }` | `"field": "card.field_name"` ✅ | | `check` | to-one (Check) | `{ "check": { "field_name": { "_eq": … } } }` | `"field": "check.field_name"` ✅ | | `company` | to-one (Company) | `{ "company": { "field_name": { "_eq": … } } }` | `"field": "company.field_name"` ✅ | | `people` | to-one (People) | `{ "people": { "field_name": { "_eq": … } } }` | `"field": "people.field_name"` ✅ | | `sourceWorkspaceConnector` | to-one (WorkspaceConnector) | `{ "sourceWorkspaceConnector": { "field_name": { "_eq": … } } }` | `"field": "sourceWorkspaceConnector.field_name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "payment_means", "whereClause": { "workspace": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.field_name", "direction": "asc" } } ``` # Update Payment Mean Source: https://docs.wellapp.ai/api-reference/endpoint/payment-means/update PATCH /v1/payment-means/{id} Update an existing payment method or account for financial transactions. This endpoint supports partial updates to payment means including bank accounts (IBAN/SWIFT), credit/debit cards, digital wallets, and other payment instruments. # Link Companies to Person Source: https://docs.wellapp.ai/api-reference/endpoint/people/add-companies-to-person POST /v1/people/{peopleId}/relationships/companies Link existing companies to a person # Create Person Source: https://docs.wellapp.ai/api-reference/endpoint/people/create POST /v1/people Create a new person record with optional emails, phone numbers, company relationships, and social links. If a person with the same name and primary email already exists, returns the existing person instead of creating a duplicate. # Delete Person Source: https://docs.wellapp.ai/api-reference/endpoint/people/delete DELETE /v1/people/{id} Delete a person record # Get Person Source: https://docs.wellapp.ai/api-reference/endpoint/people/get GET /v1/people/{id} Retrieve a single person by ID with related emails, phones, and social links # List People Source: https://docs.wellapp.ai/api-reference/endpoint/people/get-all GET /v1/people Page through a workspace's people. Filtering and sorting (including across related objects) is done through the records query. `GET /v1/people` returns the workspace's people (contacts), **page-paginated** with `page` and `limit` (max 100). It does **not** accept inline filtering or sorting — those query params are ignored. To filter or sort people — including across related objects — use the records query: `POST /v1/records/query` with `root: "people"`. It exposes the full `filters` / raw `whereClause` and `orderBy` model. For the operator set and pagination notes see [Filtering & sorting](/api-reference/filtering-and-sorting). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ----------------------------- | ---------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `source_workspace_connector` | to-one (workspaceconnector) | `{ "source_workspace_connector": { "field_name": { "_eq": … } } }` | `"field": "source_workspace_connector.field_name"` ✅ | | `media` | to-one (media) | `{ "media": { "file_name": { "_eq": … } } }` | `"field": "media.file_name"` ✅ | | `emails` | to-many (personemail) | `{ "emails": { "_some": { "address": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `phones` | to-many (personphone) | `{ "phones": { "_some": { "number": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `locations` | to-many (personlocation) | `{ "locations": { "_some": { "city": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `web_links` | to-many (personweblink) | `{ "web_links": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `companies` | to-many (companyperson) | `{ "companies": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `memberships` | to-many (membership) | `{ "memberships": { "_some": { "role": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `collect` | to-many (collect) | `{ "collect": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `workspace_connectors` | to-many (workspaceconnector) | `{ "workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `people_workspace_connectors` | to-many (peopleworkspaceconnector) | `{ "people_workspace_connectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "people", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "people", "whereClause": { "emails": { "_some": { "address": { "_ilike": "%@acme.com" } } } } } ``` # Create Phone Source: https://docs.wellapp.ai/api-reference/endpoint/phones/create-phone POST /v1/phones Create a new phone number with relationships to people, companies, or workspaces # Create Phone Verification Code Source: https://docs.wellapp.ai/api-reference/endpoint/phones/create-phone-verification-code POST /v1/people/{personId}/phones/{phoneId}/verify/{code} Validate the verification code received by WhatsApp for a phone number. If the code is valid, sets the is_verify field to true for the person's phone. # Verify Phone Number Source: https://docs.wellapp.ai/api-reference/endpoint/phones/create-verify-phone-number POST /v1/people/{personId}/phones/{phoneId}/verify Send verification code by whatsapp to a phone number for verification # Delete Person Phone Source: https://docs.wellapp.ai/api-reference/endpoint/phones/delete-phone DELETE /v1/phones/{id} Remove a phone number from a person # Get Phones Source: https://docs.wellapp.ai/api-reference/endpoint/phones/get GET /v1/phones/{id} Fetch one `phone` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Phones Source: https://docs.wellapp.ai/api-reference/endpoint/phones/get-all GET /v1/phones Workspace-scoped, cursor-paginated list of `phone` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ---------------- | ------------------------ | ------------------------------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `person_phones` | to-many (person\_phone) | `{ "person_phones": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `company_phones` | to-many (company\_phone) | `{ "company_phones": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "phones", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "phones", "whereClause": { "person_phones": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Get Tax Rates Source: https://docs.wellapp.ai/api-reference/endpoint/tax-rates/get GET /v1/tax-rates/{id} Fetch one `tax_rate` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Tax Rates Source: https://docs.wellapp.ai/api-reference/endpoint/tax-rates/get-all GET /v1/tax-rates Workspace-scoped, cursor-paginated list of `tax_rate` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------------- | ------------------------ | ---------------------------------------------------- | -------------------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | | `collected_account` | to-one (ledger\_account) | `{ "collected_account": { "name": { "_eq": … } } }` | `"field": "collected_account.name"` ✅ | | `deductible_account` | to-one (ledger\_account) | `{ "deductible_account": { "name": { "_eq": … } } }` | `"field": "deductible_account.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "tax_rates", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Create Transaction Source: https://docs.wellapp.ai/api-reference/endpoint/transactions/create POST /v1/transactions Create a new financial transaction record with comprehensive payment details, foreign exchange information, remittance data, fees, and relationship associations. # Delete Transaction Source: https://docs.wellapp.ai/api-reference/endpoint/transactions/delete DELETE /v1/transactions/{id} Delete a specific transaction by its unique ID or perform a soft delete based on business rules. # Get Transactions Source: https://docs.wellapp.ai/api-reference/endpoint/transactions/get GET /v1/transactions/{id} Fetch one `transaction` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Transactions Source: https://docs.wellapp.ai/api-reference/endpoint/transactions/get-all GET /v1/transactions Workspace-scoped, cursor-paginated list of `transaction` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------- | | `workspace` | to-one (Workspace) | `{ "workspace": { "field_name": { "_eq": … } } }` | `"field": "workspace.field_name"` ✅ | | `debtor_payment_means` | to-one (PaymentMeans) | `{ "debtor_payment_means": { "field_name": { "_eq": … } } }` | `"field": "debtor_payment_means.field_name"` ✅ | | `creditor_payment_means` | to-one (PaymentMeans) | `{ "creditor_payment_means": { "field_name": { "_eq": … } } }` | `"field": "creditor_payment_means.field_name"` ✅ | | `account_balance` | to-one (AccountBalance) | `{ "account_balance": { "field_name": { "_eq": … } } }` | `"field": "account_balance.field_name"` ✅ | | `sourceWorkspaceConnector` | to-one (WorkspaceConnector) | `{ "sourceWorkspaceConnector": { "field_name": { "_eq": … } } }` | `"field": "sourceWorkspaceConnector.field_name"` ✅ | | `ledger_account` | to-one (LedgerAccount) | `{ "ledger_account": { "field_name": { "_eq": … } } }` | `"field": "ledger_account.field_name"` ✅ | | `transaction_documents` | to-many (TransactionDocument) | `{ "transaction_documents": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `transactionWorkspaceConnectors` | to-many (TransactionWorkspaceConnector) | `{ "transactionWorkspaceConnectors": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "transactions", "whereClause": { "workspace": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.field_name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "transactions", "whereClause": { "transaction_documents": { "_some": { "field_name": { "_gt": 0 } } } } } ``` # Update Transaction Source: https://docs.wellapp.ai/api-reference/endpoint/transactions/update PATCH /v1/transactions/{id} Update a financial transaction record with comprehensive payment details, foreign exchange information, remittance data, fees, and relationship associations. This endpoint supports: **Partial updates**: All fields are optional - only provide the fields you want to update. **Status management**: Update transaction status from pending to completed, failed, or cancelled. **Relationship updates**: Modify debtor/creditor payment means, entities, documents, and workspace associations. **Financial adjustments**: Update amounts, foreign exchange rates, fees, and remittance information. **Audit trail**: Automatically updates the `updated_at` timestamp while preserving creation history. **Data integrity**: Validates business rules and maintains consistency across related entities. # Create web link Source: https://docs.wellapp.ai/api-reference/endpoint/web-link/create POST /v1/web-links Create a web or social media link and associate it with people, companies, or workspaces. # Delete web-link Source: https://docs.wellapp.ai/api-reference/endpoint/web-link/delete Delete /v1/web-links/{id} Delete a web link by ID # Get web-Link Source: https://docs.wellapp.ai/api-reference/endpoint/web-link/get Get /v1/web-links Workspace-scoped, cursor-paginated list of `social_link` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Complex Usage Example ### Advanced Web Links Filtering with Full Context This example demonstrates advanced filtering with multiple parameters, relationship inclusion, and sorting for retrieving web links and social media profiles: ```bash theme={null} curl -X GET "https://api.well.com/v1/web-links?include=workspace,persons,companies&filter[workspace_id]=123e4567-e89b-12d3-a456-426614174000&filter[external_workspace_id]=ext_workspace_123&filter[created_at]=2025-01-01T00:00:00Z&filter[updated_at]=2025-01-01T00:00:00Z&sort=-created_at" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` # Get web-Link by id Source: https://docs.wellapp.ai/api-reference/endpoint/web-link/get-by-id Get /v1/web-links/{id} Fetch one `social_link` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # Get Web Links Source: https://docs.wellapp.ai/api-reference/endpoint/web-links/get GET /v1/web-links/{id} Fetch one `social_link` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Web Links Source: https://docs.wellapp.ai/api-reference/endpoint/web-links/get-all GET /v1/web-links Workspace-scoped, cursor-paginated list of `social_link` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------- | ----------------------------- | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "web_links", "whereClause": { "workspace": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspace.name", "direction": "asc" } } ``` # Create Webhook Source: https://docs.wellapp.ai/api-reference/endpoint/webhooks/create POST /v1/subscriptions/webhooks # Delete Webhook Source: https://docs.wellapp.ai/api-reference/endpoint/webhooks/delete DELETE /v1/subscriptions/webhooks/{id} # Get Webhook Source: https://docs.wellapp.ai/api-reference/endpoint/webhooks/get GET /v1/subscriptions/webhooks/{id} # Get Webhooks Source: https://docs.wellapp.ai/api-reference/endpoint/webhooks/get-all GET /v1/subscriptions/webhooks # Update Webhook Source: https://docs.wellapp.ai/api-reference/endpoint/webhooks/update PATCH /v1/subscriptions/webhooks/{id} # Get Workspace Connector Sync Logs Source: https://docs.wellapp.ai/api-reference/endpoint/workspace-connector-sync-logs/get GET /v1/workspace-connector-sync-logs/{id} Fetch one `workspace_connector_sync_log` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Workspace Connector Sync Logs Source: https://docs.wellapp.ai/api-reference/endpoint/workspace-connector-sync-logs/get-all GET /v1/workspace-connector-sync-logs Workspace-scoped, cursor-paginated list of `workspace_connector_sync_log` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | -------------------- | ----------------------------- | ---------------------------------------------------- | -------------------------------------- | | `workspaceConnector` | to-one (workspace\_connector) | `{ "workspaceConnector": { "name": { "_eq": … } } }` | `"field": "workspaceConnector.name"` ✅ | | `workspace` | to-one (workspace) | `{ "workspace": { "name": { "_eq": … } } }` | `"field": "workspace.name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "workspace_connector_sync_logs", "whereClause": { "workspaceConnector": { "name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "workspaceConnector.name", "direction": "asc" } } ``` # Get Workspace Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/workspace-connectors/get GET /v1/workspace-connectors/{id} Fetch one `workspace_connector` by id, workspace-scoped. Returns 404 when the id is outside the caller's workspace. # List Workspace Connectors Source: https://docs.wellapp.ai/api-reference/endpoint/workspace-connectors/get-all GET /v1/workspace-connectors Workspace-scoped, cursor-paginated list of `workspace_connector` records. Served by the generic read-only resource handler over the data-views pipeline (Hasura row-level security). ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------ | ------------------ | ------------------------------------------------- | ----------------------------------- | | `connector` | to-one (Connector) | `{ "connector": { "field_name": { "_eq": … } } }` | `"field": "connector.field_name"` ✅ | | `workspace` | to-one (Workspace) | `{ "workspace": { "field_name": { "_eq": … } } }` | `"field": "workspace.field_name"` ✅ | | `person` | to-one (People) | `{ "person": { "field_name": { "_eq": … } } }` | `"field": "person.field_name"` ✅ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "workspace_connectors", "whereClause": { "connector": { "field_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "connector.field_name", "direction": "asc" } } ``` # Create Workspace Source: https://docs.wellapp.ai/api-reference/endpoint/workspaces/create POST /v1/workspaces Create a new workspace record # Delete Workspace Source: https://docs.wellapp.ai/api-reference/endpoint/workspaces/delete DELETE /v1/workspaces/{id} Delete a workspace record # Get Workspace Source: https://docs.wellapp.ai/api-reference/endpoint/workspaces/get GET /v1/workspaces/{id} Retrieve a specific workspace by ID # List Workspaces Source: https://docs.wellapp.ai/api-reference/endpoint/workspaces/get-all Query workspaces with filtering and sorting — including across related objects. Workspaces are listed through the records query: `POST /v1/records/query` with `root: "workspaces"` (a dedicated `GET /v1/workspaces` list endpoint is planned). It accepts the full `filters` / raw `whereClause` and `orderBy` model — see [Filtering & sorting](/api-reference/filtering-and-sorting) for the operator set and pagination notes. ## Filtering & sorting on related objects Filter and sort on a **related (included) object** by nesting into the relationship — the same relationships you pass to `include`. * **To-one** relations nest directly: `{ "issuer": { "name": { "_eq": "Acme" } } }`, and sort by a dot-path: `"orderBy": { "field": "issuer.name", "direction": "asc" }`. * **To-many** relations must be quantified with `_some`, `_every`, or `_none`: `{ "items": { "_some": { "quantity": { "_gt": 1 } } } }`. They are **not** directly sortable (a to-many sort needs an aggregate proxy). * **Composite** `composite_*` columns are virtual — filter and sort on their underlying source fields, not on the composite. Workspace row-level security applies at every hop, so a related-object filter never widens your tenant scope. Full operator set, deep-nesting rules, and pagination notes: [Filtering & sorting](/api-reference/filtering-and-sorting). This resource's related objects (the ones you can `include`) and how to filter or sort on each: | Relationship | Cardinality | Filter on a field | Sort by a field | | ------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------- | | `media` | to-one (media) | `{ "media": { "file_name": { "_eq": … } } }` | `"field": "media.file_name"` ✅ | | `parent_workspace` | to-one (workspace) | `{ "parent_workspace": { "name": { "_eq": … } } }` | `"field": "parent_workspace.name"` ✅ | | `own_company` | to-one (company) | `{ "own_company": { "name": { "_eq": … } } }` | `"field": "own_company.name"` ✅ | | `child_workspaces` | to-many (workspace) | `{ "child_workspaces": { "_some": { "name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `memberships` | to-many (membership) | `{ "memberships": { "_some": { "role": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `webhooks` | to-many (webhook) | `{ "webhooks": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | | `workspace_accounting_settings` | to-one (workspace\_accounting\_settings) | `{ "workspace_accounting_settings": { "field_name": { "_eq": … } } }` | `"field": "workspace_accounting_settings.field_name"` ✅ | | `workspace_providers` | to-many (workspace\_provider) | `{ "workspace_providers": { "_some": { "field_name": { "_eq": … } } } }` | aggregate proxy only ⚠️ | Replace `field_name` with any field of the related object. See its object-reference page for the full field list. **Filter by a to-one relation** (and sort by it): ```json theme={null} { "root": "workspaces", "whereClause": { "media": { "file_name": { "_ilike": "%acme%" } } }, "orderBy": { "field": "media.file_name", "direction": "asc" } } ``` **Filter by a to-many relation** (quantified — bare nesting is invalid): ```json theme={null} { "root": "workspaces", "whereClause": { "child_workspaces": { "_some": { "name": { "_ilike": "%eu%" } } } } } ``` # Update Workspace Source: https://docs.wellapp.ai/api-reference/endpoint/workspaces/update PATCH /v1/workspaces/{id} Update an existing workspace record # Get started Source: https://docs.wellapp.ai/api-reference/get-started ## 1.1 Authentication Well API Dashboard 1. Log into the Well web application: [https://app.test.wellapp.ai/](https://app.test.wellapp.ai/) 2. Create a new workspace for your organization. 3. Keep the Workspace ID safe — it will be needed for API calls. ## 1.2 Generate an API Token Well API Dashboard 1. Go in the app in the settings to generate your API key. [https://app.test.wellapp.ai/app](https://app.test.wellapp.ai/app) 2. This token will authenticate all future API calls. 3. Store it securely; treat it like a password. ## 2. Basic Flow ### Create a Workspace ```bash theme={null} POST /v1/workspaces ``` ```json theme={null} { "data": { "type": "workspace", "attributes": { "name": "Customer Workspace", "external_workspace_id": "your-customer-id" } } } ``` ## 2.1. Create as many workspaces as you have customers Whenever one of your customer will install the connector in the instance of a workspace, we will create the people object and link it with the object. If you don’t want to store our ID of workspace, you have the capacity to include your own `external_workspace_id` as a reference of the workspace created. It’s optional. For any new customer that wants to install our connector, workspace creation is considered as a pre-requirement. ### Set Up a Webhook ```bash theme={null} POST /v1/webhooks ``` ```json theme={null} { "data": { "type": "webhook", "attributes": { "url": "https://your-app.com/webhook", "events": ["document.uploaded", "company.created"], "workspace_id": "workspace_id" } } } ``` ## 2.2. Configure a Webhook per workspace To be notified whenever a new document is uploaded in the context of a workspace, and following the installation of gmail driver, you must create one webhook. * In each event payload that you subscribe you will get `workspace_id`,`external_workspace_id` and `people_id` as attributes. * It’s possible to filter out a webhook per workspace and only receive events for a given `workspace_id` or `external_workspace_id`. ## 2.3. To get the installation link of the Gmail driver, you will need to call a dedicated endpoint. This endpoint looks like GET `/v1/workspaces/{{workspace_id}}/connectors/gmail/access?redirect_url&state=xx` If you are using external\_workspace\_id as identifier of a workspace, then replace as is: `/v1/workspaces/{{external_workspace_id}}/connectors/gmail/access?redirect_url&state=xx` Redirect\_url is optional. When not present we may redirect customer to our landing page. If present we redirect at the end of the flow. **Receive events:** ```javascript theme={null} app.post("/webhook", (req, res) => { const { event, data } = req.body; // Process event res.status(200).send("OK"); }); ``` **Supported events:** `company.created`, `company.updated`, `company.deleted`, `document.uploaded`, `document.processed`, `document.deleted` **Retry policy:** 3 retries with exponential backoff, 10s timeout ## 2.4. Get the status of the Gmail driver connector by fetching the resources ### Install Gmail Connector GET `/v1/workspaces/{{external_workspace_id}}/connectors/gmail/` ```jsx theme={null} { "data": { "type": "connector", "attributes": { "connector_type": "gmail", "external_workspace_id": "fygr-test-123", "expires_at": "2025-09-04T14:26:13.905Z", "status": "active" }, "relationship": { "created_by": { "data": { "type": "people", "id": "people1" } }, } } } ``` Get installation link: ```bash theme={null} GET /v1/workspaces/{workspace_id}/connectors/gmail/access?redirect_url=https://your-app.com ``` Returns connector `status`: `active`, `inactive`, or `expired`. # Graphing library Source: https://docs.wellapp.ai/api-reference/graphing-library In progress... # Use cases Source: https://docs.wellapp.ai/api-reference/use-cases Explore practical implementation guides and integration examples for common use cases with the Well Core API. ## Featured Collections Our Gmail driver works on centralizing your invoices and receipts, so that you won't miss any important documents Our Outlook driver works on centralizing your invoices and receipts, so that you won't miss any important documents Our WhatsApp connector to make invoice and receipt retrieval as simple as a forward Our Chrome extension connector thats transfrom any website into an API to retrieve invoice and receipts Learn how to manage and configure connectors for your workspace # Webhooks Source: https://docs.wellapp.ai/api-reference/webhooks Set up real-time notifications to receive updates from the Well Core API when important events occur in your organization. ## Getting Started Webhooks allow your application to receive instant notifications when events happen, such as companies being created, documents being uploaded, or AI processing completing. Start by creating a webhook to receive real-time notifications ## How Webhooks Work 1. **Register your endpoint**: Create a webhook with your URL and specify which events you want to receive 2. **Receive notifications**: Well sends HTTP POST requests to your endpoint when events occur 3. **Process events**: Your application processes the webhook payload and takes appropriate action 4. **Respond with 200**: Return a 200 status code to acknowledge receipt ## Supported Events The Well Core API supports webhooks for various resource events: * **Company events**: `company.created`, `company.updated` * **Document events**: `document.uploaded`, `document.processed` * **People events**: `people.created`, `people.updated` * **Invoice events**: `invoice.created`, `invoice.updated` ## Quick Start Examples ```javascript theme={null} // Create a webhook const webhookResponse = await fetch('https://api.well.com/v1/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ data: { type: 'webhook', attributes: { url: 'https://your-app.com/webhook', events: ['company.created', 'document.uploaded'], description: 'Main webhook for production' } } }) }); // Handle webhook in your app (Express.js example) app.post('/webhook', (req, res) => { const { event, data } = req.body; switch (event) { case 'company.created': console.log('New company created:', data.company_id); break; case 'document.uploaded': console.log('Document uploaded:', data.document_id); break; } res.status(200).send('OK'); }); ``` ```python theme={null} import requests from flask import Flask, request # Create a webhook webhook_response = requests.post('https://api.well.com/v1/webhooks', headers={'Authorization': f'Bearer {api_key}'}, json={ 'data': { 'type': 'webhook', 'attributes': { 'url': 'https://your-app.com/webhook', 'events': ['company.created', 'document.uploaded'], 'description': 'Main webhook for production' } } } ) # Handle webhook in your app (Flask example) app = Flask(__name__) @app.route('/webhook', methods=['POST']) def handle_webhook(): data = request.json event = data.get('event') payload = data.get('data') if event == 'company.created': print(f"New company created: {payload['company_id']}") elif event == 'document.uploaded': print(f"Document uploaded: {payload['document_id']}") return 'OK', 200 ``` ```bash theme={null} # Create a webhook curl -X POST https://api.well.com/v1/webhooks \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "type": "webhook", "attributes": { "url": "https://your-app.com/webhook", "events": ["company.created", "document.uploaded"], "description": "Main webhook for production" } } }' ``` ## Webhook Management Set up a new webhook endpoint View all your configured webhooks Modify webhook settings and events Remove webhooks you no longer need ## Example Webhook Payloads ### Company Created Event ```json theme={null} { "event": "company.created", "timestamp": "2024-01-15T10:30:00Z", "data": { "company_id": "comp_12345", "name": "Acme Corp", "description": "Leading technology company", "external_workspace_id": "external workspace id (UUID)", "created_at": "2024-01-15T10:30:00Z" } } ``` ### Document Uploaded Event ```json theme={null} { "event": "document.uploaded", "timestamp": "2024-01-15T10:35:00Z", "data": { "document_id": "doc_98765", "name": "Important Document.pdf", "size": 1048576, "organisation_id": "org_11111", "uploaded_at": "2024-01-15T10:35:00Z" } } ``` ## Security & Validation * **Timeout**: Webhook requests timeout after 10 seconds * **Retries**: Failed webhooks are retried up to 3 times with exponential backoff * **Headers**: Each webhook includes a `X-Well-Signature` header for verification ## Best Practices 1. **Use HTTP status codes** to indicate success or failure 2. **Use JSON:API standard** for request and response formats # Connectors Source: https://docs.wellapp.ai/connectors > Complete list of all available connectors } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> # Banking Transaction Fee Source: https://docs.wellapp.ai/enums/banking-transaction-fee Essential banking transaction fee type indicators for categorizing different charges and fees ## Fee Types
Type Description Typical Range When Applied
transaction Standard transaction processing fee 0.1% - 3% or fixed Per transaction, at execution
transfer Wire transfer or inter-bank transfer fee Fixed (e.g., \$10-50) Domestic/international transfers
currency\_conversion Foreign exchange conversion fee 1% - 4% Cross-currency transactions
atm ATM withdrawal or usage fee Fixed (e.g., \$2-5) ATM withdrawals, balance inquiries
overdraft Overdraft or insufficient funds fee Fixed (e.g., \$25-35) When account goes negative
monthly Monthly account maintenance fee Fixed (e.g., \$5-15) Monthly recurring charge
card Card issuance, renewal, or annual fee Fixed (e.g., \$0-100/year) Card-related services
commission Commission or percentage-based fee 0.5% - 5% Brokerage, investment transactions
penalty Late payment or violation penalty Fixed (e.g., \$15-40) Late payments, rule violations
other Miscellaneous or unclassified fees Varies Special services, custom charges
# Business Entity Types by Country Source: https://docs.wellapp.ai/enums/business-entity-by-country Comprehensive list of business entity types organized by country for international business classification ## Country-Specific Business Entities ### 🇺🇸 United States
Entity Code Full Name
Inc Incorporated
Corp Corporation
LLC Limited Liability Company
LLP Limited Liability Partnership
LP Limited Partnership
LLLP Limited Liability Limited Partnership
PC Professional Corporation
PLLC Professional Limited Liability Company
PLC Public Limited Company
Co Company
Ltd Limited
DBA Doing Business As
Partnership General Partnership
Sole\_Proprietorship Individual Business
C-Corp C Corporation
S-Corp S Corporation
B-Corp Benefit Corporation
PBC Public Benefit Corporation
### 🇬🇧 United Kingdom
Entity Code Full Name
Ltd Private Limited Company
PLC Public Limited Company
LLP Limited Liability Partnership
LP Limited Partnership
CIC Community Interest Company
CIO Charitable Incorporated Organisation
IPS Industrial and Provident Society
SE European Company (Societas Europaea)
EEIG European Economic Interest Grouping
Partnership General Partnership
Sole\_Trader Individual Business
Unlimited Unlimited Company
REIT Real Estate Investment Trust
### 🇩🇪 Germany
Entity Code Full Name
GmbH Gesellschaft mit beschränkter Haftung
AG Aktiengesellschaft
UG Unternehmergesellschaft (haftungsbeschränkt)
KG Kommanditgesellschaft
OHG Offene Handelsgesellschaft
GbR Gesellschaft bürgerlichen Rechts
PartG Partnerschaftsgesellschaft
SE Societas Europaea
eG Eingetragene Genossenschaft
KGaA Kommanditgesellschaft auf Aktien
GmbH\_Co\_KG GmbH & Co. KG
Einzelunternehmen Sole Proprietorship
PartGmbB Partnerschaftsgesellschaft mit beschränkter Berufshaftung
### 🇫🇷 France
Entity Code Full Name
SARL Société à responsabilité limitée
SA Société anonyme
SAS Société par actions simplifiée
SASU Société par actions simplifiée unipersonnelle
EURL Entreprise unipersonnelle à responsabilité limitée
SNC Société en nom collectif
SCS Société en commandite simple
SCA Société en commandite par actions
SEP Société en participation
GIE Groupement d'intérêt économique
GEIE Groupement européen d'intérêt économique
SE Societas Europaea
SCOP Société coopérative de production
Auto\_Entrepreneur Auto-entrepreneur
EI Entreprise individuelle
EIRL Entreprise individuelle à responsabilité limitée
### 🇮🇹 Italy
Entity Code Full Name
SpA Società per azioni
SRL Società a responsabilità limitata
SNC Società in nome collettivo
SAS Società in accomandita semplice
SAPA Società in accomandita per azioni
SC Società cooperativa
SE Societas Europaea
GEIE Gruppo europeo di interesse economico
Ditta\_Individuale Individual Business
STP Società tra professionisti
Startup\_Innovativa Innovative Startup
### 🇪🇸 Spain
Entity Code Full Name
SA Sociedad Anónima
SL Sociedad de Responsabilidad Limitada
SC Sociedad Colectiva
Scom Sociedad Comanditaria
SCom\_A Sociedad Comanditaria por Acciones
SCOOP Sociedad Cooperativa
SE Sociedad Europea
AIE Agrupación de Interés Económico
UTE Unión Temporal de Empresas
Empresario\_Individual Individual Entrepreneur
CB Comunidad de Bienes
SAT Sociedad Agraria de Transformación
### 🇳🇱 Netherlands
Entity Code Full Name
BV Besloten vennootschap
NV Naamloze vennootschap
VOF Vennootschap onder firma
CV Commanditaire vennootschap
Maatschap Partnership
VvE Vereniging van Eigenaars
Stichting Foundation
Vereniging Association
Coöperatie Cooperative
SE Societas Europaea
EESV Europees Economisch Samenwerkingsverband
Eenmanszaak Sole Proprietorship
### 🇧🇪 Belgium
Entity Code Full Name
SA Société Anonyme / Naamloze Vennootschap
SPRL Société Privée à Responsabilité Limitée
SRL Société à Responsabilité Limitée
BV Besloten Vennootschap
SNC Société en Nom Collectif
SCS Société en Commandite Simple
SCA Société en Commandite par Actions
SCRL Société Coopérative à Responsabilité Limitée
SE Societas Europaea
GEIE Groupement Européen d'Intérêt Économique
Entreprise\_Individuelle Individual Business
ASBL Association sans but lucratif
### 🇨🇭 Switzerland
Entity Code Full Name
AG Aktiengesellschaft
GmbH Gesellschaft mit beschränkter Haftung
SA Société Anonyme
Sàrl Société à responsabilité limitée
Kollektivgesellschaft General Partnership
Kommanditgesellschaft Limited Partnership
Einzelfirma Sole Proprietorship
Genossenschaft Cooperative
Verein Association
Stiftung Foundation
Kommanditaktiengesellschaft Partnership Limited by Shares
### 🇦🇹 Austria
Entity Code Full Name
AG Aktiengesellschaft
GmbH Gesellschaft mit beschränkter Haftung
OG Offene Gesellschaft
KG Kommanditgesellschaft
Gen Genossenschaft
SE Societas Europaea
Einzelunternehmen Sole Proprietorship
Stille\_Gesellschaft Silent Partnership
EWIV Europäische wirtschaftliche Interessenvereinigung
### 🇨🇦 Canada
Entity Code Full Name
Inc Incorporation
Corp Corporation
Ltd Limited
Ltée Limitée
LP Limited Partnership
LLP Limited Liability Partnership
GP General Partnership
Co Company
ULC Unlimited Liability Corporation
Sole\_Proprietorship Individual Business
Cooperative Cooperative
NPO Non-Profit Organization
### 🇦🇺 Australia
Entity Code Full Name
Pty\_Ltd Proprietary Limited
Ltd Public Limited Company
LP Limited Partnership
Trust Trust
Sole\_Trader Individual Business
Partnership General Partnership
Co-operative Cooperative
Incorporated\_Association Incorporated Association
CCIV Corporate Collective Investment Vehicle
AMIT Attribution Managed Investment Trust
### 🇯🇵 Japan
Entity Code Full Name
KK Kabushiki Kaisha (株式会社)
GK Godo Kaisha (合同会社)
YK Yugen Kaisha (有限会社)
Gomei Gomei Kaisha (合名会社)
Goshi Goshi Kaisha (合資会社)
LLP Limited Liability Partnership
TMK Tokutei Mokuteki Kaisha
Cooperative Cooperative
Sole\_Proprietorship Individual Business
### 🇮🇳 India
Entity Code Full Name
Pvt\_Ltd Private Limited Company
Ltd Public Limited Company
LLP Limited Liability Partnership
Partnership Partnership Firm
Sole\_Proprietorship Proprietorship
HUF Hindu Undivided Family
Section\_8 Section 8 Company (Non-profit)
Producer\_Company Producer Company
OPC One Person Company
Cooperative Cooperative Society
Trust Trust
Society Society
### 🇨🇳 China
Entity Code Full Name
Ltd Limited Liability Company (有限责任公司)
JSC Joint Stock Company (股份有限公司)
WFOE Wholly Foreign-Owned Enterprise
JV Joint Venture
Rep\_Office Representative Office
Partnership Partnership Enterprise
Sole\_Proprietorship Individual Business
FIE Foreign-Invested Enterprise
Cooperative Farmers' Professional Cooperative
### 🇧🇷 Brazil
Entity Code Full Name
Ltda Sociedade Limitada
SA Sociedade Anônima
Eireli Empresa Individual de Responsabilidade Limitada
MEI Microempreendedor Individual
LTDA\_ME Microempresa Limitada
EPP Empresa de Pequeno Porte
Cooperativa Cooperative
SCP Sociedade em Conta de Participação
Empresario\_Individual Individual Entrepreneur
### 🇲🇽 Mexico
Entity Code Full Name
SA Sociedad Anónima
SRL Sociedad de Responsabilidad Limitada
SC Sociedad Colectiva
SCS Sociedad en Comandita Simple
SCA Sociedad en Comandita por Acciones
SAPI Sociedad Anónima Promotora de Inversión
SAS Sociedad por Acciones Simplificada
Persona\_Fisica Individual (Physical Person)
SOFOM Sociedad Financiera de Objeto Múltiple
### 🇸🇬 Singapore
Entity Code Full Name
Pte\_Ltd Private Limited Company
Ltd Public Limited Company
LP Limited Partnership
LLP Limited Liability Partnership
Sole\_Proprietorship Individual Business
Partnership General Partnership
Branch Branch Office
Rep\_Office Representative Office
Trust Trust
Society Society
Cooperative Cooperative
### 🇭🇰 Hong Kong
Entity Code Full Name
Ltd Limited Company
PLC Public Limited Company
Partnership Partnership
Sole\_Proprietorship Sole Proprietorship
Branch Branch Office
Rep\_Office Representative Office
Trust Trust
Cooperative Cooperative
### 🇰🇷 South Korea
Entity Code Full Name
Co\_Ltd Jusik Hoesa (주식회사)
LLC Yuhan Hoesa (유한회사)
Partnership Hapmyeong Hoesa (합명회사)
LP Hapja Hoesa (합자회사)
Branch Branch Office
Rep\_Office Representative Office
LLP Limited Liability Partnership
Sole\_Proprietorship Individual Business
### 🇷🇺 Russia
Entity Code Full Name
OAO Open Joint Stock Company (открытое акционерное общество)
ZAO Closed Joint Stock Company (закрытое акционерное общество)
OOO Limited Liability Company (общество с ограниченной ответственностью)
PAO Public Joint Stock Company (публичное акционерное общество)
AO Joint Stock Company (акционерное общество)
IP Individual Entrepreneur (индивидуальный предприниматель)
GP General Partnership (полное товарищество)
LP Limited Partnership (товарищество на вере)
Production\_Cooperative Production Cooperative
### 🇿🇦 South Africa
Entity Code Full Name
Pty\_Ltd Proprietary Limited
Ltd Public Company
CC Close Corporation
Inc Incorporated
Partnership Partnership
Sole\_Proprietorship Sole Proprietorship
Trust Trust
Cooperative Cooperative
NPO Non-Profit Organization
NPC Non-Profit Company
### 🇦🇪 UAE
Entity Code Full Name
LLC Limited Liability Company
PJSC Public Joint Stock Company
PJSCC Private Joint Stock Company
Branch Branch Office
Rep\_Office Representative Office
Sole\_Proprietorship Sole Establishment
Civil\_Company Civil Company
Partnership General Partnership
LP Limited Partnership
Free\_Zone Free Zone Entity
### 🇸🇦 Saudi Arabia
Entity Code Full Name
LLC Limited Liability Company (شركة ذات مسئولية محدودة)
JSC Joint Stock Company (شركة مساهمة)
GP General Partnership (شركة تضامن)
LP Limited Partnership (شركة توصية بسيطة)
LPS Limited Partnership by Shares (شركة توصية بالأسهم)
Sole\_Proprietorship Individual Establishment (مؤسسة فردية)
Branch Branch Office
Professional\_Company Professional Company
## Generic/Fallback Values
Entity Code Description
Other Catch-all for unrecognized types
Unknown When type cannot be determined
Partnership Generic partnership
Corporation Generic corporation
Company Generic company
Enterprise Generic enterprise
Firm Generic firm
Business Generic business entity
# Category Connector Source: https://docs.wellapp.ai/enums/category-connector Classification system for integration connectors, organizing services and platforms by business function and technical domain ## Data & Storage
Value Description
database Relational and NoSQL databases
data\_warehouse Data warehousing solutions
file File storage and management systems
storage Dedicated storage services
cloud General cloud infrastructure and services
## Communication & Messaging
Value Description
messaging Internal messaging and chat platforms
email Email services and providers
notification Push notifications, SMS, and alert services
social Social media platforms
communication Video/voice calling and collaboration tools
## Business Applications
Value Description
crm Customer relationship management
erp Enterprise resource planning
hr Human resources systems
finance Financial management and planning tools
accounting Accounting software
ecommerce E-commerce platforms
marketing Marketing automation and campaign management
project\_management Project and task management tools
productivity Office suites and productivity tools
## Analytics & Intelligence
Value Description
analytics Web analytics and business intelligence
reporting Reporting and dashboard tools
ai\_ml AI/ML services and platforms
## Technical & Infrastructure
Value Description
api General APIs and web services
authentication Identity providers, SSO, OAuth
monitoring System monitoring, logging, and APM
security Security tools and vulnerability scanners
ci\_cd Continuous integration/deployment
infrastructure Cloud infrastructure and DevOps tools
## Financial Services
Value Description
payment Payment gateways and processors
banking Banking APIs and financial institutions
## Industry-Specific
Value Description
healthcare Healthcare systems, EMR, medical records
logistics Shipping, supply chain, and inventory management
iot Internet of Things devices and platforms
government Government systems and APIs
legal Legal management systems and compliance tools
education Educational platforms and learning management systems
real\_estate Real estate platforms and property management
travel Travel and hospitality services
media Media and content management platforms
gaming Gaming platforms and services
## Catch-All
Value Description
other Any connector that doesn't fit into the above categories
# Company Roles Source: https://docs.wellapp.ai/enums/company-roles Relationship types that define how people are connected to companies ## Available Company Roles

contact

General business contact

employee

Company employee

owner

Business owner or principal

other

Other relationship types

## Company Roles Comparison Table
Role Description Internal/External Decision Making Access Level Common Use Cases
owner Business owner or principal 🏢 Internal ✅ High authority 🔓 Full access Founders, partners, directors
employee Company employee 🏢 Internal ⚠️ Role-dependent 🔐 Limited access Staff, contractors, interns
contact General business contact 🌐 External ❌ No authority 👁️ View only Clients, vendors, networking
other Other relationship types 🔄 Mixed ⚠️ Context-dependent 🔐 Variable Advisors, consultants, board
## Usage in API When creating or updating company-person relationships, use these exact role values: ```json theme={null} { "relationship_type": "employee" } ``` **Valid values:** `contact`, `employee`, `owner`, `other` ## Examples ### Creating a Company-Employee Relationship ```json theme={null} { "data": { "type": "people", "id": "person-123", "meta": { "relationship_type": "employee" } } } ``` ### Creating a Company-Contact Relationship ```json theme={null} { "data": { "type": "people", "id": "person-456", "meta": { "relationship_type": "contact" } } } ``` # Connector Data Quality Source: https://docs.wellapp.ai/enums/connector-data-quality List of resources and attributes imported through integration connectors ## 📊 Imported Resources
Value Description
invoice Invoice documents and billing information
company Company and organization records
people Individual person records and contact information
# Currency Rate Source Source: https://docs.wellapp.ai/enums/currency-rate Essential currency rate source indicators for tracking foreign exchange rate data providers
Source Description Coverage Reliability Use Cases
ecb European Central Bank Global currencies vs EUR Official, daily EU-based transactions, EUR conversions
fed Federal Reserve System (US) Major currencies vs USD Official, daily US-based transactions, USD conversions
imf International Monetary Fund 190+ currencies, SDR rates Official, periodic International reporting, SDR calculations
xe XE.com Exchange Rates 180+ currencies, real-time Commercial, frequent Live rates, consumer transactions
oanda OANDA Exchange Rates 190+ currencies, real-time Commercial, reliable Trading, business conversions
bank Commercial Bank Rates Limited, bank-specific Varies by bank Banking transactions, customer rates
manual Manually Entered Rates Custom, as needed User-dependent Custom scenarios, historical data
other Other Exchange Rate Sources Varies Varies Alternative providers, legacy systems
# Banking Transaction Status Source: https://docs.wellapp.ai/enums/details-banking-transaction-status Essential banking transaction status indicators for tracking the complete lifecycle of banking transactions ## Transaction Statuses
Status Description Lifecycle Stage Next Actions
pending Transaction initiated, awaiting processing Initial Wait for processing, system will auto-advance
processing Transaction being processed by bank/system In-progress Wait for completion, monitor progress
authorized Transaction authorized but not yet settled Authorization Wait for settlement, funds reserved
completed Transaction successfully completed and settled Success Archive, reconcile, update records
failed Transaction failed due to technical error Error Investigate, retry, contact support
rejected Transaction rejected by bank or system Rejection Review rejection reason, correct and resubmit
cancelled Transaction cancelled by user or system Cancellation Update records, notify parties
reversed Transaction reversed or rolled back Reversal Investigate reason, update accounting
on\_hold Transaction temporarily held for review Hold Wait for review, provide additional info
expired Transaction expired without completion Timeout Reinitiate if needed, update records
# UN/EDIFACT Document Type Codes Source: https://docs.wellapp.ai/enums/edifact-document-codes ISO standard document type codes for electronic data interchange (EDI) transactions, supporting global business document classification **Standard Reference**: UN/EDIFACT Code List 1001 - Document Type Code * **Maintained by**: United Nations Economic Commission for Europe (UNECE) * **Used in**: Electronic Data Interchange (EDI) transactions * **Standard**: ISO 9735 (EDIFACT Syntax Rules) * **Format**: 3-digit numeric codes * **Your Current Usage**: `"380"` for Commercial Invoice ## Primary Invoice Document Types ### Core Invoice Types (Priority Implementation)
Code Document Type Description
380 Commercial Invoice Standard business invoice
381 Credit Note Refund or adjustment document
383 Debit Note Additional charges document
384 Corrected Invoice Amended or revised invoice
325 Proforma Invoice Preliminary invoice
326 Partial Invoice Partial payment invoice
385 Consolidated Invoice Combined multiple invoices
386 Prepayment Invoice Advance payment request
### Specialized Invoice Types
Code Document Type Description
387 Hire Invoice Equipment/service rental
388 Tax Invoice Tax-compliant invoice
389 Self-Billing Invoice Buyer-generated invoice
390 Delcredere Invoice Credit guarantee invoice
391 Factored Invoice Third-party financed
392 Lease Invoice Leasing agreement
393 Consignment Invoice Goods on consignment
394 Factored Credit Note Financed credit note
395 Consignment Credit Note Consignment refund
396 Factored Debit Note Financed debit note
397 Consignment Debit Note Consignment charge
## Purchase and Order Documents ### Purchase Orders
Code Document Type Description
220 Order General order document
221 Blanket Order Framework agreement
222 Spot Order One-time purchase
230 Purchase Order Formal purchase request
231 Blanket Purchase Order Recurring purchase framework
232 Spot Purchase Order Single transaction order
235 Repair Purchase Order Maintenance services
236 Call Off Purchase Order Framework order release
### Quotations and Proposals
Code Document Type Description
310 Request for Quote Quote solicitation
311 Request for Proposal Proposal solicitation
312 Request for Price Quote Price inquiry
315 Contract Award Notice Winner notification
320 Certified Invoice Verified invoice
322 Freight Invoice Shipping charges
327 Price Variation Invoice Price adjustment
328 Tax Point Invoice Tax date specific
329 Sole Agent Invoice Exclusive agent
## Payment and Financial Documents ### Payment Documents
Code Document Type Description
440 Payment Order Payment instruction
441 Wage Payment Order Salary payment
446 Tax Payment Order Tax remittance
447 Customs Payment Order Customs duties
450 Payment Advice Payment notification
451 Credit Advice Credit notification
452 Debit Advice Debit notification
456 Remittance Advice Payment details
460 Financial Statement of Account Account statement
## Shipping and Logistics Documents ### Shipping Documents
Code Document Type Description
270 Packing List Package contents
271 Certified Packing List Verified contents
550 Despatch Advice Shipment notification
551 Goods Receipt Delivery confirmation
552 Ultimate Goods Receipt Final delivery
580 Freight Invoice Transportation charges
### Transport Documents
Code Document Type Description
622 Road Consignment Note Road transport
623 House Bill of Lading Freight forwarder B/L
705 Bill of Lading Ocean transport
740 Air Waybill Air transport
741 Master Air Waybill Airline document
743 House Air Waybill Freight forwarder AWB
## Government and Compliance Documents ### Customs and Tax
Code Document Type Description
610 Customs Declaration (SAD) Import/export declaration
611 Goods Declaration for Importation Import customs
612 Goods Declaration for Exportation Export customs
615 Customs Invoice Customs valuation
617 Tax Certificate Tax compliance
618 Tax Assessment Tax calculation
619 Tax Demand Tax payment notice
### Regulatory Documents
Code Document Type Description
700 Certificate of Origin Product origin
701 UNESCO Coupon International voucher
702 Forwarder's Certificate of Receipt Freight receipt
770 Insurance Policy Insurance contract
775 Insurance Certificate Insurance proof
## Reports and Statements ### Business Reports
Code Document Type Description
805 Inventory Report Stock status
810 Stock Report Inventory movement
815 Financial Statement Financial position
820 Balance Sheet Financial balance
825 Trial Balance Accounting trial
830 Profit and Loss Statement P\&L report
835 Tax Return Tax filing
840 Payroll Employee payments
845 Timesheet Work hours
850 Expense Report Business expenses
# File Types Source: https://docs.wellapp.ai/enums/file Essential filew type classifications for organizing different types of digital files and media content ## File Types (Max 10)
Type Description MIME Type Common Extensions Use Cases
pdf Portable Document Format application/pdf .pdf Invoices, contracts, reports, official documents
image Image files (photos, scans, graphics) image/jpeg, image/png, image/gif .jpg, .jpeg, .png, .gif, .webp Scanned documents, receipts, photos, logos
document Text documents and word processing application/msword, text/plain .doc, .docx, .txt, .rtf Letters, proposals, notes, manuscripts
spreadsheet Spreadsheet and data files application/vnd.ms-excel .xls, .xlsx, .csv Financial data, budgets, lists, calculations
presentation Presentation and slide files application/vnd.ms-powerpoint .ppt, .pptx, .key Business presentations, proposals, decks
archive Compressed and archive files application/zip, application/x-rar .zip, .rar, .7z, .tar.gz File bundles, backups, software packages
video Video and multimedia files video/mp4, video/avi .mp4, .avi, .mov, .wmv, .mkv Training videos, presentations, recordings
audio Audio files and recordings audio/mpeg, audio/wav .mp3, .wav, .aac, .ogg Voice notes, music, podcasts, recordings
web Web and markup files text/html, application/json .html, .htm, .xml, .json Web pages, configuration, data exchange
other Miscellaneous and unknown file types application/octet-stream Various Proprietary formats, unknown files
# Invoice Line Unit Source: https://docs.wellapp.ai/enums/invoice-line-unit UN/CEFACT standard units of measure for invoice line items ## Overview The `unit` field specifies the unit of measure for invoice line items, based on **UN/CEFACT Recommendation 20** - the international standard for units of measure in business transactions. **Current Implementation**: `"EA, HOUR, KG, etc."` **Field Type**: `string` **Required**: No (optional) **Standard**: UN/CEFACT Recommendation 20 (ISO 80000) **Format**: 1-3 character codes *** ## Complete Unit of Measure Enum ### 📦 Quantity & Count Units
Code Full Name Description Common Usage Examples
EA Each Individual items Products, licenses "5 EA software licenses"
PC Piece Individual pieces Parts, components "10 PC circuit boards"
SET Set Collection of items Tool sets, kits "2 SET office furniture"
PR Pair Two matching items Shoes, gloves "3 PR safety gloves"
DZ Dozen 12 pieces Bulk items "4 DZ pencils"
C62 Hundred 100 pieces Large quantities "2 C62 business cards"
MIL Thousand 1000 pieces Volume sales "5 MIL paper sheets"
KT Kit Assembly kit Products with parts "1 KT starter package"
PK Pack Packaged quantity Consumer goods "10 PK batteries"
BX Box Boxed items Shipping units "3 BX printer paper"
### ⏱️ Time-Based Units
Code Full Name Description Common Usage Examples
HUR Hour 60 minutes Services, consulting "40 HUR development work"
MIN Minute Time unit Call charges, usage "120 MIN phone calls"
SEC Second Time unit Computing, processing "3600 SEC server time"
DAY Day 24 hours Rentals, subscriptions "30 DAY hosting service"
WEE Week 7 days Project durations "4 WEE consulting project"
MON Month Calendar month Subscriptions, leases "12 MON software license"
ANN Annual Yearly Annual contracts "1 ANN support contract"
QT Quarter 3 months Quarterly services "4 QT maintenance plan"
### ⚖️ Weight & Mass Units
Code Full Name Description Common Usage Examples
KGM Kilogram 1000 grams Products, shipping "25 KGM raw materials"
GRM Gram Basic weight unit Small items "500 GRM precious metals"
TNE Metric Ton 1000 kilograms Bulk materials "2 TNE steel beams"
LBR Pound Imperial weight US market "100 LBR equipment"
ONZ Ounce Imperial weight Precious materials "10 ONZ gold"
CWT Hundredweight 100 pounds Agriculture, mining "5 CWT grain"
STN Short Ton 2000 pounds (US) US bulk materials "3 STN coal"
LTN Long Ton 2240 pounds (UK) UK shipping "1 LTN cargo"
### 📏 Length & Distance Units
Code Full Name Description Common Usage Examples
MTR Meter Base length unit Materials, cables "100 MTR ethernet cable"
CMT Centimeter 0.01 meter Small measurements "50 CMT fabric trim"
MMT Millimeter 0.001 meter Precision items "25 MMT steel wire"
KMT Kilometer 1000 meters Transportation "500 KMT delivery service"
INH Inch Imperial length US market "12 INH steel pipe"
FOT Foot 12 inches Construction "100 FOT lumber"
YRD Yard 3 feet Textiles, landscaping "50 YRD fabric"
SMI Statute Mile 5280 feet Transportation "1000 SMI shipping"
### 📐 Area Units
Code Full Name Description Common Usage Examples
MTK Square Meter Area unit Real estate, flooring "200 MTK office space"
CMK Square Centimeter Area unit Small surfaces "500 CMK labels"
INK Square Inch Imperial area US manufacturing "144 INK material"
FTK Square Foot Imperial area Real estate (US) "2000 FTK warehouse"
YDK Square Yard Imperial area Carpeting, landscaping "100 YDK carpet"
ACR Acre Land measurement Agriculture, real estate "5 ACR farmland"
HAR Hectare 10,000 sq meters Land (metric) "2 HAR industrial plot"
### 🫗 Volume & Capacity Units
Code Full Name Description Common Usage Examples
LTR Liter Volume unit Liquids, chemicals "500 LTR fuel"
MLT Milliliter 0.001 liter Small volumes "250 MLT reagent"
MTQ Cubic Meter Volume unit Gas, storage "100 MTQ storage"
CMQ Cubic Centimeter Small volume Precision liquids "10 CMQ samples"
INQ Cubic Inch Imperial volume US manufacturing "500 INQ capacity"
FTQ Cubic Foot Imperial volume US shipping "50 FTQ cargo"
YDQ Cubic Yard Imperial volume Construction materials "10 YDQ concrete"
GAL Gallon Liquid measure (US) Fuel, chemicals "100 GAL gasoline"
QT Quart 0.25 gallon Smaller liquids "8 QT motor oil"
PT Pint 0.5 quart Consumer liquids "16 PT beverages"
### 💻 Digital & Technology Units
Code Full Name Description Common Usage Examples
BYT Byte Data unit Storage, bandwidth "1024 BYT data"
KBY Kilobyte 1024 bytes Small files "500 KBY documents"
MBY Megabyte 1024 KB Files, media "100 MBY video files"
GBY Gigabyte 1024 MB Storage, transfer "50 GBY cloud storage"
TBY Terabyte 1024 GB Large storage "5 TBY backup space"
BIT Bit Basic data unit Bandwidth, processing "1000000 BIT transfer"
KBI Kilobit 1000 bits Network speeds "100 KBI connection"
MBI Megabit 1000 kilobits Internet speeds "50 MBI bandwidth"
GBI Gigabit 1000 megabits High-speed networks "10 GBI fiber link"
### 🔌 Energy & Power Units
Code Full Name Description Common Usage Examples
KWH Kilowatt Hour Energy unit Electricity billing "1500 KWH power usage"
MWH Megawatt Hour 1000 KWH Industrial power "5 MWH facility power"
WHR Watt Hour Energy unit Small devices "500 WHR battery"
KWT Kilowatt Power unit Equipment rating "10 KWT generator"
MWT Megawatt 1000 kilowatts Large systems "2 MWT solar farm"
BTU British Thermal Unit Heat energy HVAC, heating "50000 BTU heater"
CAL Calorie Energy unit Food, chemistry "2000 CAL nutrition"
### 🌡️ Temperature & Scientific Units
Code Full Name Description Common Usage Examples
CEL Celsius Temperature Scientific, global "25 CEL operating temp"
FAH Fahrenheit Temperature US market "77 FAH room temp"
KEL Kelvin Absolute temperature Scientific "298 KEL reaction temp"
BAR Bar Pressure unit Industrial pressure "10 BAR system pressure"
PSI Pounds per Sq Inch Pressure (Imperial) US industrial "150 PSI tire pressure"
PAL Pascal Pressure unit Scientific pressure "101325 PAL atmospheric"
### 🎯 Service & Usage Units
Code Full Name Description Common Usage Examples
SRV Service Service unit Professional services "1 SRV consultation"
LIC License License unit Software licensing "50 LIC user accounts"
USR User Per user SaaS, subscriptions "100 USR monthly plan"
SES Session Usage session Online services "1000 SES API calls"
TXN Transaction Per transaction Payment processing "500 TXN payment fees"
REQ Request API/service request Cloud services "10000 REQ API usage"
PAG Page Per page Printing, scanning "500 PAG document scan"
VIS Visit Website visit Analytics, marketing "10000 VIS ad campaign"
CLI Click Click-through Digital advertising "5000 CLI ad clicks"
IMP Impression Ad impression Digital marketing "100000 IMP display ads"
### 💰 Financial & Currency Units
Code Full Name Description Common Usage Examples
PTC Percent Percentage Commission, rates "5 PTC commission rate"
BPS Basis Points 0.01 percent Financial rates "250 BPS interest"
SHR Share Stock share Equity transactions "1000 SHR stock option"
LOT Lot Trading lot Securities trading "10 LOT futures contract"
PNT Point Price point Trading, scoring "100 PNT bonus points"
### 🏭 Industrial & Manufacturing Units
Code Full Name Description Common Usage Examples
TOL Tool Manufacturing tool Equipment, tooling "5 TOL cutting dies"
MOL Mole Chemical quantity Chemistry, pharma "2 MOL reagent"
PPM Parts Per Million Concentration Quality control "50 PPM impurities"
PPB Parts Per Billion Concentration Precision analysis "10 PPB contamination"
PH pH Unit Acidity measure Chemistry, water "7 PH neutral solution"
### ❓ Fallback & Generic Units
Code Full Name Description Common Usage Examples
UNT Unit Generic unit Unspecified items "10 UNT miscellaneous"
OTH Other Other unit Special cases "5 OTH custom items"
NA Not Applicable No unit needed Services, fees "1 NA setup fee"
## Implementation Guidelines ### Priority Tiers for Implementation ### **🔴 Tier 1 - Critical (Implement First)** * **`EA`** - Each (most common) * **`HUR`** - Hour (services) * **`KGM`** - Kilogram (weight) * **`LTR`** - Liter (volume) * **`MTR`** - Meter (length) * **`PC`** - Piece * **`SET`** - Set * **`DAY`** - Day * **`MON`** - Month ### **🟡 Tier 2 - Important (Second Priority)** * **`MIN`** - Minute * **`GRM`** - Gram * **`CMT`** - Centimeter * **`MLT`** - Milliliter * **`DZ`** - Dozen * **`BX`** - Box * **`PK`** - Pack * **`KT`** - Kit * **`SRV`** - Service * **`LIC`** - License ### **🟢 Tier 3 - Extended Coverage** * Digital units (BYT, KBY, MBY, GBY) * Imperial units (INH, FOT, YRD, GAL) * Scientific units (CEL, BAR, MOL) * Financial units (PTC, BPS, SHR) * Specialized industry units (See above for file contents.) # Invoice Status Source: https://docs.wellapp.ai/enums/invoice-status Lifecycle states of invoice documents from creation to completion ## 📋 Invoice Lifecycle States
Value Description
queued Invoice is queued for processing
sent Invoice has been sent to the recipient
signed Invoice has been digitally signed
rejected Invoice has been rejected by the recipient
accepted Invoice has been accepted by the recipient
outstanding Invoice is pending payment
# Labels Source: https://docs.wellapp.ai/enums/label Essential email labels for categorizing and organizing emails ## Labels (Max 10)
Label Description Use Case
work Work and professional emails Business correspondence, meetings, projects, colleagues
personal Personal and private emails Family, friends, personal matters
urgent High priority emails requiring immediate attention Critical communications, deadlines, emergencies
marketing Promotional and marketing emails Newsletters, offers, campaigns, advertisements
notification System and service notifications Confirmations, alerts, updates, reminders
support Customer support and service emails Help desk, technical support, service requests
billing Financial and billing related emails Invoices, receipts, payments, banking
social Social and event-related emails Social events, invitations, community updates
automated Automated system-generated emails No-reply emails, system messages, backups
other Miscellaneous emails not fitting above categories Catch-all for unclassified emails
# Media Types Source: https://docs.wellapp.ai/enums/media Essential media type classifications for organizing different types of digital files and media content ## Media Types
Type Description Common Use Cases File Extensions
avatar User profile pictures and personal images Profile photos, user avatars, headshots .jpg, .png, .gif, .webp
logo Company and brand logos Brand identities, company logos, icons .png, .svg, .jpg, .webp
banner Header and banner images Website headers, promotional banners, covers .jpg, .png, .webp, .svg
document Text-based documents and files PDFs, reports, contracts, presentations .pdf, .doc, .docx, .ppt, .txt
image General images and photographs Photos, illustrations, graphics, artwork .jpg, .png, .gif, .webp, .svg
video Video files and recordings Promotional videos, tutorials, recordings .mp4, .avi, .mov, .webm, .mkv
audio Audio files and recordings Music, podcasts, voice recordings .mp3, .wav, .aac, .ogg, .flac
icon Small icons and symbols UI icons, favicons, small graphics .png, .svg, .ico, .webp
thumbnail Preview and thumbnail images Video thumbnails, image previews, gallery .jpg, .png, .webp
attachment General file attachments Spreadsheets, archives, misc files .xlsx, .zip, .csv, .json, .xml
## Usage Examples ### Example 1: User Avatar ```json theme={null} { "file_name": "profile_photo.jpg", "media_type": "avatar", "file_size": 245760, "dimensions": "512x512", "mime_type": "image/jpeg" } ``` ### Example 2: Company Logo ```json theme={null} { "file_name": "company_logo.svg", "media_type": "logo", "file_size": 15840, "mime_type": "image/svg+xml" } ``` ### Example 3: Marketing Banner ```json theme={null} { "file_name": "campaign_banner.png", "media_type": "banner", "file_size": 892160, "dimensions": "1920x1080", "mime_type": "image/png" } ``` # Payment Means Source: https://docs.wellapp.ai/enums/payment-means Essential payment means type indicators for categorizing different payment methods and instruments ## Payment Means Types
Type Description Use Cases
card\_details Credit/debit card payments Online purchases, POS transactions, recurring billing
account\_details Direct bank-to-bank transfer, Automated debit from account Subscriptions, recurring bills, utilities, Large payments, B2B transactions, invoices
wallet\_details Digital wallet payments Mobile payments, e-commerce, peer-to-peer
cash\_details Physical cash payment In-person transactions, small purchases
check\_details Paper check payment Traditional payments, business transactions
crypto\_details Cryptocurrency payment Digital goods, international transfers
other Other payment methods Alternative methods, custom solutions
# Payment Means Status Source: https://docs.wellapp.ai/enums/payment-means-status Essential payment means status indicators for tracking the lifecycle and availability of payment methods ## Payment Means Statuses
Status Description Usability Next Actions
active Payment means is active and ready to use Usable Use for transactions
inactive Payment means temporarily inactive by user Not usable Reactivate when needed
pending\_verification Awaiting verification before activation Not usable Complete verification process
suspended Temporarily suspended by system/admin Not usable Resolve issue, contact support
blocked Blocked due to fraud/security concerns Not usable Contact support, verify identity
expired Payment means has expired (e.g., card expiry) Not usable Update with new payment means
failed\_verification Verification failed, cannot be used Not usable Retry verification or add new
pending\_removal Scheduled for removal/deletion Not usable Complete removal process
removed Payment means has been removed/deleted Not usable Add new payment means
archived Archived for historical reference only Not usable View history only
# Registry Name Source: https://docs.wellapp.ai/enums/registry-name Global business registration authorities for company registration verification and validation across international jurisdictions The `registration.registry_name` field identifies the official business registration authority that issued the company registration number. This enables proper validation and verification of business registrations globally. **Current Implementation**: `"HRB"` (German Handelsregister) **Field Type**: `string` **Required**: No (optional) **Standards**: ISO 3166 country codes, official registry designations **Usage**: Name of the official registration body **Example**: `"Handelsregister Berlin"` ## 🇪🇺 European Union & Europe ### 🇩🇪 Germany
Code Full Name Description Jurisdiction
HRB Handelsregister Commercial Register Local courts (Amtsgericht)
HRA Handelsregister Abteilung A Sole traders & partnerships Local courts
GnR Genossenschaftsregister Cooperative Register Local courts
VR Vereinsregister Association Register Local courts
PR Partnerschaftsregister Partnership Register Local courts
### 🇬🇧 United Kingdom
Code Full Name Description Jurisdiction
CH Companies House UK Company Registry England, Wales, Scotland, NI
CHE Companies House England England & Wales Registry England & Wales
CHS Companies House Scotland Scottish Company Registry Scotland
CHNI Companies House Northern Ireland NI Company Registry Northern Ireland
FCA Financial Conduct Authority Financial Services UK Financial Services
PRA Prudential Regulation Authority Banking Regulation UK Banking
### 🇫🇷 France
Code Full Name Description Jurisdiction
RCS Registre du Commerce et des Sociétés Commercial & Companies Register Local commercial courts
CFE Centre de Formalités des Entreprises Business Formalities Center Regional centers
INPI Institut National de la Propriété Industrielle Industrial Property Institute France
RSI Régime Social des Indépendants Self-employed Social Scheme France
### 🇮🇹 Italy
Code Full Name Description Jurisdiction
RI Registro delle Imprese Business Register Chambers of Commerce
CCIAA Camera di Commercio Chamber of Commerce Provincial/regional
REA Repertorio Economico Amministrativo Economic Administrative Register Chambers of Commerce
### 🇪🇸 Spain
Code Full Name Description Jurisdiction
RM Registro Mercantil Commercial Register Provincial registers
AEAT Agencia Estatal de Administración Tributaria Tax Administration Agency Spain
CNMV Comisión Nacional del Mercado de Valores Securities Market Commission Spain
### 🇳🇱 Netherlands
Code Full Name Description Jurisdiction
KVK Kamer van Koophandel Chamber of Commerce Netherlands
HR Handelsregister Trade Register Netherlands
AFM Autoriteit Financiële Markten Financial Markets Authority Netherlands
### 🇧🇪 Belgium
Code Full Name Description Jurisdiction
BCE Banque Carrefour des Entreprises Crossroads Bank for Enterprises Belgium
KBO Kruispuntbank van Ondernemingen Crossroads Bank for Enterprises (NL) Belgium
FSMA Financial Services and Markets Authority Financial Regulation Belgium
### 🇨🇭 Switzerland
Code Full Name Description Jurisdiction
SOGC Swiss Official Gazette of Commerce Commercial Register Cantonal registers
FINMA Swiss Financial Market Supervisory Authority Financial Supervision Switzerland
SHAB Schweizerisches Handelsamtsblatt Swiss Commercial Gazette Switzerland
### 🇸🇪 Sweden
Code Full Name Description Jurisdiction
BV Bolagsverket Swedish Companies Registration Office Sweden
FI Finansinspektionen Financial Supervisory Authority Sweden
### 🇳🇴 Norway
Code Full Name Description Jurisdiction
BRREG Brønnøysundregistrene Brønnøysund Register Centre Norway
FSA Finanstilsynet Financial Supervisory Authority Norway
### 🇩🇰 Denmark
Code Full Name Description Jurisdiction
CVR Det Centrale Virksomhedsregister Central Business Register Denmark
DFSA Danish Financial Supervisory Authority Financial Supervision Denmark
## 🌎 Americas ### 🇺🇸 United States
Code Full Name Description Jurisdiction
SEC Securities and Exchange Commission Federal Securities Regulation United States
DE\_DOS Delaware Division of Corporations Delaware Corporate Registry Delaware
CA\_SOS California Secretary of State California Business Registry California
NY\_DOS New York Department of State New York Business Registry New York
TX\_SOS Texas Secretary of State Texas Business Registry Texas
FL\_DOS Florida Department of State Florida Business Registry Florida
NV\_SOS Nevada Secretary of State Nevada Business Registry Nevada
IRS Internal Revenue Service Federal Tax Registration United States
CFTC Commodity Futures Trading Commission Derivatives Regulation United States
### 🇨🇦 Canada
Code Full Name Description Jurisdiction
IC Innovation, Science and Economic Development Canada Federal Incorporation Canada
CBCA Canada Business Corporations Act Registry Federal Corporation Registry Canada
ON\_SG Ontario ServiceOntario Ontario Business Registry Ontario
BC\_REG BC Registry Services British Columbia Registry British Columbia
AB\_REG Alberta Registry Services Alberta Business Registry Alberta
QC\_REQ Registraire des entreprises du Québec Quebec Enterprise Registry Quebec
OSC Ontario Securities Commission Securities Regulation Ontario
### 🇲🇽 Mexico
Code Full Name Description Jurisdiction
SE Secretaría de Economía Ministry of Economy Mexico
RPP Registro Público de la Propiedad Public Property Registry State level
CNBV Comisión Nacional Bancaria y de Valores Banking & Securities Commission Mexico
CONDUSEF Comisión Nacional para la Protección y Defensa de los Usuarios de Servicios Financieros Financial User Protection Mexico
### 🇧🇷 Brazil
Code Full Name Description Jurisdiction
JUCERJA Junta Comercial do Estado do Rio de Janeiro Rio de Janeiro Commercial Board Rio de Janeiro
JUCESP Junta Comercial do Estado de São Paulo São Paulo Commercial Board São Paulo
DREI Departamento de Registro Empresarial e Integração Business Registration Department Brazil
CVM Comissão de Valores Mobiliários Securities Commission Brazil
BACEN Banco Central do Brasil Central Bank of Brazil Brazil
### 🇦🇷 Argentina
Code Full Name Description Jurisdiction
IGJ Inspección General de Justicia General Justice Inspection Buenos Aires
CNV Comisión Nacional de Valores National Securities Commission Argentina
BCRA Banco Central de la República Argentina Central Bank of Argentina Argentina
## 🌏 Asia-Pacific ### 🇦🇺 Australia
Code Full Name Description Jurisdiction
ASIC Australian Securities and Investments Commission Corporate Registry Australia
ABR Australian Business Register Business Registration Australia
ACNC Australian Charities and Not-for-profits Commission Charity Registration Australia
APRA Australian Prudential Regulation Authority Financial Institution Regulation Australia
### 🇳🇿 New Zealand
Code Full Name Description Jurisdiction
NZCO New Zealand Companies Office Company Registration New Zealand
NZBN New Zealand Business Number Business Identifier System New Zealand
FMA Financial Markets Authority Financial Market Regulation New Zealand
### 🇸🇬 Singapore
Code Full Name Description Jurisdiction
ACRA Accounting and Corporate Regulatory Authority Company Registration Singapore
MAS Monetary Authority of Singapore Financial Regulation Singapore
IPTO Intellectual Property Office of Singapore IP Registration Singapore
### 🇭🇰 Hong Kong
Code Full Name Description Jurisdiction
CR Companies Registry Company Registration Hong Kong
HKMA Hong Kong Monetary Authority Financial Regulation Hong Kong
SFC Securities and Futures Commission Securities Regulation Hong Kong
### 🇯🇵 Japan
Code Full Name Description Jurisdiction
MOJ Ministry of Justice Corporate Registration Japan
FSA Financial Services Agency Financial Regulation Japan
METI Ministry of Economy, Trade and Industry Business Registration Japan
### 🇰🇷 South Korea
Code Full Name Description Jurisdiction
SCSC Supreme Court of South Korea Corporate Registration South Korea
FSS Financial Supervisory Service Financial Regulation South Korea
KOTRA Korea Trade-Investment Promotion Agency Trade Registration South Korea
### 🇮🇳 India
Code Full Name Description Jurisdiction
MCA Ministry of Corporate Affairs Corporate Registration India
ROC Registrar of Companies Company Registration State level
SEBI Securities and Exchange Board of India Securities Regulation India
RBI Reserve Bank of India Banking Regulation India
### 🇨🇳 China
Code Full Name Description Jurisdiction
SAIC State Administration for Industry and Commerce Business Registration China
CSRC China Securities Regulatory Commission Securities Regulation China
CBRC China Banking Regulatory Commission Banking Regulation China
SAMR State Administration for Market Regulation Market Regulation China
## 🌍 Middle East & Africa ### 🇦🇪 United Arab Emirates
Code Full Name Description Jurisdiction
DED Dubai Department of Economic Development Dubai Business Registration Dubai
ADGM Abu Dhabi Global Market Financial Free Zone Abu Dhabi
DIFC Dubai International Financial Centre Financial Free Zone Dubai
SCA Securities and Commodities Authority Securities Regulation UAE
### 🇸🇦 Saudi Arabia
Code Full Name Description Jurisdiction
MC Ministry of Commerce Business Registration Saudi Arabia
CMA Capital Market Authority Securities Regulation Saudi Arabia
SAMA Saudi Arabian Monetary Authority Banking Regulation Saudi Arabia
### 🇿🇦 South Africa
Code Full Name Description Jurisdiction
CIPC Companies and Intellectual Property Commission Company Registration South Africa
JSE Johannesburg Stock Exchange Securities Market South Africa
SARB South African Reserve Bank Banking Regulation South Africa
### 🇮🇱 Israel
Code Full Name Description Jurisdiction
ROC\_IL Registrar of Companies Company Registration Israel
ISA Israel Securities Authority Securities Regulation Israel
BOI Bank of Israel Banking Regulation Israel
## 🌏 Other Regions ### 🇷🇺 Russia
Code Full Name Description Jurisdiction
FTS Federal Tax Service Business Registration Russia
CBR Central Bank of Russia Financial Regulation Russia
### 🇹🇷 Turkey
Code Full Name Description Jurisdiction
TOBB Turkish Union of Chambers and Commodity Exchanges Business Registration Turkey
CMB Capital Markets Board Securities Regulation Turkey
## 🏛️ International & Supranational
Code Full Name Description Jurisdiction
GLEIF Global Legal Entity Identifier Foundation Global LEI System International
BIS Bank for International Settlements International Banking International
OECD Organisation for Economic Co-operation and Development Economic Cooperation International
FATF Financial Action Task Force Anti-Money Laundering International
## ❓ Administrative & Fallback
Code Description Usage
OTHER Other registry Non-standard registries
UNKNOWN Unknown registry Cannot determine from document
REGIONAL Regional registry Local/regional authorities
MUNICIPAL Municipal registry City/local government
PRIVATE Private registry Private registration services
### Priority Tiers for Implementation ### 🔴 **Tier 1 - Critical (Implement First)** * **`HRB`** - German Handelsregister (current example) * **`CH`** - UK Companies House * **`SEC`** - US Securities and Exchange Commission * **`DE_DOS`** - Delaware Division of Corporations * **`ASIC`** - Australian Securities and Investments Commission * **`RCS`** - French Commercial Register * **`KVK`** - Netherlands Chamber of Commerce * **`ACRA`** - Singapore Corporate Registry ### 🟡 **Tier 2 - Important (Second Priority)** * **`IC`** - Innovation Canada (Federal) * **`MCA`** - India Ministry of Corporate Affairs * **`SAIC`** - China Business Registration * **`BCE`** - Belgium Enterprise Crossroads Bank * **`BV`** - Sweden Companies Registration Office * **`CVR`** - Denmark Central Business Register * **`NZCO`** - New Zealand Companies Office * **`CR`** - Hong Kong Companies Registry ### 🟢 **Tier 3 - Extended Coverage** * Regional and state-level registries * Specialized financial authorities * Emerging market registries * Industry-specific registries # Remittance Info Reference Source: https://docs.wellapp.ai/enums/remittance-info-reference Essential remittance information reference type indicators for categorizing payment reference formats ## Reference Types
Type Full Name Standard Format Use Cases
SCOR Structured Creditor Reference ISO 11649 RF + check digits + reference International payments, invoices, creditor references
QRR QR Reference Swiss QR-Bill 27 digits with check digit Swiss QR-bill payments, domestic Swiss transactions
ISR In-payment Slip with Reference Swiss ESR/ISR 27 digits (orange slip) Traditional Swiss payment slips, ESR references
IREF Internal Reference Proprietary Company-specific format Internal tracking, custom reference systems
EREF End-to-End Reference SEPA standard Max 35 chars alphanumeric SEPA transfers, European payments, transaction tracking
PREF Payment Reference Generic Varies by institution Purchase orders, invoice numbers, payment IDs
MREF Mandate Reference SEPA Direct Debit Max 35 chars alphanumeric Direct debit mandates, recurring payments
CRED Creditor Reference ISO 11649 variant Structured format Invoice references, accounts receivable
USTD Unstructured Free text Max 140 chars General notes, unstructured payment information
NON None/Not Applicable N/A No reference Payments without specific reference requirements
# Roles Source: https://docs.wellapp.ai/enums/role User role definitions for workspace access control and permission management User roles define the permissions and access levels within a workspace. These roles control what actions users can perform and what content they can access. ## Available Roles

admin

Full administrative access

member

Standard user access

owner

Ultimate ownership permissions

guest

Limited read-only access

## Role Comparison Table
Role Description User Management Content Access Workspace Settings Billing & Deletion
owner Ultimate ownership permissions ✅ Full control ✅ All content ✅ All settings ✅ Delete workspace
admin Full administrative access ✅ Manage users ✅ All content ✅ All settings ⚠️ Billing only
member Standard user access ❌ No access ⚠️ Own + shared ❌ Limited ❌ No access
guest Limited read-only access ❌ No access ⚠️ Shared only ❌ No access ❌ No access
## Role Hierarchy The roles follow a hierarchical structure where higher-level roles inherit permissions from lower levels: ``` owner > admin > member > guest ``` ## Usage in API When creating or updating memberships, use these exact role values: ```json theme={null} { "role": "admin" } ``` **Valid values:** `admin`, `member`, `owner`, `guest` # Scheme Type Source: https://docs.wellapp.ai/enums/sheme-type Essential scheme type indicators for categorizing different payment and banking schemes ## Scheme Types
Type Full Name Coverage Use Cases
visa Visa Global Credit/debit card payments worldwide
mastercard Mastercard Global Credit/debit card payments worldwide
amex American Express Global Credit/charge card payments, premium services
sepa Single Euro Payments Area Europe (36 countries) Euro bank transfers, direct debits
swift Society for Worldwide Interbank Financial Telecommunication Global International wire transfers, cross-border payments
ach Automated Clearing House US/North America Domestic bank transfers, direct deposits, bill payments
faster\_payments Faster Payments Service UK Near-instant bank transfers (UK)
iban International Bank Account Number Global (primarily Europe) Standardized bank account identification
local Local/Domestic Payment Scheme Country-specific Domestic payment systems (BACS, EFT, etc.)
other Other Payment Schemes Varies Alternative or proprietary schemes
# Tax Categories Source: https://docs.wellapp.ai/enums/tax-cat Global tax classification system supporting VAT, GST, sales tax, and international tax standards for comprehensive business compliance ## 📊 Standard VAT/GST Categories
Category Code Rate Range Description Global Usage Examples
standard S 15-25% Standard tax rate Most countries "Electronics, services"
reduced R 5-12% Reduced tax rate EU, many others "Food, books, medicine"
super\_reduced SR 0-5% Super reduced rate Some EU countries "Basic food, newspapers"
zero\_rated Z 0% Zero rate (taxable) UK, Canada, others "Exports, basic food"
exempt E N/A Tax exempt Worldwide "Insurance, education"
reverse\_charge RC Variable Customer pays tax B2B EU, others "Construction, telecom"
out\_of\_scope OS N/A Outside tax scope Worldwide "Non-business activities"
## 🏛️ Government & Administrative
Category Description Usage Examples
government Government services Tax-free govt "Public services, permits"
municipal Local government City/county "Local fees, permits"
regulatory Regulatory fees Compliance "Filing fees, licenses"
statutory Statutory charges Legal requirements "Court fees, registrations"
administrative Admin charges Processing fees "Document processing"
## 🏥 Healthcare & Medical
Category Description Rate Treatment Examples
medical\_exempt Medical services Exempt/Zero "Doctor visits, surgery"
medical\_reduced Medical supplies Reduced rate "Prescription drugs"
medical\_standard Medical devices Standard rate "Non-essential devices"
pharmaceutical Medicines Exempt/Reduced "Prescription medication"
hospital Hospital services Exempt "Healthcare services"
dental Dental services Varies "Dental treatment"
veterinary Animal healthcare Standard/Exempt "Pet medical care"
## 🎓 Education & Cultural
Category Description Rate Treatment Examples
education\_exempt Educational services Exempt "School tuition, training"
education\_reduced Educational materials Reduced rate "Textbooks, supplies"
books Books & publications Zero/Reduced "Books, magazines"
cultural Cultural services Reduced/Exempt "Museum tickets, theater"
research Research activities Exempt "Scientific research"
library Library services Exempt "Public library services"
## 💼 Financial & Insurance
Category Description Rate Treatment Examples
financial\_exempt Financial services Exempt "Banking, loans"
insurance\_exempt Insurance services Exempt "Life, health insurance"
investment Investment services Exempt/Standard "Fund management"
banking Banking services Exempt "Account services"
credit Credit services Exempt "Credit cards, loans"
foreign\_exchange FX services Exempt "Currency exchange"
## 💻 Technology & Digital
Category Description Rate Treatment Examples
software\_license Software licensing Standard rate "Software purchases"
software\_saas SaaS services Standard rate "Cloud subscriptions"
digital\_services Digital services Standard rate "Online services"
cloud\_computing Cloud services Standard rate "AWS, Azure"
data\_processing Data services Standard rate "Analytics, storage"
telecommunications\_digital Digital telecom Standard rate "VoIP, messaging"
electronic\_delivery E-delivery Standard rate "Digital downloads"
## 🍕 Food & Beverages
Category Description Rate Treatment Examples
food\_basic Basic food items Zero/Reduced "Bread, milk, vegetables"
food\_standard Processed foods Standard rate "Confectionery, snacks"
food\_luxury Luxury foods Standard/Higher ""Caviar, premium items""
beverages\_alcoholic Alcoholic beverages High rate "Wine, beer, spirits"
beverages\_non\_alcoholic Soft drinks Standard/Reduced "Soft drinks, juices"
restaurant Restaurant services Standard/Reduced "Dining, takeaway"
catering Catering services Standard rate "Event catering"
## 🚗 Transportation & Vehicles
Category Description Rate Treatment Examples
transport\_public Public transport Zero/Reduced "Bus, train, metro"
transport\_passenger Passenger services Standard rate "Taxi, rideshare"
transport\_freight Freight transport Standard rate "Shipping, delivery"
vehicle\_sales Vehicle sales Standard rate "Car, motorcycle sales"
vehicle\_parts Auto parts Standard rate "Car parts, accessories"
fuel Motor fuel Standard/Excise "Gasoline, diesel"
parking Parking services Standard rate "Parking fees"
## 🌍 International Trade
Category Description Rate Treatment Examples
export Export goods/services Zero rated "Goods sold abroad"
import Import goods Standard + Duty "Foreign purchases"
intracom\_supply EU intra-community Zero rated "EU B2B sales"
intracom\_acquisition EU acquisitions Standard rate "EU B2B purchases"
triangular Triangular transactions Special rules "EU three-party trade"
## ⚠️ Special Categories
Category Description Usage Examples
mixed\_rate Multiple tax rates Complex items "Bundled products/services"
threshold\_based Rate depends on amount Value-based "Luxury tax threshold"
seasonal Seasonal rates Time-dependent "Holiday specials"
promotional Promotional rates Temporary "Tax holidays"
margin\_scheme Margin taxation Used goods "Second-hand items"
reverse\_auction Auction taxation Special rules "Auction sales"
## ❓Administrative & Fallback
Category Description Usage Examples
unknown Tax category unclear OCR processing "Cannot determine category"
pending Classification pending Review needed "Awaiting tax determination"
other Other category Special cases "Unique tax situations"
not\_applicable No tax applies Non-taxable "Gift transactions"
## ⚡ Utilities & Energy
Category Description Rate Treatment Examples
utilities\_domestic Home utilities Reduced rate "Home electricity, gas"
utilities\_commercial Business utilities Standard rate "Commercial power"
water Water supply Reduced/Zero "Water services"
sewage Sewage services Reduced/Zero "Waste water"
waste\_management Waste services Standard rate "Garbage collection"
telecommunications Telecom services Standard rate "Phone, internet"
energy\_renewable Green energy Reduced rate "Solar, wind power"
## 🏭 Manufacturing & Industrial
Category Description Rate Treatment Examples
manufacturing Manufacturing Standard rate "Production services"
industrial\_equipment Equipment Standard rate "Machinery, tools"
raw\_materials Raw materials Standard/Reduced "Steel, chemicals"
chemicals Chemical products Standard rate "Industrial chemicals"
mining Mining products Standard rate "Extracted materials"
agriculture Agricultural Reduced/Zero "Farming supplies"
forestry Forest products Standard rate "Timber, paper"
## ⚖️ Legal & Professional Services
Category Description Rate Treatment Examples
legal\_services Legal services Standard/Exempt "Lawyer, notary fees"
accounting Accounting services Standard rate "Tax prep, audit"
consulting Consulting Standard rate "Advisory services"
professional Professional services Standard rate "Engineering, design"
notary Notary services Standard/Exempt "Document notarization"
## 🎨 Entertainment & Leisure
Category Description Rate Treatment Examples
entertainment Entertainment Standard rate "Movies, concerts"
sports Sports services Standard/Reduced "Gym, sports events"
gambling Gambling services Higher/Special "Casino, betting"
tourism Tourism services Standard rate "Hotels, tours"
hospitality Hospitality Standard rate "Hotels, accommodation"
recreation Recreation Standard rate "Theme parks, games"
## 🏠 Real Estate & Property
Category Description Rate Treatment Examples
property\_residential Residential property Exempt/Zero "Home sales, rent"
property\_commercial Commercial property Standard rate "Office rent, sales"
construction\_new New construction Zero/Reduced "New buildings"
construction\_renovation Renovations Standard rate "Home improvements"
land Land sales Exempt "Land transactions"
property\_management Property services Standard rate "Management fees"
## Usage Examples ### Example 1: Standard Software License ```json theme={null} { "item": "Enterprise Software License", "tax": { "category": "software_license", "rate": 20.0, "amount": 200.0 } } ``` ### Example 2: Medical Services ```json theme={null} { "item": "Medical Consultation", "tax": { "category": "medical_exempt", "rate": 0.0, "amount": 0.0 } } ``` ### Example 3: Educational Books ```json theme={null} { "item": "Textbooks", "tax": { "category": "education_reduced", "rate": 5.0, "amount": 2.5 } } ``` # Tax ID Source: https://docs.wellapp.ai/enums/tax-id Comprehensive tax identification types organized by category, covering global tax identification systems for business and individual tax reporting requirements ## Value Added Tax Systems
Value Description
VAT Value Added Tax (European Union, UK, and other countries)
VATIN VAT Identification Number
TVA Taxe sur la Valeur Ajoutée (French-speaking countries)
IVA Impuesto al Valor Agregado (Spanish/Portuguese-speaking countries)
MwSt Mehrwertsteuer (German-speaking countries)
BTW Belasting over de Toegevoegde Waarde (Dutch-speaking countries)
MOSS Mini One Stop Shop VAT
OSS One Stop Shop VAT
## Goods and Services Tax Systems
Value Description
GST Goods and Services Tax (Australia, Canada, India, Singapore, Malaysia, New Zealand)
GSTIN GST Identification Number (India specific format)
HST Harmonized Sales Tax (Canada)
PST Provincial Sales Tax (Canada)
QST Quebec Sales Tax (Canada)
SST Sales and Service Tax (Malaysia)
## General Tax Identification Numbers
Value Description
TIN Tax Identification Number (Generic)
TAX\_ID Tax Identification (Generic)
TAX\_REG Tax Registration Number
TAX\_CERT Tax Registration Certificate Number
## Business Registration Numbers
Value Description
ABN Australian Business Number
ACN Australian Company Number
BN Business Number (Canada)
BRN Business Registration Number
UEN Unique Entity Number (Singapore)
BIN Business Identification Number
USCI Unified Social Credit Identifier (China)
CRN Company Registration Number
## Corporate Tax Identifiers
Value Description
CIF Corporate Identity Code de Identificación Fiscal
NIF Número de Identificación Fiscal
NIE Número de Identidad de Extranjero
CNPJ Corporate Tax ID (Brazil format)
CUIT Corporate Tax ID (Argentina format)
RUT Corporate Tax ID (Chile/Colombia format)
RUC Corporate Tax ID (Peru/Ecuador format)
NIT Corporate Tax ID (Colombia/Bolivia format)
RFC Corporate Tax ID (Mexico format)
## Individual Tax Numbers
Value Description
SSN Social Security Number
SIN Social Insurance Number (Canada)
TFN Tax File Number (Australia)
ITIN Individual Taxpayer Identification Number
ATIN Adoption Taxpayer Identification Number
PTIN Preparer Tax Identification Number
CPF Individual Tax ID (Brazil format)
CUIL Individual Tax ID (Argentina format)
CURP Individual ID (Mexico format)
PAN Permanent Account Number (India)
NINO National Insurance Number (UK)
CF Codice Fiscale (Italy)
AFM Tax Registration Number (Greece)
NIPC Corporate Tax ID (Portugal)
OIB Personal Identification Number (Croatia)
## 👥 Employer Tax Numbers
Value Description
EIN Employer Identification Number
FEIN Federal Employer Identification Number
TAN Tax Deduction Account Number (India)
IE State Registration (Brazil)
IM Municipal Registration (Brazil)
## European Specific Systems
Value Description
SIRET Establishment Identification (France - 14 digits)
SIREN Company Identification (France - 9 digits)
UTR Unique Taxpayer Reference (UK)
RSIN Legal Entities Identifier (Netherlands)
KVK Chamber of Commerce Number (Netherlands)
BCE Enterprise Crossroads Bank (Belgium)
UID Enterprise Identification (Switzerland)
CVR Central Business Register (Denmark)
ORG Organization Number (Norway/Sweden)
NIP Tax Identification Number (Poland)
REGON Business Registry Number (Poland)
DIC Tax Identification Number (Czech Republic)
INN Individual Taxpayer Number (Russia)
KPP Tax Registration Reason Code (Russia)
EDRPOU Unified State Register Code (Ukraine)
## Asian Specific Systems
Value Description
HOJIN Corporate Number (Japan)
KOJIN Individual Number (Japan)
RRN Resident Registration Number (South Korea)
NPWP Tax Registration Number (Indonesia)
UBN Unified Business Number (Taiwan)
PIN Personal Identification Number (Kenya)
## Middle Eastern Specific Systems
Value Description
TRN Tax Registration Number (UAE)
VKN Tax Identity Number (Turkey)
## Registration Authority Codes
Value Description
CIN Corporate Identification Number (India)
FCRN Foreign Contribution Registration Number (India)
CDI Identification Code (Argentina)
RIF Fiscal Information Registry (Venezuela)
## Sales Tax Systems
Value Description
SALES\_TAX Generic Sales Tax Number
USE\_TAX Use Tax Number
EXCISE\_TAX Excise Tax Number
LUXURY\_TAX Luxury Tax Number
## Customs and Trade
Value Description
CUSTOMS\_ID Customs Identification Number
EORI Economic Operators Registration and Identification
AEO Authorized Economic Operator
CTPAT Customs-Trade Partnership Against Terrorism
## Specialized Tax Systems
Value Description
WITHHOLDING\_TAX Withholding Tax Number
TRANSFER\_TAX Transfer Tax Number
PROPERTY\_TAX Property Tax Number
PAYROLL\_TAX Payroll Tax Number
FRANCHISE\_TAX Franchise Tax Number
## Fallback Values
Value Description
OTHER Other/Unspecified Tax ID Type
UNKNOWN Unknown Tax ID Type
OTH Other (legacy code - maintain for backwards compatibility)
# Tax Scheme Source: https://docs.wellapp.ai/enums/tax-scheme Global tax system identification for VAT, GST, sales tax, and other tax regimes ## Overview The `tax.scheme` field identifies the tax system/regime applied to invoice items, supporting all major global tax frameworks from VAT to GST to sales taxes. **Current Implementation**: `["VAT", "GST", "sales_tax"]` **Field Type**: `string (enum)` **Required**: No (optional) **Standards**: OECD Tax Treaties, WTO Tax Codes **Usage**: The tax regime used *** ## Complete Tax Scheme Enum ### 🇪🇺 Value Added Tax (VAT) Systems
Scheme Full Name Regions Rate Range Description
VAT Value Added Tax EU, UK, 100+ countries 0-27% Most common global system
EU\_VAT European Union VAT 27 EU member states 15-27% Harmonized EU system
UK\_VAT United Kingdom VAT United Kingdom 0-20% Post-Brexit UK system
MOSS\_VAT Mini One Stop Shop VAT EU digital services Variable Digital services to consumers
OSS\_VAT One Stop Shop VAT EU cross-border Variable Cross-border B2C services
IOSS\_VAT Import One Stop Shop EU imports Variable Import of low-value goods
### 🌏 Goods and Services Tax (GST) Systems
Scheme Full Name Regions Rate Range Description
GST Goods and Services Tax Australia, Canada, India+ 0-28% Comprehensive consumption tax
AU\_GST Australian GST Australia 0-10% Broad-based consumption tax
CA\_GST Canadian GST Canada 5% Federal GST
CA\_HST Harmonized Sales Tax Canada (some provinces) 13-15% Combined federal + provincial
CA\_PST Provincial Sales Tax Canada (some provinces) 6-10% Provincial-level tax
CA\_QST Quebec Sales Tax Quebec, Canada 9.975% Quebec provincial tax
IN\_GST Indian GST India 0-28% Unified Indian tax system
IN\_CGST Central GST India 0-14% Central government GST
IN\_SGST State GST India 0-14% State government GST
IN\_IGST Integrated GST India (interstate) 0-28% Interstate transactions
IN\_UTGST Union Territory GST India (UT) 0-14% Union Territory GST
SG\_GST Singapore GST Singapore 0-9% Singapore consumption tax
MY\_GST Malaysian GST Malaysia 0-6% Malaysian goods/services tax
MY\_SST Sales and Service Tax Malaysia 6-10% Replaced GST in 2018
NZ\_GST New Zealand GST New Zealand 0-15% New Zealand consumption tax
### 🇺🇸 Sales Tax Systems
Scheme Full Name Regions Rate Range Description
SALES\_TAX Sales Tax US States, others 0-12% Point-of-sale taxation
US\_STATE\_TAX US State Sales Tax US States 0-7.5% State-level sales tax
US\_LOCAL\_TAX US Local Sales Tax US Cities/Counties 0-5% Local jurisdiction tax
US\_USE\_TAX US Use Tax US States Variable Tax on out-of-state purchases
CA\_RETAIL\_TAX California Retail Tax California, US 7.25-10.75% California state + local
NY\_SALES\_TAX New York Sales Tax New York, US 4-8.875% NY state + local
TX\_SALES\_TAX Texas Sales Tax Texas, US 6.25-8.25% Texas state + local
### 🌍 Regional Tax Systems
Scheme Full Name Regions Description
JCT Japan Consumption Tax Japan Japanese consumption tax (10%)
KR\_VAT Korean VAT South Korea Korean value-added tax (10%)
CN\_VAT Chinese VAT China Chinese value-added tax (6-13%)
RU\_VAT Russian VAT Russia Russian value-added tax (0-20%)
BR\_ICMS Brazilian ICMS Brazil State-level circulation tax
BR\_IPI Brazilian IPI Brazil Federal industrialized products
BR\_PIS\_COFINS PIS/COFINS Brazil Social contribution taxes
MX\_IVA Mexican IVA Mexico Mexican value-added tax (16%)
AR\_IVA Argentinian IVA Argentina Argentine value-added tax
CL\_IVA Chilean IVA Chile Chilean value-added tax (19%)
### 🌐 International and Trade Taxes
Scheme Full Name Application Description
CUSTOMS\_DUTY Customs Duty Imports Tax on imported goods
IMPORT\_DUTY Import Duty Imports Duty on goods entering country
EXPORT\_DUTY Export Duty Exports Duty on goods leaving country
ANTI\_DUMPING\_DUTY Anti-Dumping Duty Dumped imports Protective duty against dumping
COUNTERVAILING\_DUTY Countervailing Duty Subsidized imports Duty against subsidized imports
TARIFF Tariff Trade protection Protective trade barrier
### 💰 Excise and Luxury Taxes
Scheme Full Name Application Description
EXCISE\_TAX Excise Tax Specific goods Tax on specific products (alcohol, tobacco)
LUXURY\_TAX Luxury Tax High-value items Tax on luxury goods above threshold
SIN\_TAX Sin Tax Vice products Tax on alcohol, tobacco, gambling
CARBON\_TAX Carbon Tax Carbon emissions Environmental tax on carbon content
FUEL\_TAX Fuel Tax Motor fuels Tax on gasoline, diesel, etc.
TOBACCO\_TAX Tobacco Tax Tobacco products Specific excise on tobacco
ALCOHOL\_TAX Alcohol Tax Alcoholic beverages Specific excise on alcohol
DIGITAL\_TAX Digital Services Tax Digital services Tax on digital platform revenues
### ⚡ Environmental and Green Taxes
Scheme Full Name Application Description
ENVIRONMENTAL\_TAX Environmental Tax Environmental impact Tax on environmental damage
PLASTIC\_TAX Plastic Tax Plastic products Tax on plastic usage
PACKAGING\_TAX Packaging Tax Product packaging Tax on packaging materials
WASTE\_TAX Waste Tax Waste disposal Tax on waste generation
CONGESTION\_TAX Congestion Tax Traffic congestion Tax on city center driving
### 🏘️ Property and Asset Taxes
Scheme Full Name Application Description
PROPERTY\_TAX Property Tax Real estate Tax on property ownership
TRANSFER\_TAX Transfer Tax Property transfers Tax on property sales
STAMP\_DUTY Stamp Duty Property/documents Tax on legal documents
INHERITANCE\_TAX Inheritance Tax Inherited assets Tax on inherited property
GIFT\_TAX Gift Tax Gift transfers Tax on gifted property
WEALTH\_TAX Wealth Tax Net wealth Tax on total wealth
### 👥 Employment and Payroll Taxes
Scheme Full Name Application Description
PAYROLL\_TAX Payroll Tax Employee wages Tax on payroll expenses
SOCIAL\_SECURITY\_TAX Social Security Tax Social programs Tax for social security
UNEMPLOYMENT\_TAX Unemployment Tax Unemployment insurance Tax for unemployment benefits
DISABILITY\_TAX Disability Tax Disability insurance Tax for disability coverage
MEDICARE\_TAX Medicare Tax Healthcare Tax for Medicare (US)
### 🔬 Specialized and Industry Taxes
Scheme Full Name Application Description
FINANCIAL\_TRANSACTION\_TAX Financial Transaction Tax Financial trades Tax on financial transactions
BANK\_TAX Bank Tax Banking services Tax on banking activities
INSURANCE\_PREMIUM\_TAX Insurance Premium Tax Insurance premiums Tax on insurance premiums
TELECOM\_TAX Telecommunications Tax Telecom services Tax on telecom services
UTILITY\_TAX Utility Tax Utilities Tax on utility services
AVIATION\_TAX Aviation Tax Air travel Tax on aviation services
SHIPPING\_TAX Shipping Tax Maritime transport Tax on shipping services
### 🏢 Business and Corporate Taxes
Scheme Full Name Application Description
CORPORATE\_TAX Corporate Income Tax Business profits Tax on corporate earnings
WITHHOLDING\_TAX Withholding Tax Payments to non-residents Tax withheld at source
BRANCH\_PROFITS\_TAX Branch Profits Tax Foreign branch earnings Tax on foreign branch profits
TURNOVER\_TAX Turnover Tax Gross receipts Tax based on gross sales
GROSS\_RECEIPTS\_TAX Gross Receipts Tax Business receipts Tax on total business receipts
### 🏛️ Municipal and Local Taxes
Scheme Full Name Application Description
MUNICIPAL\_TAX Municipal Tax Local services Local government tax
CITY\_TAX City Tax City services City-level taxation
COUNTY\_TAX County Tax County services County-level taxation
DISTRICT\_TAX District Tax Special districts Special district levies
TOURIST\_TAX Tourist Tax Tourism Tax on tourists/visitors
OCCUPANCY\_TAX Occupancy Tax Hotel stays Tax on accommodation
### ❓ Administrative and Fallback
Scheme Description Usage
OTHER Other tax scheme Non-standard tax systems
MIXED Multiple tax schemes Items with multiple taxes
UNKNOWN Unknown tax scheme Cannot determine from OCR
NONE No tax scheme Tax-free transactions
PENDING Tax determination pending Awaiting classification
# Transaction Type Source: https://docs.wellapp.ai/enums/transaction Essential transaction type indicators for categorizing different types of financial transactions ## Transaction Types
Type Description Direction Use Cases
payment General payments to vendors/suppliers Outgoing Bill payments, vendor payments, general purchases
transfer Money transfers between accounts Bidirectional Internal transfers, account-to-account moves
deposit Incoming funds or deposits Incoming Customer payments, revenue, capital deposits
withdrawal Cash withdrawals or outgoing funds Outgoing ATM withdrawals, cash disbursements
card\_payment Credit/debit card transactions Outgoing Card purchases, POS transactions, online shopping
direct\_debit Automated recurring payments Outgoing Subscriptions, recurring bills, automated payments
refund Return of previously paid funds Incoming Customer refunds, returned payments, chargebacks
fee Service fees and charges Outgoing Bank fees, transaction fees, service charges
interest Interest earned or paid Bidirectional Interest income, interest charges, loan interest
other Miscellaneous transaction types Varies Unclassified, special cases, custom types
# Universal Document Classification Source: https://docs.wellapp.ai/enums/universal-document-class Comprehensive business document classification system for automated document processing ## Document Categories Classify the document as one of the following types: ### Financial Documents
Document Type Description
invoice A bill requesting payment for goods/services provided
receipt Proof of payment received for goods/services
statement\_of\_account A summary of account transactions over a period
remittance\_advice A document sent with payment explaining what it covers
payment\_confirmation Confirmation that a payment has been processed
payment\_request A formal request for payment
dunning\_notice A demand letter for overdue payment
bank\_details Banking information or account details
wire\_transfer\_instructions Instructions for electronic money transfers
checks Bank checks or cheques
credit\_note Document issued to reduce amount owed (returns, discounts)
debit\_note Document increasing amount owed (additional charges)
pro\_forma\_invoice Preliminary bill sent before goods/services delivery
bank\_statement Record of bank account transactions over a period
expense\_report Summary of business expenses for reimbursement
### Sales & Procurement Documents
Document Type Description
purchase\_order A buyer's request to purchase goods/services
sales\_order A seller's confirmation of a sale transaction
quote\_estimate A preliminary pricing proposal for goods/services
proposal A detailed business proposal or bid
order\_form A standardized form for placing orders
order\_confirmation Confirmation that an order has been received/accepted
change\_order A modification to an existing order or contract
### Logistics Documents
Document Type Description
shipping\_notice Notification of goods being shipped
delivery\_note Confirmation of goods delivered
packing\_list List of items in a shipment
bill\_of\_lading Shipping document and receipt of goods
goods\_received\_note Confirmation of goods received and inspected
### HR & Employment Documents
Document Type Description
payslip Employee salary payment confirmation and details
timesheet Record of hours worked
employment\_contract Job agreement terms
### Legal & Contract Documents
Document Type Description
contract Legal agreement between parties
purchase\_agreement Formal agreement to buy goods/services
service\_agreement Contract for service provision
non\_disclosure\_agreement Confidentiality agreement (NDA)
terms\_and\_conditions Legal terms governing transactions
### Operations Documents
Document Type Description
inventory\_report Stock levels and inventory details
return\_merchandise\_authorization Authorization to return goods (RMA)
### Compliance Documents
Document Type Description
tax\_form Tax-related documents (W-2, 1099, etc.)
customs\_declaration Import/export documentation
certificate\_of\_origin Document certifying goods' country of origin
compliance\_report Regulatory compliance documentation
# Web Link Source: https://docs.wellapp.ai/enums/web-link Essential web link platform indicators for categorizing different types of web links and online presence. Limited to 10 core platforms for simplicity and broad coverage. ## Web Link Platforms
Platform Description Primary Use Case URL Pattern
website Official company or personal websites Main web presence, corporate sites [https://example.com](https://example.com)
linkedin Professional networking platform Professional profiles, company pages [https://linkedin.com/in/](https://linkedin.com/in/)*, [https://linkedin.com/company/](https://linkedin.com/company/)*
github Code repository and development platform Developer profiles, project repositories [https://github.com/](https://github.com/)\*
twitter Social media microblogging platform Social presence, announcements [https://twitter.com/](https://twitter.com/)*, [https://x.com/](https://x.com/)*
facebook Social media networking platform Business pages, community presence [https://facebook.com/](https://facebook.com/)\*
instagram Visual social media platform Brand imagery, visual content [https://instagram.com/](https://instagram.com/)\*
youtube Video sharing and streaming platform Video content, channels, tutorials [https://youtube.com/](https://youtube.com/)*, [https://youtu.be/](https://youtu.be/)*
portfolio Personal or professional portfolio sites Creative work, case studies, showcases Various domains, portfolio platforms
blog Blogging platforms and personal blogs Content marketing, thought leadership Medium, Substack, personal domains
other Miscellaneous platforms and custom links Any platform not covered above Various domains and platforms
# Authentication Source: https://docs.wellapp.ai/mcp/authentication API Key and OAuth authentication for Well MCP Well MCP supports two authentication methods: **API Key** for simple setups and **OAuth** for production use. ## API Key (Simple) Best for: personal use, testing, development ### How to Get an API Key 1. Log into [Well Dashboard](https://app.wellapp.ai) 2. Go to **Settings** > **API Keys** 3. Click **Generate New Key** 4. Copy the key immediately (it won't be shown again) ### Usage Include the API key in the `Authorization` header: ``` Authorization: Bearer YOUR_API_KEY ``` Or set it as an environment variable for stdio transport: ```bash theme={null} WELL_API_KEY=your_api_key_here ``` Never commit API keys to version control. Use environment variables or secure secret management. *** ## OAuth (Recommended) Best for: production, multi-user, shared access OAuth provides secure, token-based authentication with automatic refresh. ### How It Works Most MCP clients (Claude, Cursor, Windsurf, ChatGPT) support adding custom MCP servers with just a URL. Simply enter: ``` https://api.wellapp.ai/v1/mcp ``` The client handles everything automatically: 1. Redirects you to Well's authorization page 2. You log in and authorize access 3. Redirects back to your client with tokens 4. Handles token refresh automatically That's it - no manual configuration required. *** ## Which Should I Use? | Use Case | Recommended | | ----------------- | ----------- | | Personal testing | API Key | | Development | API Key | | Production app | OAuth | | Multi-user access | OAuth | | Shared workspaces | OAuth | Start with an API key for testing, then switch to OAuth for production deployments. # Changelog Source: https://docs.wellapp.ai/mcp/changelog Version history for Well MCP Server ## v1.0.0 (January 2025) **Initial Release** ### Tools * `well_get_schema` - Discover available data types and fields * `well_query_records` - Query invoices, companies, people, documents, connectors * `well_create_company` - Create new companies * `well_create_person` - Create new contacts ### Features * OAuth 2.0 authentication with Dynamic Client Registration (RFC 7591) * API Key authentication support * Streamable HTTP transport (Claude web) * SSE transport (Claude Desktop, Cursor, Windsurf) * Full MCP protocol compliance ### Supported Clients * Claude Desktop * Claude Code * Cursor * Windsurf * ChatGPT # Client Setup Source: https://docs.wellapp.ai/mcp/clients Configure Well MCP in your AI client ## Permissions & Scopes When you authorize Well MCP, you grant access to: | Scope | Description | | ----------------- | ------------------------------ | | `invoices:read` | View invoices and related data | | `companies:read` | View company records | | `companies:write` | Create new companies | | `people:read` | View contact information | | `people:write` | Create new contacts | | `documents:read` | View uploaded documents | | `connectors:read` | View connected integrations | You can revoke access anytime from [Well Dashboard > Settings > API Keys](https://app.wellapp.ai/settings/api-keys). *** ## Claude Desktop Available on Pro, Max, Team, and Enterprise plans.
Open **Claude Desktop** → **Settings** → **Connectors** Click **Add custom connector** ``` https://api.wellapp.ai/v1/mcp ``` Click **Add** and log in with your Well account
**Location:** * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` * Windows: `%APPDATA%\Claude\claude_desktop_config.json` ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "@wellapp/mcp"], "env": { "WELL_API_KEY": "YOUR_API_KEY", "WELL_API_URL": "https://api.wellapp.ai/v1" } } } } ``` Restart Claude Desktop after saving.
*** ## Claude Code ```bash theme={null} # Add Well MCP server claude mcp add --transport http well https://api.wellapp.ai/v1/mcp # Authenticate (opens browser) /mcp ``` **Scope options:** | Flag | Description | | ----------------- | ------------------------------ | | `--scope local` | Current project only (default) | | `--scope project` | Shared via `.mcp.json` | | `--scope user` | All your projects | ```bash theme={null} claude mcp add well -- npx -y @wellapp/mcp \ --env WELL_API_KEY="YOUR_API_KEY" \ --env WELL_API_URL="https://api.wellapp.ai/v1" ``` **Location:** `~/.claude.json` (user) or `.mcp.json` (project) ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "@wellapp/mcp"], "env": { "WELL_API_KEY": "YOUR_API_KEY", "WELL_API_URL": "https://api.wellapp.ai/v1" } } } } ``` **Useful commands:** ```bash theme={null} claude mcp list # List all MCP servers claude mcp remove well # Remove a server claude mcp get well # Get server info ``` *** ## Cursor Click **gear icon** → **Cursor Settings** → **Tools & Integrations** Under **MCP Tools**, click **Add Custom MCP** → opens `~/.cursor/mcp.json` Paste one of the configurations below and save ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.wellapp.ai/v1/mcp"] } } } ``` After saving, a browser window will open to authenticate with your Well account. ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "@wellapp/mcp"], "env": { "WELL_API_KEY": "YOUR_API_KEY", "WELL_API_URL": "https://api.wellapp.ai/v1" } } } } ``` *** ## Windsurf Press `Cmd + ,` (Mac) or `Ctrl + ,` (Windows) Scroll to **Cascade** → **MCP Servers** Click **Add Server** → **Add custom server** Enter name: `well` and URL: `https://api.wellapp.ai/v1/mcp` Log in with your Well account when prompted **Location:** * macOS: `~/.codeium/windsurf/mcp_config.json` * Windows: `%USERPROFILE%\.codeium\windsurf\mcp_config.json` ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.wellapp.ai/v1/mcp"] } } } ``` ```json theme={null} { "mcpServers": { "well": { "command": "npx", "args": ["-y", "@wellapp/mcp"], "env": { "WELL_API_KEY": "YOUR_API_KEY", "WELL_API_URL": "https://api.wellapp.ai/v1" } } } } ``` *** ## ChatGPT Requires Pro or Plus plan with Developer Mode enabled. ChatGPT only supports remote MCP servers (no local/stdio mode). Open **ChatGPT** → **Settings** → **Advanced** → Enable **Developer mode** Go to **Settings** → **Connectors** → **Add custom connector** `https://api.wellapp.ai/v1/mcp` Log in with your Well account *** ## Summary | Client | OAuth | API Key | | -------------- | -------------------------- | ------------------ | | Claude Desktop | Settings UI | Config file | | Claude Code | CLI | CLI or config file | | Cursor | Config file (`mcp-remote`) | Config file | | Windsurf | Settings UI | Config file | | ChatGPT | Settings UI | — | *** ## Test Your Connection Once configured, try these prompts: ```text Invoices theme={null} Show me my invoices ``` ```text Companies theme={null} What companies do I have in Well? ``` ```text Filters theme={null} Find all unpaid invoices over 1000 EUR ``` *** ## Troubleshooting This error occurs with `mcp-remote`. Try: 1. Clear mcp-remote cache: - macOS/Linux: `rm -rf ~/.mcp-auth` - Windows: `rmdir /s /q "%USERPROFILE%\.mcp-auth"` 2. Restart Cursor completely 3. Make sure you're using `https://api.wellapp.ai/v1/mcp` exactly 1. Make sure you're logged into Well 2. Clear browser cookies for `wellapp.ai` 3. Check your browser isn't blocking popups 1. Verify the key is correct (no extra spaces) 2. Check the key hasn't expired 3. Regenerate from [Well Dashboard](https://app.wellapp.ai) if needed 1. Remove the Well server from settings 2. Add it again 3. Re-authenticate 1. Verify your Well workspace has data 2. Check you authorized the correct workspace **Claude Desktop:** - macOS: `~/Library/Logs/Claude/mcp*.log` - Windows: `%APPDATA%\Claude\logs\mcp*.log` *** ## References * [Claude Desktop - Custom Connectors](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp) * [Claude Code - MCP Docs](https://docs.anthropic.com/en/docs/claude-code/mcp) * [Cursor - MCP Documentation](https://docs.cursor.com/context/model-context-protocol) * [Windsurf - MCP Documentation](https://docs.windsurf.com/windsurf/cascade/mcp) * [OpenAI - MCP Documentation](https://platform.openai.com/docs/mcp) # Introduction Source: https://docs.wellapp.ai/mcp/introduction Connect your AI assistant to Well financial data ## What is MCP? The **Model Context Protocol (MCP)** is an open standard that allows AI assistants to securely connect to external data sources and tools. Instead of copy-pasting data into your AI chat, MCP enables direct, real-time access to your Well data. ## Why Use Well MCP? Ask questions like "Show me unpaid invoices over 1000 EUR" and get instant answers Access your latest invoices, companies, and contacts directly Create companies and contacts through natural conversation OAuth authentication keeps your data safe ## How It Works **AI Client** → **Well MCP Server** → **Well API** → **Your Data** Your AI client connects to the Well MCP server, which securely fetches data from your Well workspace using OAuth authentication. ## Capabilities | Category | Data Types | Status | | ------------------- | --------------------- | ----------- | | **Financial Data** | Invoices | Available | | **Financial Data** | Payments, Accounts | Coming Soon | | **Contacts** | Companies, People | Available | | **Documents** | Documents, Connectors | Available | | **Payment Methods** | Payment Means | Coming Soon | New capabilities are added regularly. Check the [Tools Reference](/mcp/tools-reference) for the latest available tools. ## Supported Clients Well MCP works with any MCP-compatible client: } href="/mcp/clients#claude-desktop"> Anthropic's desktop app } href="/mcp/clients#claude-code"> CLI for developers } href="/mcp/clients#cursor"> AI-powered IDE } href="/mcp/clients#windsurf"> AI-powered IDE } href="/mcp/clients#chatgpt"> OpenAI's assistant ## Next Steps Get up and running in 5 minutes Configure your specific AI client ## Privacy & Support * **Privacy Policy:** [wellapp.ai/privacy](https://wellapp.ai/privacy) * **Support:** [support@wellapp.ai](mailto:support@wellapp.ai) * **Documentation:** [docs.wellapp.ai](https://docs.wellapp.ai) # Quickstart Source: https://docs.wellapp.ai/mcp/quickstart Get started with Well MCP in 5 minutes ## Prerequisites Before you begin, ensure you have: Active workspace with data at [app.wellapp.ai](https://app.wellapp.ai) From [Settings > API Keys](https://app.wellapp.ai/settings/api-keys) Claude, Cursor, Windsurf, or ChatGPT Required for local/stdio transport only *** ## Option A: Settings UI (Recommended) Most clients support adding custom MCP servers directly. Just add: ``` https://api.wellapp.ai/v1/mcp ``` Then authenticate with your Well account. Done! → [See detailed steps per client](/mcp/clients) *** ## Option B: Config File For manual setup or automation, use an API key: 1. Log into [Well Dashboard](https://app.wellapp.ai) 2. Go to **Settings** > **API Keys** 3. Click **Generate New Key** 4. Copy the key and add it to your client's config file → [See config examples per client](/mcp/clients) *** ## Test the Connection Once configured, try these prompts: ``` Show me my invoices ``` ``` What companies do I have in Well? ``` ``` Find all unpaid invoices over 1000 EUR ``` If you see your data, you're all set! ## Troubleshooting 1. Verify your API key is correct 2. Restart your AI client completely 3. Check the config file syntax (valid JSON) 1. Ensure your workspace has data 2. Check that your API key has access to the workspace 1. Regenerate your API key 2. Check for extra spaces or characters in the key ## Next Steps Learn about API Key vs OAuth See all available MCP tools # Tools Reference Source: https://docs.wellapp.ai/mcp/tools-reference Available MCP tools for Well ## well\_get\_schema Discover available data types and fields. | Parameter | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------------------------------------------------------------------- | | `root` | string | No | `invoices`, `companies`, `people`, `documents`, `connectors`, `transactions`, `accounts`, `payment_means` | | `depth` | number | No | 0=scalars, 1=relations (default), 2=nested | ## well\_query\_records Query records from Well's database. | Parameter | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------------------- | | `root` | string | Yes | Entity type | | `fields` | array | No\* | Field paths as arrays, e.g. `["invoices", "grand_total"]` | | `allFields` | boolean | No\* | Fetch all scalar fields | | `limit` | number | No | Max records (default 50, max 500) | \*Either `fields` or `allFields: true` required ## well\_create\_company Create a new company. | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------- | | `name` | string | Yes | Company name | | `domain` | string | No | Website domain | | `tax_id` | string | No | Tax ID | ## well\_create\_person Create a new contact. | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ------------- | | `first_name` | string | Yes | First name | | `last_name` | string | No | Last name | | `email` | string | No | Email address | | `phone` | string | No | Phone (E.164) | *** ## Examples ### Example 1: Query financial data **User prompt:** ``` Show me all unpaid invoices over 1000 EUR from the last 3 months ``` **What happens:** * Server calls `well_get_schema("invoices")` to discover available fields * Server calls `well_query_records` with invoice root, filtering by status and amount * Returns matching invoices with invoice number, amount, due date, and issuer name * AI presents results in a formatted table *** ### Example 2: Create a new contact **User prompt:** ``` Create a new contact: John Doe, email john@acme.com, CEO at Acme Corp ``` **What happens:** * Server calls `well_create_company` to create Acme Corp if it doesn't exist * Server calls `well_create_person` with first\_name, last\_name, email, and job\_title * Returns confirmation with the new person ID and company association * Contact is immediately available in your Well workspace *** ### Example 3: Build a financial dashboard **User prompt:** ``` Create a complete financial dashboard in React with shadcn/ui dark mode showing: - KPI cards: Total received, Total spent, Net balance, Invoice count - Area chart of cash flow over 12 months - Tabs for Income, Expenses, Analytics - Pie charts for client/supplier breakdown - Use my actual invoice data from Well ``` **What happens:** * Server calls `well_query_records` to fetch all invoices with issuer, receiver, amounts, and dates * Server calls `well_query_records` to fetch companies data * AI analyzes the data structure and generates a complete React dashboard artifact * Dashboard displays real financial data with interactive charts (Recharts) * Includes filtering, sorting, and responsive dark mode design *** ### Example 4: Query transactions **User prompt:** ``` Show me all transactions from last month ``` **What happens:** * Server calls `well_get_schema("transactions")` to discover available fields * Server calls `well_query_records` with transactions root, filtering by date * Returns matching transactions with amounts, dates, and associated accounts * AI presents results in a formatted table *** ### Example 5: View connected accounts **User prompt:** ``` What bank accounts are connected to my workspace? ``` **What happens:** * Server calls `well_get_schema("accounts")` to discover available fields * Server calls `well_query_records` with accounts root * Returns all connected accounts with provider, status, and balance info * AI presents a summary of connected financial accounts # Account Balance Source: https://docs.wellapp.ai/object-reference/account_balances AccountBalance represents a time-bounded balance snapshot for a bank account, recording opening and closing booked/value figures in a specified currency alongsi AccountBalance represents a time-bounded balance snapshot for a bank account, recording opening and closing booked/value figures in a specified currency alongside the valid-from/to period window. It is produced by the financial data ingestion pipeline (connector syncs) and acts as the join anchor for all Transactions, which reference the balance period they were captured within via a foreign key. Each AccountBalance belongs to exactly one Account (and transitively to one Workspace), and includes a suite of verification fields that the reconciliation pipeline populates to flag discrepancies between the bank-reported closing figure and the computed sum of transactions. | Naming | Value | | ------------------------------- | ------------------ | | Object | Account Balance | | Resource type (JSON:API `type`) | `account_balance` | | Collection / records root | `account_balances` | | REST base | `/v1/balances` | | Entity class | `AccountBalance` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ------------- | | List | `GET /v1/balances` | ✅ Implemented | | Retrieve | `GET /v1/balances/{id}` | ✅ Implemented | | Create | `POST /v1/balances` | 🟡 Planned | | Update | `PATCH /v1/balances/{id}` | 🟡 Planned | | Delete | `DELETE /v1/balances/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------------------------- | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | account\_balance\_id | string, UUID | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public stable identifier for this balance snapshot. Used in all API responses and external references. Never expose the internal pk. | | accounting\_balance | object (JSONB), nullable | ⚪ No | JSONB; all sub-fields documented below | — | Bank-reported balance figures for this period. Contains four monetary fields (opening\_booked, opening\_value, closing\_booked, closing\_value) and the ISO-4217 currency code. closing\_booked and closing\_value may be null for open (current) periods. | | accounting\_balance.opening\_booked | number | ⚪ No | JSONB sub-field; display\_type: amount | — | Booked (settled) opening balance for the period in the account's currency. | | accounting\_balance.opening\_value | number | ⚪ No | JSONB sub-field; display\_type: amount | — | Value (including pending) opening balance for the period. | | accounting\_balance.closing\_booked | number \| null | ⚪ No | JSONB sub-field; display\_type: amount; null for open periods | — | Booked closing balance at the end of the period. Null when balance\_at\_to is null (current open period). | | accounting\_balance.closing\_value | number \| null | ⚪ No | JSONB sub-field; display\_type: amount; null for open periods | — | Value closing balance at the end of the period. Null when balance\_at\_to is null. | | accounting\_balance.currency | string | ⚪ No | JSONB sub-field; display\_type: unique\_key; ISO-4217 3-letter code | — | ISO-4217 currency code for all monetary figures within this accounting\_balance object. | | foreign\_exchange | array of objects (JSONB), nullable | ⚪ No | JSONB array; each element has currency\_rate, currency\_pair, currency\_rate\_source, currency\_rate\_at | — | Optional array of FX rate snapshots applied to this balance period. Carries the rate, the pair (e.g. EUR/USD), the source (e.g. ECB), and the timestamp of the rate. Populated by connectors that provide multi-currency balance data. | | foreign\_exchange\[].currency\_rate | number | ⚪ No | JSONB sub-field | — | The exchange rate value for the currency pair at currency\_rate\_at. | | foreign\_exchange\[].currency\_pair | string | ⚪ No | JSONB sub-field | — | Currency pair in ISO-4217 slash notation (e.g. EUR/USD). | | foreign\_exchange\[].currency\_rate\_source | string | ⚪ No | JSONB sub-field | — | Source of the exchange rate (e.g. ECB, Plaid, provider-specific). | | foreign\_exchange\[].currency\_rate\_at | string (ISO-8601 timestamp) | ⚪ No | JSONB sub-field | — | Timestamp at which the FX rate was observed or fixed. | | balance\_at\_from | string (timestamp), nullable | ⚪ No | columnType: timestamp; display\_type: datetime; indexed in idx\_account\_balances\_date\_range; also in partial index idx\_account\_balances\_current | — | Start of the validity window for this balance snapshot. Inclusive lower bound of the period. Combined with balance\_at\_to it defines the interval. Indexed for efficient period range lookups. | | balance\_at\_to | string (timestamp), nullable | ⚪ No | columnType: timestamp; display\_type: datetime; indexed in idx\_account\_balances\_date\_range; null when period is still open (current balance) | — | End of the validity window. Null for the current (open) balance period. The partial index idx\_account\_balances\_current targets rows WHERE balance\_at\_to IS NULL for fast current-balance lookups. | | verified\_at | string (timestamp), nullable | ⚪ No | columnType: timestamp | — | Timestamp when the reconciliation pipeline last successfully verified this balance period (i.e. sum of transactions matched the closing\_booked figure within tolerance). | | verification\_error | boolean, nullable | ⚪ No | nullable boolean | true \| false \| null | Flag set by the reconciliation pipeline. True when calculated\_balance\_diff does not equal expected\_balance\_diff. Null means verification has not yet run for this period. | | verification\_error\_detail | string (text), nullable | ⚪ No | type: text | — | Human-readable explanation of the verification failure. Populated only when verification\_error is true. Contains the discrepancy detail reported by the reconciliation pipeline. | | calculated\_balance\_diff | number (numeric), nullable | ⚪ No | type: numeric(10,0) — integer precision, no decimal places | — | The balance difference computed by the verification pipeline: sum of transaction amounts in the period minus the expected delta between opening and closing booked balances. | | expected\_balance\_diff | number (numeric), nullable | ⚪ No | type: numeric(10,0) — integer precision, no decimal places | — | The expected balance difference derived from accounting\_balance (closing\_booked minus opening\_booked). Compared against calculated\_balance\_diff to detect missing or duplicate transactions. | | verification\_last\_run\_at | string (timestamp), nullable | ⚪ No | columnType: timestamp | — | Timestamp of the most recent reconciliation pipeline run against this balance period, regardless of outcome. Distinct from verified\_at, which is only stamped on success. | | raw\_data | unknown (JSONB), nullable | ⚪ No | JSONB; shape is connector-specific | — | Complete provider-native payload from which this balance was derived (e.g. the full Plaid account/balance JSON response). Shape varies by connector. Preserved for audit and re-processing; not exposed in standard API responses. | | created\_at | string (timestamptz), 🔒 system | ✅ Yes | onCreate: () => new Date(); not null | — | Timestamp when the record was first persisted. Set automatically by the MikroORM onCreate lifecycle hook. | | updated\_at | string (timestamptz), 🔒 system | ⚪ No | onCreate and onUpdate: () => new Date() | — | Timestamp of the last update. Set automatically on create and every subsequent write by the MikroORM onUpdate lifecycle hook. | | deleted\_at | string (timestamptz), nullable | ⚪ No | nullable; soft-delete sentinel; partial index idx\_account\_balances\_current WHERE deleted\_at IS NULL | — | Soft-delete timestamp. Null means the record is active. All queries must filter deleted\_at IS NULL. Also guards the partial index idx\_account\_balances\_workspace\_deleted. | ### Relationships | Name | Type | Required | Description | | ------------ | --------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | account | to-one (Account) | ⚪ No (nullable) | The bank account this balance snapshot belongs to. FK: account\_pk → core\_api.accounts.pk. ON UPDATE CASCADE / ON DELETE SET NULL. Indexed via idx\_account\_balances\_account. Also part of the partial index idx\_account\_balances\_current for current-balance lookups. | | workspace | to-one (Workspace) | ⚪ No (nullable) | Tenant scope. FK: workspace\_pk → core\_api.workspaces.pk. ON UPDATE CASCADE / ON DELETE SET NULL. Every query must filter by workspace. Indexed together with deleted\_at in idx\_account\_balances\_workspace\_deleted for Hasura permission filter hot-path. | | transactions | to-many (Transaction) | — | All Transaction records that fall within this balance period. Inverse of Transaction.account\_balance (FK: transactions.account\_balance\_pk). A single AccountBalance can hold zero to many transactions. Used by composites such as composite\_total\_amount\_currency on the accounts root. | ### System-computed * account\_balance\_id: generated by PostgreSQL gen\_random\_uuid() on INSERT (defaultRaw). Unique constraint enforced at the database level. * created\_at: set to new Date() by the MikroORM onCreate lifecycle hook; never writable by the API consumer. * updated\_at: set to new Date() on both onCreate and onUpdate by MikroORM lifecycle hooks. * deleted\_at: null on creation; set by the application soft-delete pattern. All active-record queries filter WHERE deleted\_at IS NULL. * Partial index idx\_account\_balances\_current on (account\_pk, balance\_at\_from DESC, pk DESC) WHERE balance\_at\_to IS NULL AND deleted\_at IS NULL — enables an index-only scan with LIMIT 1 for the 'current balance' lookup pattern. * Partial index idx\_account\_balances\_workspace\_deleted on (workspace\_pk, deleted\_at) — optimises the Hasura permission filter (workspace\_pk = \$1 AND deleted\_at IS NULL) on the hot read path. * verification\_error, verification\_error\_detail, calculated\_balance\_diff, expected\_balance\_diff, verified\_at, verification\_last\_run\_at: all populated exclusively by the reconciliation pipeline; never set by the ingestion connector or by edge API writes. * raw\_data: written once by the ingestion connector at sync time; treated as an immutable audit trail. Shape is connector-specific (e.g. Plaid account/balance JSON). * accounting\_balance and foreign\_exchange: written by the connector sync pipeline from provider-native balance data. Not editable via the public API. ## Example ```json theme={null} { "data": { "type": "account_balance", "id": "c3f8d2a1-4e57-4b2c-9a1d-88f702b63c14", "attributes": { "account_balance_id": "c3f8d2a1-4e57-4b2c-9a1d-88f702b63c14", "accounting_balance": { "opening_booked": 142500.00, "opening_value": 142500.00, "closing_booked": 138920.50, "closing_value": 138920.50, "currency": "EUR" }, "foreign_exchange": [ { "currency_rate": 1.0832, "currency_pair": "EUR/USD", "currency_rate_source": "ECB", "currency_rate_at": "2026-05-31T00:00:00.000Z" } ], "balance_at_from": "2026-05-01T00:00:00.000Z", "balance_at_to": "2026-05-31T23:59:59.000Z", "verified_at": "2026-06-01T06:15:22.000Z", "verification_error": false, "verification_error_detail": null, "calculated_balance_diff": 0, "expected_balance_diff": 0, "verification_last_run_at": "2026-06-01T06:15:22.000Z", "raw_data": null, "created_at": "2026-05-01T04:00:12.000Z", "updated_at": "2026-06-01T06:15:22.000Z", "deleted_at": null }, "relationships": { "account": { "data": { "type": "account", "id": "a1b2c3d4-0001-4000-8000-000000000001" } }, "workspace": { "data": { "type": "workspace", "id": "w9f8e7d6-0001-4000-8000-000000000001" } }, "transactions": { "data": [ { "type": "transaction", "id": "t0000001-0001-4000-8000-000000000001" }, { "type": "transaction", "id": "t0000002-0001-4000-8000-000000000002" } ] } } } } ``` Source: `apps/api/src/database/entities/AccountBalance.ts` · domain: financial-graph · tier: Supporting # AccountWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/account_workspace_connectors AccountWorkspaceConnector is a per-entity junction record that tracks provenance for an Account → WorkspaceConnector association AccountWorkspaceConnector is a per-entity junction record that tracks provenance for an Account → WorkspaceConnector association. Each row captures which connector (input or output) interacted with a given bank account, discriminated by direction, following the same `document_workspace_connectors` pattern. Tenant scope is inherited transitively through `account.workspace` and `workspaceConnector.workspace` — no direct `workspace_pk` column is stored on the junction itself. Rows are written exclusively by the connector sync pipeline and are never modified by users. | Naming | Value | | ------------------------------- | ---------------------------------- | | Object | AccountWorkspaceConnector | | Resource type (JSON:API `type`) | `account_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/account-workspace-connectors` | | Entity class | `AccountWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------------------- | ---------- | | List | `GET /v1/account-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/account-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/account-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/account-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/account-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------------------------- | -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | direction | 🔒 system — enum (direction\_enum) | ✅ Yes | Native Postgres enum `core_api.direction_enum`; stored values are the enum VALUE strings | "input" (source / data collection), "output" (router / data distribution) | Indicates whether this workspace connector is acting as a data source (input) or a data sink/router (output) for the associated account. | | created\_at | 🔒 system — datetime | ✅ Yes | Set on INSERT via `onCreate` hook; Postgres default `now()`. Never null. | — | Timestamp when this provenance row was created by the connector sync pipeline. | | updated\_at | 🔒 system — datetime | ⚪ No | Set on INSERT and again on every UPDATE via `onCreate` / `onUpdate` hooks. Nullable. | — | Timestamp of the last update to this row. Null until the first update occurs after creation. | | deleted\_at | 🔒 system — datetime | ⚪ No | Nullable; set to a timestamp on soft-delete, null when active. | — | Soft-delete timestamp. When non-null the row is logically deleted. All queries must filter `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | account | to-one (ManyToOne) | ✅ Yes | The bank account this provenance row is attached to. FK `account_pk` references `core_api.accounts.pk` ON UPDATE CASCADE. Tenant isolation for this junction is derived from the account's workspace. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The workspace-scoped connector instance that interacted with the account. FK `workspace_connector_pk` references `core_api.workspace_connectors.pk` ON UPDATE CASCADE. Combined with `direction` this identifies the specific sync source or output sink. | ### System-computed * pk — auto-increment serial primary key, internal only; never exposed on the public API. * created\_at — set automatically via MikroORM `onCreate` hook (and Postgres default `now()`); not writable after insert. * updated\_at — set automatically via MikroORM `onCreate` and `onUpdate` hooks; updated on every flush. * deleted\_at — populated exclusively by soft-delete logic in the sync pipeline; no user-initiated deletion path. * direction — written at row creation time by the connector sync orchestrator based on the connector's role; never changed after insert. * No `workspace_pk` column: tenant scope is resolved by traversing the `account → workspace` or `workspaceConnector → workspace` relationship, consistent with the `document_workspace_connectors` design precedent. * No UNIQUE constraint on (account\_pk, workspace\_connector\_pk, direction) — duplicate rows are allowed by design in the current schema; deduplication is deferred to iteration 3+ per migration comments. * No `external_id` field — external object identity is deferred to a future iteration; this table records the connector pointer only. * Rows are created exclusively by the connector sync pipeline (connector sync writer). There is no resource PATCH endpoint for this entity. ## Example ```json theme={null} { "data": { "type": "account_workspace_connector", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "attributes": { "direction": "input", "created_at": "2026-05-10T08:14:22.000Z", "updated_at": "2026-05-10T08:14:22.000Z", "deleted_at": null }, "relationships": { "account": { "data": { "type": "account", "id": "a1b2c3d4-0000-0000-0000-000000000001" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "d4c3b2a1-0000-0000-0000-000000000002" } } } } } ``` Source: `apps/api/src/database/entities/AccountWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # Account Source: https://docs.wellapp.ai/object-reference/accounts Account represents a bank or financial account (deposit, credit, loan, investment, payroll, or other) held by a workspace, company, or individual Account represents a bank or financial account (deposit, credit, loan, investment, payroll, or other) held by a workspace, company, or individual. It is the primary model the connector layer (Plaid, Qonto, etc.) writes synced bank accounts into, and it is also used to record counterparty accounts extracted from invoice payment means. Key associations are to Workspace (tenant scope), Company (account holder or the workspace's own company), People (individual holder), a bank Company via bank\_company, a source WorkspaceConnector tracking provenance, and one-to-many AccountWorkspaceConnectors for multi-connector sync audit. The ownership field distinguishes workspace-held accounts from counterparty accounts from unclassified legacy rows. | Naming | Value | | ------------------------------- | -------------- | | Object | Account | | Resource type (JSON:API `type`) | `account` | | Collection / records root | `accounts` | | REST base | `/v1/accounts` | | Entity class | `Account` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ------------- | | List | `GET /v1/accounts` | ✅ Implemented | | Retrieve | `GET /v1/accounts/{id}` | ✅ Implemented | | Create | `POST /v1/accounts` | 🟡 Planned | | Update | `PATCH /v1/accounts/{id}` | 🟡 Planned | | Delete | `DELETE /v1/accounts/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | account\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen\_random\_uuid() on INSERT | — | Public immutable identifier for the account. Exposed in all API responses and external references. | | account\_external\_id | string | ⚪ No | max length 255; partial unique index on (workspace\_pk, account\_external\_id) WHERE deleted\_at IS NULL AND account\_external\_id IS NOT NULL | — | Provider-assigned identifier for the account (e.g. Plaid account\_id, Qonto account external id). Used as the deduplication key during connector sync to prevent re-creating an already-ingested account. | | type | string (enum) | ✅ Yes | non-null; maps to native pg enum account\_type\_enum | deposit, credit, loan, investment, payroll, other | Top-level account classification. deposit and credit are the most common for operational bank accounts; investment covers treasury / brokerage accounts (e.g. Arc Treasury via Pershing). | | subtype | string (enum) | ⚪ No | nullable; maps to native pg enum account\_subtype\_enum | checking account, savings account, money market account, cash management, certificate of deposit, electronic benefit transfer, health savings account, PayPal account, prepaid card (deposit); card (credit); auto, business, commercial, construction, consumer, home equity, home mortgage, line of credit, mortgage, overdraft, student (loan); 529 plan, 401a plan, 401k plan, 403b plan, 457b plan, brokerage account, cash isa, crypto exchange, education savings account, fixed annuity, guaranteed investment certificate, health reimbursement account, IRA, ISA, Keogh, lif, life insurance, LIRA, LRIF, LRSP, mutual fund, non custodial wallet, non taxable brokerage, other annuity, other insurance, pension, pension prif, profit sharing plan, QSHR, RDSP, RESP, retirement account, RLIF, ROTH, Roth 401k, RRIF, RRSP, SARSEP, sep IRA, simple IRA, SIPP, stock plan, TFSA, thrift savings plan, trust, UGMA, UTMA, variable annuity (investment); Roth IRA (payroll); other (generic) | Granular Plaid-compatible account subtype. Provides detailed classification within each account type. Not always populated for non-Plaid connectors. | | account\_name | string | ⚪ No | max length 255; user-editable in some connectors — must not be used as a unique key | — | Human-readable display name for the account, typically provided by the bank or fintech (e.g. 'Qonto EUR — Operating'). May contain the bank name as free text; use as display label only, not as an identity signal. | | iban | string | ⚪ No | max length 34; CHECK: iban IS NULL OR iban \~ '^\[A-Z]\{2}\[0-9]\{2}\[A-Z0-9]\{1,30}\$' (ISO 13616) | — | International Bank Account Number in ISO 13616 format. Fully populated by the Qonto connector; absent for US-centric connectors (Plaid/Mercury). The IBAN prefix encodes the country and CIB (FR IBANs: positions 5–9 are the bank code). | | account\_number | string | ⚪ No | max length 50 | — | Domestic account number, used where IBAN is not applicable (e.g. US accounts). Stored as-is from the provider. | | bic | string | ⚪ No | max length 11; CHECK: bic IS NULL OR bic \~ '^\[A-Z]\{4}\[A-Z]\{2}\[A-Z0-9]\{2}(\[A-Z0-9]\{3})?\$' (ISO 9362, 8 or 11 chars) | — | BIC/SWIFT code for the account's bank in ISO 9362 format. First 4 characters are the institution code. Used for bank identity resolution and cross-workspace deduplication. | | routing\_number | string | ⚪ No | max length 9; CHECK: routing\_number IS NULL OR routing\_number \~ '^\[0-9]\{9}\$' (US ABA 9-digit) | — | US ABA routing number. Maps deterministically to a US bank via the FedACH directory. Populated for some US-side accounts; absent for EU/UK accounts. | | sort\_code | string | ⚪ No | max length 6; CHECK: sort\_code IS NULL OR sort\_code \~ '^\[0-9]\{6}\$' (UK 6-digit) | — | UK bank sort code. Maps to a specific UK bank and branch. Absent for non-UK accounts. | | currency | string | ⚪ No | max length 3; CHECK: currency IS NULL OR currency \~ '^\[A-Z]\{3}\$' (ISO 4217) | — | ISO 4217 three-letter currency code for the primary currency of the account. Helps disambiguate multiple accounts at the same institution (e.g. Mercury USD vs Mercury IO). | | digital\_wallet\_provider | string (enum) | ⚪ No | nullable; maps to native pg enum digital\_wallet\_provider\_enum | paypal, apple\_pay, google\_pay, samsung\_pay, alipay, wechat\_pay | Provider of the digital wallet when the account represents a digital wallet rather than a traditional bank account. | | digital\_wallet\_id | string | ⚪ No | max length 255 | — | External identifier for the digital wallet account at the specified digital\_wallet\_provider. | | digital\_wallet\_type | string (enum) | ⚪ No | nullable; maps to native pg enum digital\_wallet\_type\_enum | personal, business, merchant | Classification of the digital wallet account by usage context. | | ownership | string (enum) | ✅ Yes | non-null; default 'unknown'; maps to native pg enum account\_ownership\_enum. Set at write time from structured signals only — never from string/regex matching. | workspace, counterparty, unknown | Classifies whether the account belongs to the workspace itself (a connector-synced bank account) or to a counterparty (extracted from an invoice or document payment means). 'unknown' is the safe default for legacy rows and rows lacking sufficient provenance signal. Backfilled to 'workspace' where source\_workspace\_connector\_pk is set or where a PaymentMeans links the account back to the workspace's own company. | | raw\_data | jsonb | ⚪ No | nullable; shape varies by connector — always branch on originating connector before parsing | — | Complete connector-native payload as received from the provider. For Plaid: contains institution\_id, institution\_name, official\_name, and counterparty metadata. Shape is connector-specific; do not assume a fixed path without checking the source connector. | | created\_at | Date, 🔒 system | ✅ Yes | set once on INSERT via onCreate lifecycle hook | — | Timestamp of record creation. Set automatically by MikroORM; never supplied by callers. | | updated\_at | Date, 🔒 system | ⚪ No | set on INSERT and refreshed on every UPDATE via onCreate/onUpdate lifecycle hooks | — | Timestamp of last update. Refreshed automatically by MikroORM on every write. | | deleted\_at | Date | ⚪ No | nullable; soft-delete sentinel. All queries must filter deleted\_at IS NULL. The partial index idx\_accounts\_workspace\_external\_id\_active uses WHERE deleted\_at IS NULL. | — | Soft-delete timestamp. When set, the account is logically deleted and excluded from all standard queries. Hard deletes are not performed on this entity. | ### Relationships | Name | Type | Required | Description | | ------------------------------ | ----------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (Workspace) | ⚪ No (nullable FK) | Tenant scope. Points to the Workspace that owns this account. Every standard query must filter through this relation. Indexed via idx\_accounts\_workspace\_deleted (workspace, deleted\_at). | | company | to-one (Company) | ⚪ No (nullable FK, fieldName: company\_pk) | The company that holds this account. For workspace-owned accounts, this is typically the workspace's own company. Mutually exclusive with the people relation in practice — each account belongs to either a Company or a People. Indexed via idx\_accounts\_company. | | people | to-one (People) | ⚪ No (nullable FK, fieldName: people\_pk) | The individual person who holds this account. Used when the account belongs to a natural person rather than a legal entity. Mutually exclusive with the company relation in practice. Indexed via idx\_accounts\_people. | | bank\_company | to-one (Company) | ⚪ No (nullable FK, fieldName: bank\_company\_pk) | The bank institution (represented as a Company) that operates this account. Used for logo display and bank identity resolution. When NULL, the bank can sometimes be recovered from raw\_data or inferred from IBAN/BIC. Indexed via idx\_accounts\_bank\_company. | | source\_workspace\_connector | to-one (WorkspaceConnector) | ⚪ No (nullable FK) | The WorkspaceConnector instance that first created this account row. When non-NULL, indicates connector-confirmed provenance and is a primary signal for setting ownership = 'workspace'. When NULL, the account was created by a fallback path (manual, invoice parse, payment means extraction). | | workspace\_connector | to-one (WorkspaceConnector) | ⚪ No (nullable FK, fieldName: workspace\_connector\_pk) | The WorkspaceConnector currently associated with this account for ongoing sync operations. Distinct from source\_workspace\_connector — a different connector may currently manage sync even if it did not create the row. | | account\_workspace\_connectors | to-many (AccountWorkspaceConnector) | — | Collection of AccountWorkspaceConnector pivot rows recording per-connector sync provenance for this account, including the sync direction (inbound/outbound). Used for multi-connector audit trails. | ### System-computed * account\_id: generated via gen\_random\_uuid() on INSERT; unique constraint enforced at the database level. * created\_at: set once on INSERT via MikroORM onCreate lifecycle hook; never supplied by callers. * updated\_at: set on INSERT and refreshed on every UPDATE via MikroORM onCreate/onUpdate lifecycle hooks. * deleted\_at: soft-delete sentinel. When set, the account is excluded from all standard queries. The partial index idx\_accounts\_workspace\_external\_id\_active uses WHERE deleted\_at IS NULL AND account\_external\_id IS NOT NULL for the Plaid-sync hot-path dedup lookup. * ownership default: defaults to 'unknown' at the database level for all new rows. Backfilled to 'workspace' by Migration20260527120000\_account\_ownership where source\_workspace\_connector\_pk IS NOT NULL, or where a PaymentMeans row links the account to the workspace's own company (payment\_means.account\_pk = accounts.pk AND payment\_means.company\_pk = workspace.own\_company\_pk). * account\_external\_id dedup: the partial unique index idx\_accounts\_workspace\_external\_id\_active on (workspace\_pk, account\_external\_id) WHERE deleted\_at IS NULL AND account\_external\_id IS NOT NULL is the connector-layer dedup key. Plaid sync uses this to find an existing account before creating a new one, preventing re-linking on reconnect. * source\_workspace\_connector provenance: when source\_workspace\_connector\_pk IS NOT NULL, the account was created by the connector pipeline and ownership should be 'workspace'. A NULL source\_workspace\_connector\_pk means the account was created via a fallback path (manual entry, invoice payment means extraction) and ownership defaults to 'unknown' pending classification. * bank\_company recovery: when bank\_company\_pk IS NULL, the bank identity may be recoverable from raw\_data (look for raw\_data->>'institution\_name' or raw\_data->'institution'->>'name') or inferred from the IBAN bank code prefix (FR IBANs: positions 5–9 = CIB) or BIC first 4 characters. * Composite composites.yml definitions: two composites are defined for the accounts root — composite\_account\_summary (source\_fields: account\_id, bank\_company.primary\_media.url, account\_name, currency, iban, account\_number; display\_type: account\_summary) and composite\_latest\_balance\_currency (source\_fields: account\_id, account\_balances.accounting\_balance; display\_type: currency\_amount). ## Example ```json theme={null} { "data": { "type": "account", "id": "a3f7c2e1-84b0-4d5e-9f12-6c8a1b0e3d7f", "attributes": { "account_external_id": "qonto_acc_EU84QNTO00001234567890", "type": "deposit", "subtype": "checking account", "account_name": "Qonto EUR — Operating", "iban": "FR7616958000010000012345678", "account_number": null, "bic": "QNTOFRP1XXX", "routing_number": null, "sort_code": null, "currency": "EUR", "digital_wallet_provider": null, "digital_wallet_id": null, "digital_wallet_type": null, "ownership": "workspace", "raw_data": { "institution_id": "ins_qonto", "institution_name": "Qonto", "official_name": "Qonto Business Current Account" }, "created_at": "2025-03-14T09:22:11.000Z", "updated_at": "2026-02-01T14:05:33.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "b1e9d4f2-3301-4a8c-b7e0-9f4c2d1a5e88" } }, "company": { "data": { "type": "company", "id": "c2a8f5e3-7712-4b9d-a1f2-8e3d0c6b4a21" } }, "people": { "data": null }, "bank_company": { "data": { "type": "company", "id": "d4b7e8f1-2290-4c3a-b5e7-1f9d0a2c8b64" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "e5c9d1a2-4403-4e7b-c2f3-2a4b5d0e9c77" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "e5c9d1a2-4403-4e7b-c2f3-2a4b5d0e9c77" } }, "account_workspace_connectors": { "data": [ { "type": "account_workspace_connector", "id": "f6d0e2b3-5514-4f8c-d3g4-3b5c6e1f0d88" } ] } } } } ``` Source: `apps/api/src/database/entities/Account.ts` · domain: financial-graph · tier: Main # ApiKey Source: https://docs.wellapp.ai/object-reference/api_keys ApiKey represents a long-lived programmatic credential scoped to a workspace (and optionally to a specific person/member) that authenticates API requests via th ApiKey represents a long-lived programmatic credential scoped to a workspace (and optionally to a specific person/member) that authenticates API requests via the `ApiKeyStrategy` in the auth chain. It is created by workspace members through the `POST /v1/api-keys` endpoint and revoked via `DELETE /v1/api-keys/:id`; no PATCH route exists. Each key is linked to one or more workspace connectors via a separate `api_key_workspace_connector_links` join table, and those connectors are cleaned up automatically on revocation. The `value` field is the raw bearer secret — it is only surfaced once at creation; all subsequent list responses return a `masked_key` derived from it. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ApiKey | | Resource type (JSON:API `type`) | `api_key` | | Collection / records root | — (not a records root) | | REST base | `/v1/api-key` | | Entity class | `ApiKey` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------- | ---------- | | List | `GET /v1/api-key` | 🟡 Planned | | Retrieve | `GET /v1/api-key/{id}` | 🟡 Planned | | Create | `POST /v1/api-key` | 🟡 Planned | | Update | `PATCH /v1/api-key/{id}` | 🟡 Planned | | Delete | `DELETE /v1/api-key/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ------------------------- | -------- | --------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | api\_key\_id | string (UUID) — 🔒 system | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public stable identifier for the API key. Used as the JSON:API `id` and in all API routes (`:id` param). Generated by the database on INSERT. | | value | string — 🔒 system | ✅ Yes | unique; varchar(255) | — | The raw bearer secret. Generated server-side by ApiKeyService at creation; never surfaced again after the creation response. List and GET responses return `masked_key` instead. | | name | string | ✅ Yes | varchar(255); not null | — | Human-readable label for the key, supplied by the caller at creation (e.g. 'CI Pipeline Key'). | | created\_at | datetime — 🔒 system | ✅ Yes | timestamptz; not null; set on INSERT via onCreate hook | — | Timestamp of key creation, set automatically by the MikroORM `onCreate` lifecycle hook. | | updated\_at | datetime — 🔒 system | ⚪ No | timestamptz; nullable; set on INSERT and UPDATE via onCreate/onUpdate hooks | — | Timestamp of last modification, managed by MikroORM lifecycle hooks. | | last\_used\_at | datetime — 🔒 system | ⚪ No | timestamptz; nullable | — | Timestamp of the last successful authentication using this key, stamped by ApiKeyStrategy at each successful auth pass. | | expiration\_date | datetime | ⚪ No | timestamptz; nullable | — | Optional expiry date-time after which the key should be considered invalid. Supplied as `expiration_at` in the creation payload. No server-side expiry enforcement beyond this stored value. | | is\_active | boolean | ✅ Yes | not null; default true | true, false | Soft-disable flag. Set to false by ApiKeyService.revokeApiKey() on DELETE. Keys with is\_active=false are rejected by ApiKeyStrategy. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | No (nullable: true) | The workspace this API key is scoped to. Set at creation; required at the business-logic level (controller throws 400 if absent). Target: Workspace. | | people | to-one (ManyToOne) | No (nullable: true) | The person (workspace member) who created the key, resolved from the Firebase membership at creation time. Null for system-created keys. Target: People. | ### System-computed * api\_key\_id — generated via gen\_random\_uuid() defaultRaw on INSERT * created\_at — set by MikroORM onCreate hook, never writable * updated\_at — set by MikroORM onCreate + onUpdate hooks, never writable * value — generated server-side by ApiKeyService (random secure secret); returned once at creation, never again * last\_used\_at — stamped by ApiKeyStrategy on each successful auth pass, not user-writable * is\_active — defaults to true on INSERT; set to false by ApiKeyService.revokeApiKey() on DELETE, not directly patchable ## Example ```json theme={null} { "data": { "type": "api_key", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "name": "CI Pipeline Key", "is_active": true, "created_at": "2026-01-15T09:00:00.000Z", "last_used_at": "2026-05-28T14:32:11.000Z", "expiration_at": "2027-01-15T09:00:00.000Z", "masked_key": "sk_live_ab...ef12" } } } ``` Source: `apps/api/src/database/entities/ApiKey.ts` · domain: platform · tier: Platform # BlueprintPersistenceError Source: https://docs.wellapp.ai/object-reference/blueprint_persistence_errors BlueprintPersistenceError is a structured diagnostic sink for failures that occur during blueprint run persistence — replacing silent `logger.error` catches in BlueprintPersistenceError is a structured diagnostic sink for failures that occur during blueprint run persistence — replacing silent `logger.error` catches in `BlueprintStorageService.persistStep` and the `runInBackground` helper. Each row records the phase of the blueprint pipeline that failed, the associated run ID, the workspace context (nullable for pre-auth failures), and the full error detail including message, type, stack, and an arbitrary JSON payload. Rows are pruned opportunistically after 30 days (\~1-in-20 writes trigger cleanup), mirroring the `connector_sync_diagnostics` retention pattern. The entity is append-only and owned entirely by the system — no user-facing PATCH route exists. | Naming | Value | | ------------------------------- | ---------------------------------- | | Object | BlueprintPersistenceError | | Resource type (JSON:API `type`) | `blueprint_persistence_error` | | Collection / records root | — (not a records root) | | REST base | `/v1/blueprint-persistence-errors` | | Entity class | `BlueprintPersistenceError` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------------------- | ---------- | | List | `GET /v1/blueprint-persistence-errors` | 🟡 Planned | | Retrieve | `GET /v1/blueprint-persistence-errors/{id}` | 🟡 Planned | | Create | `POST /v1/blueprint-persistence-errors` | 🟡 Planned | | Update | `PATCH /v1/blueprint-persistence-errors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/blueprint-persistence-errors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ------------------------------------------------- | -------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | error\_id | 🔒 system — UUID | ✅ Yes | unique, NOT NULL, default gen\_random\_uuid() | — | Public-facing unique identifier for this error row. Generated by the database via gen\_random\_uuid() at insert time. Used as the JSON:API `id`. | | blueprint\_run\_id | string (text) | ✅ Yes | NOT NULL | — | Identifies the blueprint run that triggered this failure. Stored as free-form text matching the run's external identifier. Indexed for efficient per-run lookup. | | phase | enum (blueprint\_persistence\_error\_phase\_enum) | ✅ Yes | NOT NULL; native Postgres enum stored as enum value string | upsert\_run \| increment\_step \| create\_step \| upload\_screenshot \| update\_run\_terminal \| persist\_step \| background\_runner \| handler | The pipeline phase in which the error occurred. Backed by a native Postgres enum. Indexed alongside created\_at for time-bucketed phase analysis. | | error\_type | string (text) | ⚪ No | nullable | — | Optional classification of the error class or exception type (e.g. the JavaScript error constructor name). Null when not captured. | | error\_message | string (text) | ✅ Yes | NOT NULL | — | The full human-readable error message from the caught exception. | | stack | string (text) | ⚪ No | nullable | — | Full exception stack trace at the point of capture. Null when unavailable (e.g. non-Error throws). | | payload | jsonb | ⚪ No | nullable, JSONB | — | Arbitrary structured context captured at the failure site — may include step index, run ID, workspace ID, or any other diagnostic fields relevant to the phase. Shape varies per phase. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL, default now() | — | Timestamp set by the MikroORM onCreate hook (and defaulted to now() at the database level) when the error row is inserted. No updated\_at — this entity is append-only. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | nullable | — | Soft-delete timestamp. Set by the application retention logic (BlueprintPersistenceErrorService.write prunes rows older than 30 days on \~1/20 writes). Null means the row is active. Rows with deleted\_at set are filtered from normal queries. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ⚪ No | The workspace this error belongs to. Nullable because some blueprint failure modes are caught before auth resolves a workspace (e.g. handler-level catch on a malformed request). Rows where workspace is null are intentionally invisible to the tenant `user` role in Hasura — only accessible via admin / Metabase / direct DB queries. References core\_api.workspaces. | ### System-computed * error\_id: generated by gen\_random\_uuid() at the database level on insert; never supplied by callers * created\_at: set by MikroORM onCreate hook (also defaulted to now() in the DDL); no updated\_at column exists — the entity is append-only * deleted\_at: managed by application-level retention logic inside BlueprintPersistenceErrorService.write(), which prunes rows older than 30 days on approximately 1-in-20 writes; no scheduled job is required * Retention sampling: the \~1/20 probabilistic pruning is designed to avoid a dedicated cron; the window may allow rows to survive slightly beyond 30 days * Workspace nullability: workspace\_pk is nullable by design to capture pre-auth failures; Hasura user-role select\_permissions filter by workspace.workspace\_id, making null-workspace rows admin-only ## Example ```json theme={null} { "data": { "type": "blueprint_persistence_error", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "error_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "blueprint_run_id": "run_01HXYZ9876ABCDEF", "phase": "persist_step", "error_type": "EntityNotFoundError", "error_message": "Could not find BlueprintRun with id run_01HXYZ9876ABCDEF", "stack": "Error: Could not find BlueprintRun with id run_01HXYZ9876ABCDEF\n at BlueprintStorageService.persistStep (/app/src/services/blueprint-storage.service.ts:142:13)", "payload": { "step_index": 3, "run_id": "run_01HXYZ9876ABCDEF", "workspace_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, "created_at": "2026-04-29T14:23:11.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } } } } } ``` Source: `apps/api/src/database/entities/BlueprintPersistenceError.ts` · domain: automation · tier: Activity # Blueprint Run Source: https://docs.wellapp.ai/object-reference/blueprint_runs A `blueprint_run` records a single execution of the Blueprint Analyzer — Well's multi-phase web-navigation agent (Claude Opus) that autonomously browses a targe A `blueprint_run` records a single execution of the Blueprint Analyzer — Well's multi-phase web-navigation agent (Claude Opus) that autonomously browses a target URL to accomplish a natural-language goal. Each run belongs to exactly one workspace and progresses through a lifecycle (`queued → running → persisting → completed | failed | abandoned`). The run is the audit spine for the analysis session: it captures the high-level intent (`goal`, `target_url`), the agent's final synthesized output (`result_blueprint`), and — on failure — structured error diagnostics (`error_type`, `error_message`, `failed_at_step`, `error_context`). Individual navigation actions are stored as child `BlueprintStep` records linked by a one-to-many relationship. | Naming | Value | | ------------------------------- | -------------------- | | Object | Blueprint Run | | Resource type (JSON:API `type`) | `blueprint_run` | | Collection / records root | `blueprint_runs` | | REST base | `/v1/blueprint-runs` | | Entity class | `BlueprintRun` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ------------- | | List | `GET /v1/blueprint-runs` | ✅ Implemented | | Retrieve | `GET /v1/blueprint-runs/{id}` | ✅ Implemented | | Create | `POST /v1/blueprint-runs` | 🟡 Planned | | Update | `PATCH /v1/blueprint-runs/{id}` | 🟡 Planned | | Delete | `DELETE /v1/blueprint-runs/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ----------------------------- | -------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | blueprint\_run\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen\_random\_uuid() on insert | — | Public immutable identifier for the run. Used in all API surfaces; the internal `pk` is never exposed. | | goal | string | ✅ Yes | text; no max length | — | Natural-language objective passed to the Blueprint Analyzer agent at run creation. Describes what the agent should accomplish on the target website. | | target\_url | string | ✅ Yes | text; no max length; must be a valid URL reachable by the agent | — | The URL the agent navigates as the starting point of the run. | | firebase\_uid | string | ⚪ No | text; nullable | — | Firebase UID of the authenticated user who initiated the run. Written at run creation from the Firebase auth context. Used to associate the run back to a specific user session within the workspace. | | status | enum (BlueprintRunStatusEnum) | ✅ Yes | default 'running'; not null; native Postgres enum `blueprint_run_status_enum` | queued, running, persisting, completed, failed, abandoned | Lifecycle state of the run. Transitions: queued (created but not yet started) → running (agent executing steps) → persisting (BlueprintStorageService writing results) → completed (success) \| failed (agent error) \| abandoned (timed out or manually cancelled). The `queued` and `persisting` states were added in Migration20260429110000. | | step\_count | integer | ✅ Yes | default 0; not null; incremented atomically by BlueprintStorageService.persistStep | — | Running count of steps persisted for this run. Incremented atomically (not derived from the steps collection count) so it reflects the true number of completed steps even if child rows are soft-deleted. | | result\_blueprint | jsonb | ⚪ No | nullable; free-form JSON | — | The structured output produced by the agent upon successful completion. Written by BlueprintStorageService from the AI's final `aiResponse.blueprint` payload. Shape is agent-defined and varies by goal type (e.g. pricing tables, API schemas, form fields). Null until the run reaches `completed` status. | | error\_type | string | ⚪ No | text; nullable | — | Machine-readable error category written when the run transitions to `failed`. Examples: navigation\_error, timeout, parse\_error. Used to group failures in monitoring dashboards. | | error\_message | string | ⚪ No | text; nullable | — | Human-readable error description accompanying `error_type` on failure. Written by BlueprintStorageService when the agent reports a terminal error. | | failed\_at\_step | integer | ⚪ No | nullable; references the 1-based step\_number of the failing BlueprintStep | — | The step\_number at which the run failed. Written alongside `error_type` and `error_message` by BlueprintStorageService. Null when the run completed successfully or was abandoned before any step executed. | | error\_context | jsonb | ⚪ No | nullable; free-form JSON | — | Full diagnostic payload for the failure event. Written by BlueprintStorageService as `{ aiResponse: <raw agent response> }` at the point of failure. Provides the raw AI output that caused or accompanied the error, for debugging and replay. | | created\_at | timestamptz, 🔒 system | ✅ Yes | set by MikroORM onCreate lifecycle hook; not null | — | Timestamp when the run record was created. Indexed DESC for recency queries (partial index on deleted\_at IS NULL). | | updated\_at | timestamptz, 🔒 system | ⚪ No | set by MikroORM onCreate + onUpdate lifecycle hooks; nullable | — | Timestamp of the most recent update to any field on the run. Refreshed automatically on every flush that modifies the record. | | completed\_at | timestamptz | ⚪ No | nullable; written by BlueprintStorageService when status transitions to completed or failed | — | Timestamp when the run reached its terminal state (completed or failed). Combined with `created_at`, this gives the wall-clock duration of the agent execution. Null while the run is in-flight (queued/running/persisting) or abandoned. | | deleted\_at | timestamptz | ⚪ No | nullable; soft-delete sentinel | — | Soft-delete timestamp. When non-null the run is considered deleted and excluded from all standard queries and both partial indexes (which filter WHERE deleted\_at IS NULL). Cascade delete from the workspace also hard-deletes via the FK constraint. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | The workspace that owns this blueprint run. FK: `workspace_pk → workspaces.pk`. Cascade: ON DELETE CASCADE — deleting a workspace hard-deletes all its runs. Used to scope the run to a tenant and enforce multi-tenant isolation in Hasura RLS. | | steps | to-many (blueprint\_step) | — | Ordered collection of `BlueprintStep` records representing the individual navigation actions the agent took during this run. Related via `BlueprintStep.blueprint_run_pk → BlueprintRun.pk`. Cascade: ON DELETE CASCADE — deleting a run hard-deletes all its steps. The composite `composite_steps_list` in composites.yml surfaces step\_number, action\_type, and action\_success in the records table view. | ### System-computed * `blueprint_run_id` is generated by Postgres `gen_random_uuid()` on insert; unique constraint enforced at the DB level. * `created_at` is set by the MikroORM `onCreate` lifecycle hook on first flush; never updated. * `updated_at` is set by both `onCreate` and `onUpdate` lifecycle hooks; refreshed on every write to the record. * `status` defaults to `running` at the DB level; the controller may set it to `queued` when enqueuing before agent dispatch. `BlueprintStorageService` drives transitions: running → persisting at start of persist, then persisting → completed on success or persisting → failed on error. * `step_count` is incremented atomically by `BlueprintStorageService.persistStep` (not derived from the child collection) so it is consistent even when child rows are soft-deleted. * `completed_at` is written by `BlueprintStorageService` when the run reaches `completed` or `failed` status. It is never set for `abandoned` runs. * `result_blueprint` is written from `aiResponse.blueprint` by `BlueprintStorageService` only on the final successful step (terminal `completed` transition). * `failed_at_step` and `error_context` are written together by `BlueprintStorageService` when the agent reports a terminal error; `error_context` captures the raw `aiResponse` payload for replay. * Both partial indexes (`idx_blueprint_runs_workspace_status` on `(workspace_pk, status)` and `idx_blueprint_runs_created_at_desc` on `(created_at DESC)`) filter `WHERE deleted_at IS NULL`, so they apply only to active rows. * Cascade deletes flow from workspace → blueprint\_runs → blueprint\_steps via FK ON DELETE CASCADE chains. ## Example ```json theme={null} { "data": { "type": "blueprint_run", "id": "c3f7a2b1-8e4d-4f91-b035-2a6c1d9e0f84", "attributes": { "goal": "Extract the current pricing plans and feature comparison table from the Stripe pricing page", "target_url": "https://stripe.com/pricing", "firebase_uid": "XkJ7Qm2pL9nRvBwdTzAs8oY3eH5fCi", "status": "completed", "step_count": 7, "result_blueprint": { "pricing_plans": [ { "name": "Starter", "price_usd": 0, "features": ["Payments", "Dashboard"] }, { "name": "Growth", "price_usd": 49, "features": ["Payments", "Dashboard", "Radar", "Billing"] } ], "summary": "Three tiers identified: Starter (free), Growth ($49/mo), Enterprise (custom)." }, "error_type": null, "error_message": null, "failed_at_step": null, "error_context": null, "created_at": "2026-05-14T09:12:03.000Z", "updated_at": "2026-05-14T09:14:47.000Z", "completed_at": "2026-05-14T09:14:47.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "b91e4d02-7c3a-41f6-9d28-5f0a3bc87e14" } }, "steps": { "data": [ { "type": "blueprint_step", "id": "a1b2c3d4-0001-4e5f-8abc-def012345678" }, { "type": "blueprint_step", "id": "a1b2c3d4-0002-4e5f-8abc-def012345679" } ] } } } } ``` Source: `apps/api/src/database/entities/BlueprintRun.ts` · domain: automation · tier: Activity # BlueprintStep Source: https://docs.wellapp.ai/object-reference/blueprint_steps BlueprintStep records a single atomic action taken by the Blueprint Analyzer AI agent during a web-navigation run BlueprintStep records a single atomic action taken by the Blueprint Analyzer AI agent during a web-navigation run. Each step belongs to exactly one BlueprintRun (cascade-deleted with it) and is sequenced by a monotonically increasing `step_number`. Steps capture the agent's reasoning, the page URL at the time of execution, the action it chose (e.g. click, type, scroll), structured action parameters, the raw LLM response, screenshot storage references, and an outcome flag — forming the complete execution trace for a run. The table is written exclusively by the `blueprint-analyzer.service.ts` pipeline; no user-facing PATCH endpoint exists. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | BlueprintStep | | Resource type (JSON:API `type`) | `blueprint_step` | | Collection / records root | — (not a records root) | | REST base | `/v1/blueprint-steps` | | Entity class | `BlueprintStep` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------- | ---------- | | List | `GET /v1/blueprint-steps` | 🟡 Planned | | Retrieve | `GET /v1/blueprint-steps/{id}` | 🟡 Planned | | Create | `POST /v1/blueprint-steps` | 🟡 Planned | | Update | `PATCH /v1/blueprint-steps/{id}` | 🟡 Planned | | Delete | `DELETE /v1/blueprint-steps/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | blueprint\_step\_id | string (UUID) 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() at INSERT | Any valid UUID v4 | Public-facing stable identifier for the step. Exposed as the JSON:API `id`. Never reused. | | step\_number | integer | ✅ Yes | composite UNIQUE (blueprint\_run\_pk, step\_number); composite partial index idx\_blueprint\_steps\_run\_step on (blueprint\_run\_pk, step\_number) WHERE deleted\_at IS NULL | Positive integer; 1-based sequence assigned by the analyzer service | 1-based ordinal position of this step within its parent run. Uniqueness is enforced at the DB level per run (soft-delete-aware index). | | action\_type | string (text) | ✅ Yes | NOT NULL | Free-form string set by the analyzer (e.g. 'click', 'type', 'scroll', 'navigate', 'extract', 'wait') | Identifies the category of browser action the agent decided to execute at this step. | | reasoning | string (text) | ⚪ No | nullable | — | Free-text chain-of-thought produced by the LLM before committing to this action. Populated when the model includes a reasoning block in its response. | | page\_url | string (text) | ✅ Yes | NOT NULL | Full URL of the page at the time the step was executed | Captures the browser's current URL when the step fired, enabling replay and debugging. | | screenshot\_path | string (text) | ⚪ No | nullable | GCS object path relative to the bucket, or null if no screenshot was captured | Path of the screenshot image stored in GCS at the time of this step. Paired with screenshot\_bucket to form the full storage reference. | | screenshot\_bucket | string (text) | ⚪ No | nullable | GCS bucket name, or null | GCS bucket that holds the screenshot referenced by screenshot\_path. Null when no screenshot was taken for this step. | | action\_details | jsonb | ⚪ No | nullable | Arbitrary JSON object; shape depends on action\_type (e.g. selector, coordinates, input text, scroll delta) | Structured parameters needed to replay or audit the exact action that was executed (e.g. CSS selector, click coordinates, text typed). | | ai\_response | jsonb | ⚪ No | nullable | Arbitrary JSON; typically the raw Anthropic API response object | Full LLM response payload recorded for observability — includes model ID, stop\_reason, token usage, and tool calls. Enables cost analysis and debugging. | | dom\_summary\_length | integer | ⚪ No | nullable | Non-negative integer representing character count of the DOM summary sent to the LLM | Character length of the DOM summary fed to the model for this step. Useful for prompt-size analytics and detecting pages with unusually large DOMs. | | action\_success | boolean | ⚪ No | nullable | true \| false \| null | Whether the browser action completed without error. Null means the outcome was not recorded (e.g. the step was interrupted). false indicates the browser threw an exception, detailed in action\_error. | | action\_error | string (text) | ⚪ No | nullable | Error message string, or null | Human-readable error message when action\_success is false. Captures Playwright/browser errors (selector not found, navigation timeout, etc.). | | created\_at | timestamptz 🔒 system | ✅ Yes | NOT NULL; set by onCreate hook to new Date() | — | Timestamp when the row was inserted. Set automatically at creation; never updated. | | updated\_at | timestamptz 🔒 system | ⚪ No | nullable; set by onCreate and onUpdate hooks | — | Timestamp of the last mutation. Set automatically at creation and on every update by MikroORM lifecycle hooks. | | deleted\_at | timestamptz 🔒 system | ⚪ No | nullable | null (active) \| ISO 8601 timestamp (soft-deleted) | Soft-delete sentinel. Non-null means the row is logically deleted. The partial index idx\_blueprint\_steps\_run\_step covers only rows WHERE deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | -------------- | ------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | blueprint\_run | to-one (ManyToOne) | Yes — NOT NULL FK with ON DELETE CASCADE | The parent BlueprintRun that owns this step. Deleting the run cascades and hard-deletes all its steps. This is the only relationship declared on BlueprintStep itself; the inverse collection (steps) is declared on BlueprintRun. | ### System-computed * blueprint\_step\_id — generated by gen\_random\_uuid() at INSERT, never caller-supplied * created\_at — set to new Date() by MikroORM onCreate hook * updated\_at — set to new Date() by both onCreate and onUpdate hooks * deleted\_at — soft-delete sentinel; written by the service layer when soft-deleting a step or its parent run * step\_number — assigned by blueprint-analyzer.service.ts before INSERT; not derived from a DB sequence but from the service's step counter * Cascade delete — when the parent BlueprintRun is deleted (hard or via cascade), all BlueprintStep rows are hard-deleted by the DB foreign-key constraint (ON DELETE CASCADE) ## Example ```json theme={null} { "data": { "id": "7e4c1a2b-d3f0-4e8a-b5c6-9d0e1f2a3b4c", "type": "blueprint_step", "attributes": { "blueprint_step_id": "7e4c1a2b-d3f0-4e8a-b5c6-9d0e1f2a3b4c", "step_number": 3, "action_type": "click", "reasoning": "The login button is visible and I need to authenticate before accessing the invoices page.", "page_url": "https://app.example.com/login", "screenshot_path": "blueprint-runs/run_abc123/step_003.png", "screenshot_bucket": "well-blueprints-prod", "action_details": { "selector": "#login-submit-btn", "coordinates": { "x": 640, "y": 420 } }, "ai_response": { "model": "claude-opus-4-6", "stop_reason": "tool_use", "usage": { "input_tokens": 4120, "output_tokens": 87 } }, "dom_summary_length": 8432, "action_success": true, "action_error": null, "created_at": "2026-02-24T14:23:01.000Z", "updated_at": "2026-02-24T14:23:02.000Z", "deleted_at": null }, "relationships": { "blueprint_run": { "data": { "type": "blueprint_run", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } } } ``` Source: `apps/api/src/database/entities/BlueprintStep.ts` · domain: automation · tier: Activity # BrowsingSignal Source: https://docs.wellapp.ai/object-reference/browsing_signals BrowsingSignal is an append-only infrastructure entity that records hourly deltas of provider-domain visits flushed by the Chrome extension BrowsingSignal is an append-only infrastructure entity that records hourly deltas of provider-domain visits flushed by the Chrome extension. Each row represents one (workspace, domain, hour-bucket) tuple: the count of visits accumulated since the last successful flush from that extension session. The provider-scoring pipeline aggregates rows within a 24-hour window to produce the `signal_boost` factor that ranks providers higher for workspaces that recently visited their domain. Rows have no soft-delete column and are pruned by the daily rescore cron after 90 days. The table is not exposed via Hasura and is internal-only. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | BrowsingSignal | | Resource type (JSON:API `type`) | `browsing_signal` | | Collection / records root | — (not a records root) | | REST base | `/v1/browsing-signals` | | Entity class | `BrowsingSignal` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------- | ---------- | | List | `GET /v1/browsing-signals` | 🟡 Planned | | Retrieve | `GET /v1/browsing-signals/{id}` | 🟡 Planned | | Create | `POST /v1/browsing-signals` | 🟡 Planned | | Update | `PATCH /v1/browsing-signals/{id}` | 🟡 Planned | | Delete | `DELETE /v1/browsing-signals/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------- | ------------------------- | ---------------- | ----------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | browsing\_signal\_id | 🔒 system — UUID (string) | ✅ Yes | UNIQUE; generated via gen\_random\_uuid() at INSERT | Any valid UUID v4 | Public-facing stable identifier for this browsing signal row. Generated by the database on insert; never supplied by callers. | | domain | string (text) | ✅ Yes | NOT NULL; no max-length constraint at the DB level | — | The provider domain that was visited (e.g. pennylane.com, stripe.com). Written by the Chrome extension flush handler; identifies which provider the workspace was browsing. | | visit\_count | integer | ✅ Yes | NOT NULL; column type int | — | Delta of visits to this domain recorded since the last successful flush from the Chrome extension. Used by the provider-scoring pipeline as the raw signal weight before aggregation. | | last\_visited\_at | timestamptz | ✅ Yes | NOT NULL | — | Timestamp of the most recent visit to this domain within the flush batch. Used to bound recency windows in the 24-hour aggregation pass. | | created\_at | 🔒 system — timestamptz | ⚪ No (defaulted) | DEFAULT now(); set by DB on insert; also set via MikroORM onCreate hook | — | Wall-clock timestamp of row insertion. Used as the composite index key for workspace-scoped time-range queries and as the pruning anchor for the 90-day cron cleanup. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace whose Chrome extension produced this signal. Foreign key stored as workspace\_pk with ON DELETE CASCADE — deleting a workspace removes all its browsing signals. This is the tenant scope anchor for every query on the table. | ### System-computed * browsing\_signal\_id: generated via gen\_random\_uuid() at INSERT — never supplied by the caller * created\_at: set to now() by the database DEFAULT and additionally via MikroORM onCreate hook — not user-supplied * No soft-delete (deleted\_at column is absent): this is an append-only table; rows are pruned physically by the daily rescore cron after 90 days, not soft-deleted * No updated\_at column: the entity is append-only by design; rows are never mutated after insertion * pk: internal serial primary key — exposed only in the composite index definitions; never surfaced in the API response * workspace\_pk: FK column written by MikroORM from the workspace relationship — never set directly by the caller * Two composite indexes maintained automatically by Postgres: idx\_browsing\_signals\_workspace\_created (workspace\_pk, created\_at DESC) and idx\_browsing\_signals\_workspace\_last\_visited (workspace\_pk, last\_visited\_at DESC) ## Example ```json theme={null} { "data": { "type": "browsing_signal", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "browsing_signal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "domain": "pennylane.com", "visit_count": 4, "last_visited_at": "2026-06-02T08:45:00.000Z", "created_at": "2026-06-02T09:00:00.000Z" }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws_9f3a8b2c-1234-5678-abcd-000000000001" } } } } } ``` Source: `apps/api/src/database/entities/BrowsingSignal.ts` · domain: ingestion · tier: Infrastructure # CanvasView Source: https://docs.wellapp.ai/object-reference/canvas_views CanvasView stores a user-customised investor-report canvas layout for a workspace CanvasView stores a user-customised investor-report canvas layout for a workspace. One active row exists per (workspace, template\_id) pair, enforced by a partial unique index on deleted\_at IS NULL. The entity is REST-only and is not tracked in Hasura; the financial-overview page fetches one row by template via PUT /v1/workspaces/:id/canvas-views/:template\_id (upsert). It belongs to a Workspace and optionally records the People who created it. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CanvasView | | Resource type (JSON:API `type`) | `canvas_view` | | Collection / records root | — (not a records root) | | REST base | `/v1/canvas-views` | | Entity class | `CanvasView` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------ | ---------- | | List | `GET /v1/canvas-views` | 🟡 Planned | | Retrieve | `GET /v1/canvas-views/{id}` | 🟡 Planned | | Create | `POST /v1/canvas-views` | 🟡 Planned | | Update | `PATCH /v1/canvas-views/{id}` | 🟡 Planned | | Delete | `DELETE /v1/canvas-views/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | canvas\_view\_id | string (UUID) — 🔒 system | ✅ Yes | UNIQUE; DEFAULT gen\_random\_uuid() | — | Public-facing UUID for JSON:API addressing. System-assigned at creation; never client-provided. | | template\_id | string (varchar 50) | ✅ Yes | max 50 chars; partial unique with workspace on deleted\_at IS NULL (idx\_canvas\_views\_workspace\_template\_active); validated at service layer against CANVAS\_TEMPLATES | Service-layer validated against CANVAS\_TEMPLATES enum from @wellapp/shared (e.g. 'investor-report-v1'). Stored as varchar — no DB enum. | Identifies which canvas template this layout belongs to. Used as the natural write key; the FE addresses the row as PUT .../canvas-views/:template\_id. | | name | string (varchar 255) \| null | ⚪ No | max 255 chars; nullable | — | Optional human label for the layout. Reserved for future multi-named-layout support per workspace/template pair. Null = the implicit default layout. | | blocks | CanvasBlockConfig\[] (jsonb) | ✅ Yes | NOT NULL jsonb; service enforces: 1-7 entries, unique slot\_id values, valid width enum ('one\_third'\|'two\_thirds'), positions form a permutation of \[0, length) | Array of \{ slot\_id: 'header'\|'kpi-1'\|'kpi-2'\|'kpi-3'\|'body-left'\|'body-right'\|'footer', width: 'one\_third'\|'two\_thirds', position: number } | Ordered list of selected blocks defining the canvas layout. The read formatter drops unknown slot\_id values (forward-compat); the write path strictly validates against CANVAS\_SLOT\_IDS. | | created\_at | datetime — 🔒 system | ✅ Yes | NOT NULL timestamptz; set once via onCreate hook | — | Timestamp when the canvas view was first created. Set automatically by the MikroORM onCreate lifecycle hook. | | updated\_at | datetime — 🔒 system | ⚪ No | nullable timestamptz; set by onCreate and onUpdate hooks | — | Timestamp of the last upsert. Updated automatically via the MikroORM onUpdate lifecycle hook on every write. | | deleted\_at | datetime \| null — 🔒 system | ⚪ No | nullable timestamptz; null = active row | — | Soft-delete timestamp. Null for active rows. The partial unique index exempts soft-deleted rows so delete + re-create preserves history. Set by the service on logical deletion. | ### Relationships | Name | Type | Required | Description | | ----------- | ------------------ | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | Yes — NOT NULL FK with ON DELETE CASCADE | The workspace that owns this canvas layout. Workspace deletion cascades and removes all associated canvas\_views. All queries are workspace-scoped via this FK. | | created\_by | to-one (ManyToOne) | No — nullable FK with ON DELETE SET NULL | The People (user) who created this canvas view. Nullable; set to null if the creating user is deleted. References the peoples table. | ### System-computed * canvas\_view\_id: auto-assigned via gen\_random\_uuid() default at row creation — never client-provided * created\_at: set once by MikroORM onCreate hook (new Date()) * updated\_at: set by onCreate and onUpdate hooks on every write * deleted\_at: set by the service layer on soft deletion; null for active rows * pk: internal auto-increment serial primary key — never exposed in the API * Partial unique index idx\_canvas\_views\_workspace\_template\_active enforces at most one active row per (workspace\_pk, template\_id) while allowing soft-deleted history rows to coexist * The write path is an upsert keyed on (workspace, template\_id) — the service resolves or creates the row rather than requiring the caller to supply canvas\_view\_id * blocks validation is fully service-layer enforced (1-7 entries, unique slot\_ids, valid widths, position permutation) — no DB-level CHECK constraint on the JSONB column * The read formatter (canvas-view\.formatter.ts) drops unknown slot\_id entries rather than throwing — forward-compat for future slot registry expansions ## Example ```json theme={null} { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "canvas_view", "attributes": { "template_id": "investor-report-v1", "name": null, "blocks": [ { "slot_id": "kpi-1", "width": "one_third", "position": 0 }, { "slot_id": "kpi-2", "width": "one_third", "position": 1 }, { "slot_id": "kpi-3", "width": "one_third", "position": 2 }, { "slot_id": "body-left", "width": "one_third", "position": 3 }, { "slot_id": "body-right", "width": "two_thirds", "position": 4 } ], "created_at": "2026-05-16T17:00:00.000Z", "updated_at": "2026-05-20T09:30:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "id": "wsp_uuid_here", "type": "workspace" } }, "created_by": { "data": { "id": "ppl_uuid_here", "type": "people" } } } } } ``` Source: `apps/api/src/database/entities/CanvasView.ts` · domain: workspace · tier: Infrastructure # Card Source: https://docs.wellapp.ai/object-reference/cards A `card` represents a physical or virtual payment card (credit, debit, prepaid, corporate, or virtual) associated with a workspace A `card` represents a physical or virtual payment card (credit, debit, prepaid, corporate, or virtual) associated with a workspace. Each card record identifies its cardholder — either a Company or a People (mutually exclusive) — and carries card-identification attributes such as the last four digits, anonymized PAN, brand, type, and lifecycle dates. The entity is used to enrich payment-means data and surfaces in the financial graph as a composite via `composite_cards_list` on related roots (companies, people) and as a first-class `cards` records root. | Naming | Value | | ------------------------------- | ----------- | | Object | Card | | Resource type (JSON:API `type`) | `card` | | Collection / records root | `cards` | | REST base | `/v1/cards` | | Entity class | `Card` | ## API operations | Operation | Method & path | Status | | --------- | ----------------------- | ------------- | | List | `GET /v1/cards` | ✅ Implemented | | Retrieve | `GET /v1/cards/{id}` | ✅ Implemented | | Create | `POST /v1/cards` | 🟡 Planned | | Update | `PATCH /v1/cards/{id}` | 🟡 Planned | | Delete | `DELETE /v1/cards/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | card\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen\_random\_uuid() on insert | — | Public stable identifier for the card. Use this UUID in all API references; never expose the internal `pk`. | | last\_four\_digits | string | ✅ Yes | length = 4; CHECK last\_four\_digits \~ '^\[0-9]\{4}\$' | Exactly 4 numeric digits | The last four digits of the card number. Required for all cards; used for display and matching against payment-means records. | | anonymized\_pan | string | ⚪ No | max length 30; nullable | — | Partially masked PAN string, e.g. '4539xXXXXXXXXXX4291'. Populated by connectors that provide it; absent when only the last four digits are known. | | brand | enum (CardBrandEnum) | ⚪ No | nullable; native PostgreSQL enum 'card\_brand\_enum' | visa, mastercard, amex, discover, diners, jcb, unionpay | Card network / brand. Sourced from the connector's card metadata. Null when the brand cannot be determined. | | type | enum (CardTypeEnum) | ⚪ No | nullable; native PostgreSQL enum 'card\_type\_enum' | credit, debit, prepaid, corporate, virtual | Functional category of the card. Corporate and virtual cards commonly surface from expense-management connectors. | | expiration\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date after which the card is no longer valid. Typically the last day of the expiry month. | | start\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date from which the card becomes valid. Common on UK-issued cards that carry an explicit start date on the face. | | issue\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date the card was issued by the issuing institution. Distinct from start\_date — a card may be issued before it becomes valid. | | cardholder\_name | string | ⚪ No | max length 100; nullable | — | The name embossed or printed on the card. May differ from the linked People.full\_name when the card was issued in a trade name or role name. | | created\_at | datetime, 🔒 system | ✅ Yes | set once via onCreate lifecycle hook; never updated | — | Timestamp when the card record was created in the Well database. Auto-populated; not accepted from the API client. | | updated\_at | datetime, 🔒 system | ⚪ No | set via onCreate and onUpdate lifecycle hooks; nullable in schema | — | Timestamp of the last modification to this record. Auto-managed by MikroORM lifecycle hooks. | | deleted\_at | datetime | ⚪ No | nullable; soft-delete sentinel; all active-record queries filter deleted\_at IS NULL | null (active) or a past ISO timestamp (soft-deleted) | Soft-delete timestamp. When set, the card is logically deleted. Indexes on (company\_pk, deleted\_at) and (workspace\_pk, deleted\_at) are defined to avoid scanning deleted tuples in Hasura traversal paths. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | company | to-one (company) | ⚪ No — mutually exclusive with `people`; one cardholder FK must be non-null for a meaningful record | The Company that owns or is the named cardholder of this card. FK: cards.company\_pk → companies.pk. Indexed as (company\_pk, deleted\_at) to support Hasura traversal `companies.cards` without sequential scans. Set to NULL on company deletion (ON DELETE SET NULL). | | people | to-one (people) | ⚪ No — mutually exclusive with `company`; at most one cardholder FK is set | The People (person) who is the named cardholder on this card. FK: cards.people\_pk → peoples.pk. Indexed as (people\_pk) to support Hasura traversal `peoples.cards`. Set to NULL on people deletion (ON DELETE SET NULL). | | workspace | to-one (workspace) | ⚪ No (nullable FK, but all well-formed records carry a workspace) | The tenant workspace this card belongs to. FK: cards.workspace\_pk → workspaces.pk. Indexed as (workspace\_pk, deleted\_at) for workspace-scoped queries and Hasura RLS filtering. Set to NULL on workspace deletion (ON DELETE SET NULL). | ### System-computed * `card_id` is generated by PostgreSQL `gen_random_uuid()` on INSERT; the value is immutable after creation. * `created_at` is set once by the MikroORM `@Property({ onCreate })` lifecycle hook at record creation and is never modified thereafter. * `updated_at` is set by the MikroORM `@Property({ onCreate, onUpdate })` lifecycle hooks — populated on both create and every subsequent update. * `deleted_at` is null for all active records. Setting it to a timestamp soft-deletes the card; the row is retained in the database. Every active-record query must filter `deleted_at IS NULL`. * Cardholder is polymorphic: exactly one of `company` or `people` should be non-null for a meaningful card record. Neither FK carries a database-level mutual-exclusion constraint — the invariant is enforced at the service layer. * Three composite indexes are maintained for hot-path Hasura traversals: `idx_cards_company_deleted (company_pk, deleted_at)`, `idx_cards_people (people_pk)`, and `idx_cards_workspace_deleted (workspace_pk, deleted_at)`. These were added in Migration20260416200000 after Query Insights revealed sequential scans on the `companies.cards`, `peoples.cards`, and workspace-scoped traversal paths. * The `cards` root is surfaced as a `composite_cards_list` array composite on the `companies` and `people` records roots (source\_fields: card\_id, brand, last\_four\_digits, type). The composite is defined in `composites.yml` under `companies` and `people` with `display_type: relation_list` and a `sort_proxy` of `cards_aggregate.min.brand`. * Cards are also linked from payment\_means via the `payment_means.card` relationship (FK: payment\_means.card\_pk → cards.pk, ON DELETE SET NULL), added in Migration20260102111942. This underpins the `composite_payment_means_summary` composite renderer which reads `payment_means.card.brand`, `payment_means.card.last_four_digits`, and `payment_means.card.type`. ## Example ```json theme={null} { "data": { "type": "card", "id": "c3a7e2f1-84bb-4f91-b9c3-1d2e5f6a7b89", "attributes": { "card_id": "c3a7e2f1-84bb-4f91-b9c3-1d2e5f6a7b89", "last_four_digits": "4291", "anonymized_pan": "4539xXXXXXXXXXX4291", "brand": "visa", "type": "corporate", "expiration_date": "2027-09-30", "start_date": "2023-10-01", "issue_date": "2023-09-15", "cardholder_name": "Alice Moreau", "created_at": "2023-09-15T10:22:00.000Z", "updated_at": "2024-03-01T08:14:35.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "people": { "data": null }, "workspace": { "data": { "type": "workspace", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" } } } } } ``` Source: `apps/api/src/database/entities/Card.ts` · domain: financial-graph · tier: Supporting # Category Source: https://docs.wellapp.ai/object-reference/categories Category is a workspace-global catalog entity representing a named classification tag that can be applied to companies or transactions Category is a workspace-global catalog entity representing a named classification tag that can be applied to companies or transactions. It is not workspace-scoped itself — it acts as a shared taxonomy that any workspace can reference through the CompanyCategory pivot. A category carries a `category_type` discriminator (`company` or `transaction`) that controls which entity type it may label, and a `name` string that is the human-visible label. | Naming | Value | | ------------------------------- | ---------------- | | Object | Category | | Resource type (JSON:API `type`) | `category` | | Collection / records root | `categories` | | REST base | `/v1/categories` | | Entity class | `Category` | ## API operations | Operation | Method & path | Status | | --------- | ---------------------------- | ------------- | | List | `GET /v1/categories` | ✅ Implemented | | Retrieve | `GET /v1/categories/{id}` | ✅ Implemented | | Create | `POST /v1/categories` | 🟡 Planned | | Update | `PATCH /v1/categories/{id}` | 🟡 Planned | | Delete | `DELETE /v1/categories/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | category\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() at insert | — | Public immutable identifier for the category, used in all API surfaces and relationships. | | name | string | ✅ Yes | varchar(255), NOT NULL | — | Human-readable label for the category (e.g. 'SaaS', 'Payroll', 'Transport'). Must be unique within a given category\_type in practice (enforced at the application layer, not at the DB level). | | category\_type | string (enum) | ✅ Yes | NOT NULL; native PG enum `core_api.category_type_enum`; default 'company' | company \| transaction | Discriminator controlling which entity type this category tag applies to. `company` categories are attached to companies via CompanyCategory; `transaction` categories label bank transactions. | | created\_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | NOT NULL; set by MikroORM onCreate lifecycle hook | — | Timestamp of when the category record was first created. | | updated\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | nullable; set by MikroORM onCreate and onUpdate lifecycle hooks | — | Timestamp of the most recent update to this category record. | | deleted\_at | string (ISO 8601 datetime) \| null, 🔒 system | ⚪ No | nullable; null means active | — | Soft-delete sentinel. When non-null, the category is considered deleted and must be excluded from all active queries via `deleted_at IS NULL` predicate. | ### System-computed * `category_id` is generated at insert by the Postgres expression `gen_random_uuid()` (defaultRaw). It is unique and immutable. * `created_at` is set by the MikroORM `onCreate` lifecycle hook to `new Date()` at insertion time. * `updated_at` is set by both `onCreate` and `onUpdate` lifecycle hooks; tracks last modification. * `deleted_at` is null on active records and set to the deletion timestamp on soft-delete. All queries must filter `deleted_at IS NULL`. * `category_type` defaults to `CategoryTypeEnum.COMPANY` ('company') at the entity level. The DB column carries `NOT NULL DEFAULT 'company'` via the `category_type_enum` native PG type added in `Migration20260528120000`. * The partial composite index `idx_categories_category_type_active ON core_api.categories (category_type, name) WHERE deleted_at IS NULL` is maintained for fast lookup by type within the active catalog. * Category is a global (non-workspace-scoped) catalog entity — it has no `workspace_pk` column. Workspace-level association is achieved exclusively through the `CompanyCategory` pivot, which joins a workspace-scoped Company to a global Category. * Transaction-type catalog entries were seeded by `Migration20260528120000` with a fixed list of standard transaction category names (e.g. 'Payroll', 'Rent', 'Tax', etc.). ## Example ```json theme={null} { "data": { "type": "category", "id": "d4f8a3c1-09b2-4e77-bc3a-f21e6098d5ea", "attributes": { "category_id": "d4f8a3c1-09b2-4e77-bc3a-f21e6098d5ea", "name": "SaaS", "category_type": "company", "created_at": "2025-09-14T10:23:45.000Z", "updated_at": "2025-09-14T10:23:45.000Z", "deleted_at": null } } } ``` Source: `apps/api/src/database/entities/Category.ts` · domain: financial-graph · tier: Supporting # Chat Conversation Source: https://docs.wellapp.ai/object-reference/chat_conversations A ChatConversation record represents a single AI chat session within a workspace, persisting the full message history, model-side context snapshot, UI tab state A ChatConversation record represents a single AI chat session within a workspace, persisting the full message history, model-side context snapshot, UI tab state, and runtime metadata needed to resume the conversation exactly where it left off. It is workspace-scoped (every row carries a mandatory workspace FK) and is used by both the web app and the browser extension, with the source column preventing each surface from polluting the other's recency list. The entity belongs to the Activity category and is notable for being soft-delete-capable but not exposed through the standard data-views pipeline; it has no entry in overrides.yml or composites.yml. | Naming | Value | | ------------------------------- | ------------------------ | | Object | Chat Conversation | | Resource type (JSON:API `type`) | `chat_conversation` | | Collection / records root | `chat_conversations` | | REST base | `/v1/chat-conversations` | | Entity class | `ChatConversation` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------ | ------------- | | List | `GET /v1/chat-conversations` | ✅ Implemented | | Retrieve | `GET /v1/chat-conversations/{id}` | ✅ Implemented | | Create | `POST /v1/chat-conversations` | 🟡 Planned | | Update | `PATCH /v1/chat-conversations/{id}` | 🟡 Planned | | Delete | `DELETE /v1/chat-conversations/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ----------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | thread\_id | string, UUID | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public stable identifier for the conversation. Used as the resource id in all API responses and by the frontend to resume a specific session. Never changes after creation. | | title | string | ⚪ No | varchar(255), nullable | — | Human-readable label for the conversation, typically auto-generated from the first user message or set manually. NULL until the AI or user assigns one. | | mode | string | ⚪ No | varchar(20), nullable | ask \| agent \| task\_flow | Chat mode active when the conversation was last persisted. Determines which set of AI tools and prompts the assistant uses. NULL for rows created before mode was introduced. | | source | string | ⚪ No | varchar(20), nullable; partial composite index idx\_chat\_conversations\_workspace\_source\_updated on (workspace\_pk, source, updated\_at DESC) WHERE deleted\_at IS NULL | web \| extension | Surface that created the conversation. The web app's recent-conversations popover and the extension sidepanel history sheet each default-filter to their own source so neither clutters the other's list. Conversations remain reachable from both surfaces by thread\_id regardless of source. Existing rows were backfilled by Migration20260427160000 using a page\_path heuristic (non-null page\_path not starting with /workspaces/ → extension, otherwise → web); only API-created rows that omit the field are NULL. | | page\_path | string | ⚪ No | varchar(512), nullable | — | Full pathname (without origin) of the page from which the conversation was initiated or last active. Used by the frontend to restore navigation context on resume. | | total\_tokens | integer | ✅ Yes | default 0 | — | Cumulative token count across all AI model calls in the conversation. Used for usage tracking and to surface cost attribution per conversation. | | messages | jsonb (ConversationMessage\[]) | ✅ Yes | default '\[]'; each element: \{ role: 'user'\|'assistant', content: string, parts?: PersistedPart\[], ts: string (ISO) } | — | Ordered array of all messages exchanged in the conversation. Each message carries a role, a plain-text content field, an optional structured parts array (for rich assistant output such as tool-call results, images, or embedded data), and an ISO timestamp. This is the canonical source of truth for conversation replay. | | context | jsonb (ConversationContext \| null) | ⚪ No | nullable; all sub-fields optional and nullable | — | Snapshot of the UI and query state at the time of the last persist. Enables full-state restoration when a conversation is resumed: includes the active records root, where-clause, order-by, field selection, selection type (cells/rows/columns/none), selected cells and columns, the last executed GraphQL query and its result summary, a document-analysis summary, and a GCS path for any uploaded document. | | tabs | jsonb (PersistedTab\[]) | ✅ Yes | default '\[]'; each element: \{ key: string, destinationId: WorkspaceDestinationId, subPath?: string }; max 50 entries (MAX\_PERSISTED\_TABS); subPath validated by isSafeSubPath (no '..' traversal, no absolute URLs, no protocol-relative paths) | — | Open tab state at the time of last persist. Each tab carries a unique key (UUID for new\_tab instances; destinationId for all other destinations) so multiple new\_tab launchers survive hydration as distinct instances, and an optional subPath to carry per-tab in-flight state such as a detail route or picker selection. Replaces the legacy string-array tabs column as of Migration20260522150000. | | active\_tab\_key | string \| null | ⚪ No | varchar(64), nullable; must reference a key present in tabs\[] — the DB does not enforce this as a FK, but the API and migration maintain the invariant | — | Pointer into tabs\[] identifying which tab was active when the conversation was last persisted. References tabs\[i].key (a UUID for new\_tab instances; the destinationId string for all other destinations). Replaced the legacy active\_path URL string in Migration20260522150000. | | deleted\_at | timestamptz \| null | ⚪ No | nullable | — | Soft-delete timestamp. When set, the conversation is treated as deleted and excluded from normal queries. Unlike most other entities in the platform, ChatConversation is listed in CLAUDE.md as an exception to the standard soft-delete filter pattern (alongside DataView and SessionEvent). | | created\_at | timestamptz, 🔒 system | ✅ Yes | set once via onCreate lifecycle hook | — | Timestamp of conversation creation. Set automatically by the MikroORM onCreate hook; never writable via the API. | | updated\_at | timestamptz, 🔒 system | ✅ Yes | set on every write via onCreate + onUpdate lifecycle hooks; indexed | — | Timestamp of the most recent update to any field on the conversation. Used by the frontend recent-conversations list to sort by recency. Carries an explicit @Index decorator for performant ORDER BY updated\_at DESC queries. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | The workspace this conversation belongs to. Every chat\_conversations row is workspace-scoped; the foreign key is NOT NULL. The compound index idx\_chat\_conversations\_workspace\_deleted covers (workspace\_pk, deleted\_at) to accelerate the Hasura permission filter and the recency-sorted list query. | ### System-computed * thread\_id is the public resource identifier, generated via defaultRaw: gen\_random\_uuid() at INSERT time. The internal pk (auto-increment integer) is never exposed via the API. * created\_at is set once by the MikroORM onCreate lifecycle hook (new Date()) and is never subsequently modified. * updated\_at is set by both onCreate and onUpdate lifecycle hooks, reflecting the wall-clock time of every PATCH or write. It carries an explicit @Index decorator for ORDER BY performance. * deleted\_at uses soft-delete semantics consistent with the rest of the platform, but ChatConversation is an explicitly listed exception in apps/api/CLAUDE.md to the standard soft-delete filter ('append-only audit log' category). * messages defaults to an empty array \[] at entity construction. Parts within each message element use the PersistedPart discriminated union (type-tagged), making round-tripped history messages safely hydrable from JSONB. * tabs defaults to an empty array \[] at entity construction. As of Migration20260522150000, tabs are keyed objects (PersistedTab\[]) rather than the legacy string\[]. The migration backfilled all existing rows: new\_tab elements received gen\_random\_uuid() keys; all other destinations used their destinationId as the key. * active\_tab\_key replaced the legacy active\_path varchar(512) column in Migration20260522150000. The migration extracted the active destination from active\_path URLs via regex prefix matching against the known WORKSPACE\_DESTINATIONS segment list, then dropped the active\_path column. * total\_tokens defaults to 0 at construction and is incremented by the AI chat service after each model call. It is never reset. * The compound index idx\_chat\_conversations\_workspace\_deleted (workspace\_pk, deleted\_at) was added in Migration20260416000000 specifically to accelerate the Hasura hot-path permission filter (workspace\_pk = \$1 AND deleted\_at IS NULL). * The partial composite index idx\_chat\_conversations\_workspace\_source\_updated (workspace\_pk, source, updated\_at DESC) WHERE deleted\_at IS NULL was added in Migration20260427160000 to accelerate surface-filtered recency-sorted list queries (the web app and extension history panels each filter by their own source value). * source was backfilled by Migration20260427160000 for all pre-existing rows using a page\_path heuristic: rows with a non-null page\_path not starting with /workspaces/ were set to 'extension'; all others were set to 'web'. Only rows created via the API without a source value remain NULL. * There is no sourceWorkspaceConnector relation. ChatConversation rows are created exclusively by the chat service in response to user actions; they are not produced by connector syncs. * source is nullable (rows created via the API without a source field). The frontend's surface-filtering logic treats NULL as 'unknown' and shows those conversations on both surfaces. ## Example ```json theme={null} { "data": { "type": "chat_conversation", "id": "c7e1a2f3-88d4-4b9e-9c6a-3f0e1d2b5a7c", "attributes": { "thread_id": "c7e1a2f3-88d4-4b9e-9c6a-3f0e1d2b5a7c", "title": "Q1 invoice reconciliation", "mode": "agent", "source": "web", "page_path": "/workspaces/9f3e2d1a-7b4c-4e5f-8a2b-1c0d3e6f9a8b/records/invoices", "total_tokens": 14832, "messages": [ { "role": "user", "content": "Show me all unpaid invoices from March", "ts": "2026-05-14T09:12:33.000Z" }, { "role": "assistant", "content": "I found 7 unpaid invoices from March totalling EUR 34,200.", "parts": [ { "type": "text", "text": "I found 7 unpaid invoices from March totalling EUR 34,200." } ], "ts": "2026-05-14T09:12:36.000Z" } ], "context": { "root": "invoices", "lastGqlQuery": "{ invoices(where: { status: { _eq: \"unpaid\" } }) { id amount } }", "queryResult": null, "columns": ["id", "amount", "status", "due_date"], "rowCount": 7, "sampleRow": { "id": "inv_001", "amount": 4800, "status": "unpaid" }, "whereClause": { "status": { "_eq": "unpaid" } }, "orderBy": { "field": "due_date", "direction": "asc" }, "fields": [["id"], ["amount"], ["status"]], "selectionType": "rows", "selectedCells": null, "selectedColumns": null, "documentAnalysisSummary": null, "gcsPath": null }, "tabs": [ { "key": "invoices", "destinationId": "invoices" }, { "key": "3a9c1e2d-44b7-4f0e-8c6a-2b1d3e5f7a9c", "destinationId": "new_tab", "subPath": "/workspaces/9f3e2d1a/new-tab" } ], "active_tab_key": "invoices", "deleted_at": null, "created_at": "2026-05-14T09:12:30.000Z", "updated_at": "2026-05-14T09:12:36.000Z" }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "9f3e2d1a-7b4c-4e5f-8a2b-1c0d3e6f9a8b" } } } } } ``` Source: `apps/api/src/database/entities/ChatConversation.ts` · domain: workspace · tier: Activity # Check Source: https://docs.wellapp.ai/object-reference/checks Check represents a paper cheque instrument within the Well financial graph Check represents a paper cheque instrument within the Well financial graph. It captures the identifying information printed on a physical cheque — the CMC7 magnetic-ink line, the check number, and the issue date — and ties the instrument to either a Company or a Person (the cheque holder) and to the Workspace that owns the record. Checks are referenced by PaymentMeans rows so that a payment settled by cheque can link back to the originating instrument. | Naming | Value | | ------------------------------- | ------------ | | Object | Check | | Resource type (JSON:API `type`) | `check` | | Collection / records root | `checks` | | REST base | `/v1/checks` | | Entity class | `Check` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------ | ------------- | | List | `GET /v1/checks` | ✅ Implemented | | Retrieve | `GET /v1/checks/{id}` | ✅ Implemented | | Create | `POST /v1/checks` | 🟡 Planned | | Update | `PATCH /v1/checks/{id}` | 🟡 Planned | | Delete | `DELETE /v1/checks/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | check\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() on INSERT | — | Public immutable identifier for the check. Used in all API responses and external references. Never expose the internal pk. | | cmc7 | string | ⚪ No | max length 35; nullable | — | CMC7 magnetic-ink character recognition line printed at the bottom of the cheque. Encodes: 7-digit check number, 12-digit bank/branch code, 11-digit account number, 2-digit check key. Format is bank-dependent; no application-level regex is enforced. | | check\_number | string | ⚪ No | max length 20; nullable | — | Human-readable check number as printed on the instrument (the sequential identifier within a cheque book). Used as the sort proxy for the composite\_checks\_list composite. | | issue\_date | date (YYYY-MM-DD) | ⚪ No | nullable; stored as PostgreSQL date (no time component) | — | The date the cheque was issued (written), as declared on the instrument. Distinct from any settlement or booking date on an associated transaction. | | created\_at | datetime, 🔒 system | ✅ Yes | set on INSERT via onCreate lifecycle hook; not nullable | — | Timestamp of record creation in the Well database. Set automatically; not editable by callers. | | updated\_at | datetime, 🔒 system | ⚪ No | set on INSERT and on every UPDATE via onUpdate lifecycle hook; nullable (null until first update on legacy rows) | — | Timestamp of the most recent mutation. Managed by MikroORM lifecycle hooks. | | deleted\_at | datetime | ⚪ No | nullable; null means record is active | — | Soft-delete sentinel. When set, the record is logically deleted and must be excluded from all queries (WHERE deleted\_at IS NULL). Hasura select\_permissions enforce this gate automatically for the user role. | ### Relationships | Name | Type | Required | Description | | -------------- | ------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (company) | No — mutually exclusive with people; at most one of the two is set | The Company that holds or issued this cheque. Stored as company\_pk FK. Either company or people is set, never both — the entity comment documents this constraint as a business rule enforced at the application layer. | | people | to-one (people) | No — mutually exclusive with company; at most one of the two is set | The Person who holds or issued this cheque. Stored as people\_pk FK. Mutually exclusive with company at the business-rule level. | | workspace | to-one (workspace) | No (nullable FK) — effectively required for any workspace-scoped use | The Workspace that owns this check record. Used by Hasura RLS to tenant-isolate query results via X-Hasura-Workspace-Id header. Stored as workspace\_pk FK. | | payment\_means | to-many (payment\_means) | No | The PaymentMeans rows that reference this cheque instrument via check\_pk FK. A single Check may be referenced by multiple payment means (e.g. if re-presented or recorded across multiple contexts). The inverse FK lives on payment\_means; checks.payment\_means is a Hasura array\_relationship only — there is no @OneToMany decorator in the MikroORM entity. | ### System-computed * check\_id is generated by PostgreSQL gen\_random\_uuid() on INSERT and exposed as the public identifier; the internal auto-increment pk is never surfaced in API responses. * created\_at is set automatically by the MikroORM onCreate lifecycle hook on INSERT. * updated\_at is set by both onCreate and onUpdate lifecycle hooks, reflecting the timestamp of the most recent write. * deleted\_at is managed by application-layer soft-delete logic; Hasura select\_permissions for the user role enforce deleted\_at IS NULL at the query layer so soft-deleted checks are never returned to end users. * The company / people mutual-exclusion constraint (checkholder is either a Company OR a Person, not both) is a business-rule invariant documented in the entity source; no DB-level CHECK enforces it — enforcement is at the service/application layer. * Workspace scoping is enforced by Hasura RLS: the user-role filter requires workspace.workspace\_id = X-Hasura-Workspace-Id (direct) or workspace.workspace.workspace\_id = X-Hasura-Workspace-Id (child workspace), with workspace\_group membership paths for consolidated-view contexts. * The composite\_checks\_list composite (used when Check appears as a relation on Company or People records pages) is defined in composites.yml and surfaces check\_id, check\_number, and issue\_date as source\_fields with display\_type: relation\_list and is editable. * On Company and People records roots the Check appears as a composite\_checks\_list array field sorted by checks\_aggregate.min.check\_number. * On the checks records root, company and people checkholder composites (company.composite\_logo\_name, people.composite\_avatar\_fullname) and workspace composites are defined in composites.yml, along with composite\_payment\_means\_list. ## Example ```json theme={null} { "data": { "type": "check", "id": "c3f4a2e1-8b7d-4c09-a5f6-1d2e3f4a5b6c", "attributes": { "check_id": "c3f4a2e1-8b7d-4c09-a5f6-1d2e3f4a5b6c", "cmc7": "7654321012345678901234567890123", "check_number": "000123", "issue_date": "2026-01-15", "created_at": "2026-01-15T09:30:00.000Z", "updated_at": "2026-01-15T09:30:00.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "people": { "data": null }, "workspace": { "data": { "type": "workspace", "id": "w9x8y7z6-a5b4-c3d2-e1f0-g9h8i7j6k5l4" } }, "payment_means": { "data": [ { "type": "payment_means", "id": "pm-uuid-0001-0002-0003-000400050006" } ] } } } } ``` Source: `apps/api/src/database/entities/Check.ts` · domain: financial-graph · tier: Supporting # CollectError Source: https://docs.wellapp.ai/object-reference/collect_errors CollectError is a structured diagnostic sink for runtime errors raised by the Chrome extension while executing a saved blueprint (auth walls, page-not-found, ti CollectError is a structured diagnostic sink for runtime errors raised by the Chrome extension while executing a saved blueprint (auth walls, page-not-found, timeouts, selector failures, etc.). Each row captures the provider, error classification, severity, and optional contextual metadata at the moment the failure occurred. Rows are created exclusively by `CollectErrorService.write()` via `POST /v1/collect/blueprint-error`; the same service also opens a GitHub issue for human triage. Rows are soft-deleted and pruned opportunistically (sampled \~1 in 20 writes) after 30 days. The entity belongs to a nullable Workspace and optionally references the parent Collect session that triggered the error. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CollectError | | Resource type (JSON:API `type`) | `collect_error` | | Collection / records root | — (not a records root) | | REST base | `/v1/collect-errors` | | Entity class | `CollectError` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/collect-errors` | 🟡 Planned | | Retrieve | `GET /v1/collect-errors/{id}` | 🟡 Planned | | Create | `POST /v1/collect-errors` | 🟡 Planned | | Update | `PATCH /v1/collect-errors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/collect-errors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ------------------------------------- | -------- | -------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | error\_id | string (UUID) — 🔒 system | ✅ Yes | unique; default gen\_random\_uuid() | — | Stable public identifier for this error record. Generated by the database at insert time. Used as the JSON:API `id`. | | provider | string (text) | ✅ Yes | not null | — | Slug identifying the web provider the blueprint was targeting when the error occurred (e.g. `linkedin`, `hubspot`, `salesforce`). Free-form text; set by the Chrome extension. | | error\_type | string (text) | ✅ Yes | not null | — | Short machine-readable classification of the failure class (e.g. `auth_wall`, `selector_not_found`, `page_timeout`, `unexpected_redirect`). Indexed alongside `created_at` for Metabase aggregations. | | error\_message | string (text) | ✅ Yes | not null | — | Human-readable description of what went wrong, as reported by the Chrome extension at the point of failure. | | severity | enum (collect\_error\_severity\_enum) | ✅ Yes | not null; stored as native PostgreSQL enum in core\_api schema | low \| medium \| high \| critical | Operational severity of the error. Determined by the Chrome extension or CollectErrorService based on whether the error is recoverable and how much of the blueprint it blocks. | | metadata | jsonb | ⚪ No | nullable | — | Arbitrary structured context attached by the Chrome extension at the time of the error — may include DOM selector paths, step indexes, blueprint version, partial extraction results, or browser state. Schema is free-form. | | source\_url | string (text) | ⚪ No | nullable | — | The full URL the Chrome extension was visiting when the error was raised. | | domain | string (text) | ⚪ No | nullable | — | Extracted domain of `source_url` (e.g. `linkedin.com`). Stored separately for indexed lookups without requiring URL parsing at query time. | | github\_issue\_url | string (text) | ⚪ No | nullable | — | URL of the GitHub issue automatically created for human triage by `CollectErrorService.write()` in parallel with this row insert. Preserves the prior error-handling path (GitHub issue creation) alongside the new structured row. | | occurred\_at | timestamptz | ⚪ No | nullable | — | Client-reported timestamp of when the error actually occurred in the browser, as opposed to `created_at` which is the server ingestion time. May differ from `created_at` by network/queue latency. | | created\_at | timestamptz — 🔒 system | ✅ Yes | not null; default now(); set via MikroORM onCreate hook | — | Server-side ingestion timestamp. Set automatically on insert; never modified after. | | deleted\_at | timestamptz — 🔒 system | ⚪ No | nullable; soft-delete sentinel | — | Soft-delete timestamp. Null means the row is active. Set by `CollectErrorService` during opportunistic 30-day pruning (sampled \~1/20 writes). Rows with `deleted_at IS NOT NULL` are excluded from standard queries and are tenant-invisible via Hasura RLS. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | No — nullable | The Workspace this error belongs to. Stored as `workspace_pk` (FK → `core_api.workspaces.pk`, ON DELETE CASCADE). Nullable by design: errors with `workspace_pk IS NULL` are tenant-invisible via Hasura RLS and are only accessible via admin/direct DB access. This mirrors the `BlueprintPersistenceError.workspace` pattern. | | collect | to-one (ManyToOne) | No — nullable | The parent Collect session (Chrome extension execution run) during which this error was raised. Stored as `collect_pk` (FK → `core_api.collects.pk`, ON DELETE SET NULL). Nullable because errors may be reported outside of a tracked Collect session, and the parent session can be deleted independently without cascading the error row. | ### System-computed * error\_id — generated by gen\_random\_uuid() at INSERT time; never modified * created\_at — set via MikroORM onCreate hook (new Date()) on INSERT; no updatedAt hook on this entity * deleted\_at — managed by CollectErrorService opportunistic pruning (\~1/20 writes prune rows older than 30 days via soft-delete); not set by standard MikroORM lifecycle hooks * workspace\_pk — FK resolved from request context by CollectErrorService.write(); the caller supplies the Workspace entity reference, not a raw ID * collect\_pk — FK resolved from the paired Collect session if one is active; NULL when the error arrives without a session context ## Example ```json theme={null} { "data": { "type": "collect_error", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "provider": "linkedin", "error_type": "auth_wall", "error_message": "Login gate detected at /in/johndoe — blueprint paused.", "severity": "high", "metadata": { "selector": "#main-content", "step_index": 3, "blueprint_version": "2" }, "source_url": "https://www.linkedin.com/in/johndoe", "domain": "linkedin.com", "github_issue_url": "https://github.com/well-app/ops/issues/4821", "occurred_at": "2026-04-30T14:22:10.000Z", "created_at": "2026-04-30T14:22:11.543Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws-uuid-here" } }, "collect": { "data": { "type": "collect", "id": "collect-uuid-here" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/CollectError.ts` · domain: ingestion · tier: Infrastructure # Collect Source: https://docs.wellapp.ai/object-reference/collects A `Collect` represents a single data-collection run initiated by an authenticated user, typically driven by the browser extension (popup, side panel, or bluepri A `Collect` represents a single data-collection run initiated by an authenticated user, typically driven by the browser extension (popup, side panel, or blueprint automation). Each run is bound to one `People` (the subject being enriched), one `Workspace` (stamped at creation from `req.workspace` and never re-derived), and optionally one `Task` (the automation trigger). The `status` lifecycle progresses from `todo` through `in_progress` to `finalized` or `error`. Workspace binding is nullable on legacy rows to handle orphaned people records; all workspace-scoped queries silently exclude those rows via Hasura RLS. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Collect | | Resource type (JSON:API `type`) | `collect` | | Collection / records root | — (not a records root) | | REST base | `/v1/collects` | | Entity class | `Collect` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ---------- | | List | `GET /v1/collects` | 🟡 Planned | | Retrieve | `GET /v1/collects/{id}` | 🟡 Planned | | Create | `POST /v1/collects` | 🟡 Planned | | Update | `PATCH /v1/collects/{id}` | 🟡 Planned | | Delete | `DELETE /v1/collects/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------------------- | -------- | ------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | collect\_id | string (UUID) | ✅ Yes | unique | — | Public stable identifier for the collect run. Generated server-side via gen\_random\_uuid(); never user-supplied. | | provider | string | ⚪ No | varchar(255), nullable | — | Slug identifying the data-collection provider or browser extension surface that initiated the run (e.g. 'linkedin', 'popup'). Null for manual or legacy rows. | | status | enum (collect\_status\_enum) | ✅ Yes | native enum collect\_status\_enum, default 'todo' | todo \| in\_progress \| finalized \| error | Lifecycle state of the collection run. Defaults to 'todo' at creation; transitions to 'in\_progress', then 'finalized' (success) or 'error' (terminal failure). Terminal statuses trigger a workspace\_connector\_sync\_logs entry. | | started\_at | 🔒 system — timestamptz | ⚪ No | nullable, timestamptz | — | Timestamp when the run began active processing. Set by the client via PATCH; null until the extension begins extraction. | | ended\_at | 🔒 system — timestamptz | ⚪ No | nullable, timestamptz | — | Timestamp when the run reached a terminal state (finalized or error). Set by the client via PATCH alongside status update. | | created\_at | 🔒 system — timestamptz | ✅ Yes | not null, set once on insert | — | Row creation timestamp. Set via onCreate hook; never user-editable. | | updated\_at | 🔒 system — timestamptz | ⚪ No | nullable, set on insert and every update | — | Last modification timestamp. Automatically set on every update via onUpdate hook. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | nullable | — | Soft-delete timestamp. When set, the row is excluded from all workspace-scoped queries and Hasura RLS filters. Set via DELETE endpoint (CollectService.deleteCollect). | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | people | to-one (ManyToOne) | ✅ Yes | The People record being enriched during this collection run. Resolved at creation from the authenticated user's associated person. FK: collects.people\_pk → peoples.pk. Indexed via idx\_collects\_people (partial, deleted\_at IS NULL). | | workspace | to-one (ManyToOne) | ⚪ No (nullable; required on all new rows by service-layer enforcement) | The Workspace this collect run belongs to. Stamped once at creation from req.workspace and never re-derived from subsequent JWT calls, ensuring cross-workspace switches cannot reroute writes mid-flight. Nullable to absorb legacy orphan rows; all workspace-scoped queries (Hasura RLS, service layer) exclude null rows automatically. FK: collects.workspace\_pk → workspaces.pk (cascade update+delete). Indexed via idx\_collects\_workspace\_status. | | task | to-one (ManyToOne) | ⚪ No | Optional link to the Task that triggered this collection run (blueprint automation). Null for manual/popup/side-panel collections and legacy rows. FK: collects.task\_pk → tasks.pk. On Task deletion the FK is set to null (deleteRule: 'set null'). | ### System-computed * collect\_id — generated server-side via gen\_random\_uuid() defaultRaw; never accepted from client input * created\_at — set once on insert via MikroORM onCreate hook (new Date()) * updated\_at — refreshed on every update via onUpdate hook (new Date()) * deleted\_at — soft-delete; set to current timestamp by CollectService.deleteCollect; null on active rows * workspace (workspace\_pk) — stamped at creation time from req.workspace by CollectService.createCollect and never subsequently overwritten, even on PATCH calls; this immutability is the tenant-safety guarantee for in-flight runs * status default — CollectStatusEnum.TODO at row creation unless overridden by the POST body * idx\_collects\_status — partial index on status WHERE deleted\_at IS NULL * idx\_collects\_people — partial index on people\_pk WHERE deleted\_at IS NULL * idx\_collects\_workspace\_status — composite index on (workspace\_pk, status) for active-run queries ## Example ```json theme={null} { "data": { "type": "collect", "id": "a3f7c2e1-84b0-4d9e-9c12-1e8f7a3b5d22", "attributes": { "collect_id": "a3f7c2e1-84b0-4d9e-9c12-1e8f7a3b5d22", "provider": "linkedin", "status": "in_progress", "started_at": "2026-06-02T09:15:00.000Z", "ended_at": null, "created_at": "2026-06-02T09:14:58.123Z", "updated_at": "2026-06-02T09:15:01.450Z", "deleted_at": null }, "relationships": { "people": { "data": { "type": "people", "id": "d8b1a4f2-11cc-4e77-b933-abc123456789" } }, "workspace": { "data": { "type": "workspace", "id": "7e91cd02-0a3b-4f55-8811-fedcba987654" } }, "task": { "data": null } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/Collect.ts` · domain: ingestion · tier: Infrastructure # CompanyCategory Source: https://docs.wellapp.ai/object-reference/company_categories CompanyCategory is a pivot relation that links a Company to a Category within the Well financial graph CompanyCategory is a pivot relation that links a Company to a Category within the Well financial graph. It carries no business attributes beyond the join keys and a creation timestamp, enabling a many-to-many classification of companies by user-defined categories (e.g. "Supplier", "Client"). Rows are soft-deleted via `deleted_at` rather than hard-deleted, preserving the audit trail of historical classifications. The entity is written exclusively by the connector-sync and enrichment pipelines; users cannot create or mutate rows through a resource PATCH. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyCategory | | Resource type (JSON:API `type`) | `company_category` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-categories` | | Entity class | `CompanyCategory` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------ | ---------- | | List | `GET /v1/company-categories` | 🟡 Planned | | Retrieve | `GET /v1/company-categories/{id}` | 🟡 Planned | | Create | `POST /v1/company-categories` | 🟡 Planned | | Update | `PATCH /v1/company-categories/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-categories/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | -------------------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | 🔒 system — timestamptz | ✅ Yes | Set once on INSERT via onCreate hook; never NULL | ISO-8601 datetime | Timestamp recording when this company–category association was created. Immutable after creation. | | deleted\_at | timestamptz \| null | ⚪ No | NULL means active; non-NULL means soft-deleted. No updated\_at present on this entity. | ISO-8601 datetime or null | Soft-delete timestamp. Set by the system when the company–category association is removed. All live queries must filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | -------- | ------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (ManyToOne) | Yes — NOT NULL FK with ON UPDATE CASCADE | The Company this category is attached to. FK: company\_categories.company\_pk → companies.pk. A soft-deleted company\_category row no longer appears in live queries but the FK is preserved for the audit trail. | | category | to-one (ManyToOne) | Yes — NOT NULL FK with ON UPDATE CASCADE | The Category being associated with the company. FK: company\_categories.category\_pk → categories.pk. Category rows carry a category\_type\_enum discriminator (e.g. COMPANY, TRANSACTION). | ### System-computed * pk — serial auto-increment integer, internal join key only; never exposed in the public API * created\_at — set once on INSERT via MikroORM onCreate: () => new Date(); no onUpdate hook exists on this entity * deleted\_at — written by the system (pipeline or service layer) when the pivot row is logically removed; never user-settable via PATCH * No UUID public identifier (company\_category\_id) exists on this entity — the row is identified exclusively by its compound context (company + category). The JSON:API id is therefore the internal pk. * No updated\_at column — the entity has no MikroORM onUpdate hook and the migration DDL confirms the column is absent. ## Example ```json theme={null} { "data": { "type": "company_category", "id": "1", "attributes": { "created_at": "2025-09-14T08:23:11.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "category": { "data": { "type": "category", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" } } } } } ``` Source: `apps/api/src/database/entities/CompanyCategory.ts` · domain: financial-graph · tier: Supporting # CompanyEmail Source: https://docs.wellapp.ai/object-reference/company_emails CompanyEmail is a soft-deletable pivot (bridge) entity that links a Company to an Email address within the same workspace CompanyEmail is a soft-deletable pivot (bridge) entity that links a Company to an Email address within the same workspace. Each row carries three metadata flags — is\_primary, is\_verify, and label — that characterize how the email relates to its parent company. The entity enforces a partial-unique constraint: at most one non-deleted row per company can carry is\_primary = TRUE, preventing multiple primary emails on the same company. It has no public UUID of its own; it is accessed via the parent company's relationship graph, not as a first-class API resource. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyEmail | | Resource type (JSON:API `type`) | `company_email` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-emails` | | Entity class | `CompanyEmail` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/company-emails` | 🟡 Planned | | Retrieve | `GET /v1/company-emails/{id}` | 🟡 Planned | | Create | `POST /v1/company-emails` | 🟡 Planned | | Update | `PATCH /v1/company-emails/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-emails/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | is\_primary | boolean | ✅ Yes | Partial-unique index uniq\_company\_emails\_primary\_company: only one row per company\_pk may have is\_primary = TRUE WHERE deleted\_at IS NULL | true \| false | Designates this as the primary email address for the company. At most one non-deleted record per company can be primary. Used by computed fields and by enrichment to surface the canonical contact email. | | is\_verify | boolean | ✅ Yes | NOT NULL (varchar(255) in raw DDL maps to boolean not null) | true \| false | Indicates whether this email address has been verified (e.g. by an enrichment pass or user confirmation). Does not affect tenant access control. | | label | string | ✅ Yes | varchar(255) NOT NULL | Free text; common values: 'billing', 'support', 'general', 'sales', 'contact' | Semantic tag describing the purpose of this email address for the company (e.g. billing, support). Stored as free text; no enum enforced at the DB layer. | | created\_at | 🔒 system — Date | ✅ Yes | timestamptz NOT NULL; set once via MikroORM onCreate hook | — | Timestamp when this company-email association was created. Set automatically at insert; never updated. | | deleted\_at | Date \| null | ⚪ No | timestamptz NULL; soft-delete sentinel — all live queries must filter WHERE deleted\_at IS NULL | — | Soft-delete timestamp. NULL means the record is active. When set, the row is logically removed: the partial-unique primary-email index ignores it, and Hasura RLS / repository queries filter it out. | ### Relationships | Name | Type | Required | Description | | ------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (ManyToOne) | ✅ Yes | The Company this email belongs to. FK company\_pk → core\_api.companies.pk. Indexed together with deleted\_at via idx\_company\_emails\_company\_deleted for Hasura array-relationship traversals. | | email | to-one (ManyToOne) | ✅ Yes | The Email address record (atomic entity storing the email string). FK email\_pk → core\_api.emails.pk. Indexed via idx\_company\_emails\_email. The actual address string is on the Email entity, not on this pivot. | ### System-computed * created\_at — set once by MikroORM onCreate: () => new Date(); never updated afterward (note: no updated\_at column on this entity). * deleted\_at — written by soft-delete cascades when the parent Company is soft-deleted; also written by service-layer cleanup when an Email association is explicitly removed. * Partial-unique index uniq\_company\_emails\_primary\_company is maintained by the database engine; enforcement is automatic on INSERT/UPDATE WHERE deleted\_at IS NULL AND is\_primary IS TRUE. * The entity has no public UUID (\*\_id column). It is not directly addressable via the public API; access is always through the parent company's array relationship (e.g. company\_emails on the Company resource). * Provenance (source\_workspace\_connector\_pk) is not tracked on this entity; rows are written by the enrichment pipeline, connector sync (via reconciliation persister), and direct service-layer mutations — the source is inferred from the parent Company's sourceWorkspaceConnector if needed. ## Example ```json theme={null} { "data": { "type": "company_email", "attributes": { "is_primary": true, "is_verify": true, "label": "billing", "created_at": "2025-09-14T10:22:00.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "a3f1bc2d-0001-4e88-9c1d-000000000001" } }, "email": { "data": { "type": "email", "id": "7e9d2a1c-0002-4b77-ab2e-000000000002" } } } } } ``` Source: `apps/api/src/database/entities/CompanyEmail.ts` · domain: financial-graph · tier: Supporting # CompanyFinancial Source: https://docs.wellapp.ai/object-reference/company_financials CompanyFinancial is a one-to-one 'financial profile' extension of the Company entity that stores default ledger-account preferences (accounts receivable and pay CompanyFinancial is a one-to-one "financial profile" extension of the Company entity that stores default ledger-account preferences (accounts receivable and payable) plus classifier provenance metadata indicating how and with what confidence those defaults were assigned. It is written exclusively by the internal classification pipeline — never directly by users — making it a read-only supporting record from the API consumer perspective. Each company may have at most one CompanyFinancial row (enforced by a UNIQUE constraint on `company_pk`). The `source` / `confidence` pair is governed by a database-level invariant: confidence is stored only when `source = 'classifier_auto'`; any transition away from the auto-classifier must clear confidence in the same write. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyFinancial | | Resource type (JSON:API `type`) | `company_financial` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-financials` | | Entity class | `CompanyFinancial` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------ | ---------- | | List | `GET /v1/company-financials` | 🟡 Planned | | Retrieve | `GET /v1/company-financials/{id}` | 🟡 Planned | | Create | `POST /v1/company-financials` | 🟡 Planned | | Update | `PATCH /v1/company-financials/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-financials/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------------- | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company\_financial\_id | string (UUID) | ✅ Yes | UNIQUE; DEFAULT gen\_random\_uuid() | — | Public UUID identifier for this record. Generated via gen\_random\_uuid() on insert. Used as the JSON:API resource `id`. | | source | 🔒 system — enum (CompanyFinancialSourceEnum) \| null | ⚪ No | Nullable; native PostgreSQL enum 'core\_api.company\_financial\_source\_enum'; CHECK company\_financials\_source\_confidence\_invariant | classifier\_auto \| classifier\_suggested\_human\_confirmed \| human\_override \| imported\_chart\_rule \| migration | Provenance of the default ledger-account assignment. Set by the classification pipeline; never written directly by users. When 'classifier\_auto', the `confidence` field must also be non-null (enforced by DB CHECK). Transitions away from 'classifier\_auto' must clear `confidence` in the same write. | | confidence | 🔒 system — decimal(4,3) stored as string \| null | ⚪ No | Nullable; DECIMAL(4,3); CHECK confidence IS NULL OR (confidence >= 0 AND confidence \<= 1); CHECK company\_financials\_source\_confidence\_invariant (confidence non-null ↔ source='classifier\_auto') | 0.000 – 1.000 | Classifier confidence score in the range \[0, 1] with three decimal places. Non-null only when source='classifier\_auto'; any other source value requires this to be null (DB CHECK invariant). Stored as DECIMAL(4,3) and returned as a string by MikroORM. | | created\_at | 🔒 system — Date | ✅ Yes | NOT NULL; set once on create | — | Timestamp of record creation. Set automatically on insert via MikroORM onCreate hook; never updated. | | updated\_at | 🔒 system — Date \| null | ⚪ No | Nullable on initial read before first update; set on create and update | — | Timestamp of the last update. Set on create and on every subsequent update via MikroORM onUpdate hook. | | deleted\_at | 🔒 system — Date \| null | ⚪ No | Nullable | — | Soft-delete timestamp. Null means the record is active. All live queries must filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ---------------------------- | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (OneToOne → Company) | ✅ Yes | The company this financial profile belongs to. Enforced unique at the DB level (company\_pk UNIQUE), guaranteeing at most one CompanyFinancial per Company. | | account\_receivable\_default | to-one (ManyToOne → LedgerAccount) | ⚪ No | Default ledger account applied to accounts-receivable postings for this company. Nullable; set by the chart-of-accounts classification pipeline. | | account\_payable\_default | to-one (ManyToOne → LedgerAccount) | ⚪ No | Default ledger account applied to accounts-payable postings for this company. Nullable; set by the chart-of-accounts classification pipeline. | ### System-computed * company\_financial\_id — generated via gen\_random\_uuid() on insert; unique public identifier * created\_at — set to new Date() on insert via MikroORM onCreate hook * updated\_at — set to new Date() on insert and on every update via MikroORM onUpdate hook * deleted\_at — set by the pipeline soft-delete path; null for active records * source + confidence — both fields are written exclusively by the chart-of-accounts classification pipeline (classifier\_auto), human review confirmation flow (classifier\_suggested\_human\_confirmed, human\_override), rule import (imported\_chart\_rule), or data migration (migration); never set via a user-facing PATCH * DB CHECK company\_financials\_confidence\_range — database enforces confidence ∈ \[0,1] when non-null * DB CHECK company\_financials\_source\_confidence\_invariant — database enforces that confidence is non-null iff source='classifier\_auto'; any pipeline transition clearing the auto-classifier must write confidence=null in the same statement ## Example ```json theme={null} { "data": { "type": "company_financial", "id": "b3e7c1a2-84fd-4f1e-9c20-3a77d5e82b01", "attributes": { "company_financial_id": "b3e7c1a2-84fd-4f1e-9c20-3a77d5e82b01", "source": "classifier_auto", "confidence": "0.921", "created_at": "2026-03-15T10:42:00.000Z", "updated_at": "2026-05-20T14:08:33.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "e1c4a7b0-1234-4abc-b000-99aabb001122" } }, "account_receivable_default": { "data": { "type": "ledger_account", "id": "d9f3a100-aaaa-4bbb-cccc-000011112222" } }, "account_payable_default": { "data": { "type": "ledger_account", "id": "f2e1b200-bbbb-4ccc-dddd-111122223333" } } } } } ``` Source: `apps/api/src/database/entities/CompanyFinancial.ts` · domain: financial-graph · tier: Supporting # CompanyMedia Source: https://docs.wellapp.ai/object-reference/company_media CompanyMedia is a soft-deleteable pivot (bridge) entity that links a Company to a Media asset CompanyMedia is a soft-deleteable pivot (bridge) entity that links a Company to a Media asset. It records when a media file (logo, avatar, or banner) was associated with a company and is the persistence mechanism for the "add/remove company media" mutations. The entity carries no scalar attributes of its own beyond timestamps; all descriptive content lives on the related Media row. Two compound indexes optimise the Hasura array-relationship traversals in both directions: `(company_pk, deleted_at)` for the forward pass and `(media_pk)` for the reverse pass. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyMedia | | Resource type (JSON:API `type`) | `company_media` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-media` | | Entity class | `CompanyMedia` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ---------- | | List | `GET /v1/company-media` | 🟡 Planned | | Retrieve | `GET /v1/company-media/{id}` | 🟡 Planned | | Create | `POST /v1/company-media` | 🟡 Planned | | Update | `PATCH /v1/company-media/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-media/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------ | -------- | ------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | datetime 🔒 system | ✅ Yes | Auto-set via onCreate hook; not nullable | — | Timestamp when the company–media association was created. Set automatically on insert; never updated. | | deleted\_at | datetime \| null | ⚪ No | Nullable. Non-null value soft-deletes the row; all queries must filter deleted\_at IS NULL. | — | Soft-delete sentinel. Set to the deletion timestamp when the media is unlinked from the company; null when the association is active. | ### Relationships | Name | Type | Required | Description | | ------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (ManyToOne) | ✅ Yes | The Company to which this media asset is attached. Indexed as part of the composite `(company_pk, deleted_at)` index for Hasura array-relationship traversals. Target entity: Company. | | media | to-one (ManyToOne) | ✅ Yes | The Media asset (logo, avatar, or banner) being linked to the company. Indexed on `(media_pk)` for the reverse array-relationship traversal. Target entity: Media. | ### System-computed * created\_at — set via MikroORM onCreate hook (new Date()); not writable by callers * deleted\_at — written by soft-delete mutations (useAddCompanyMedia / useDeleteCompanyMedia service layer); never set by the user directly via PATCH * pk — internal auto-increment integer primary key; never exposed in the public API. No public UUID (\*\_id) column exists on this entity — the entity is identified in the API by its relationship context (company\_id + media\_id pair), not by a standalone resource id * Indexes maintained by migrations: idx\_company\_media\_company\_deleted (company\_pk, deleted\_at) — hot-path for Hasura forward traversal; idx\_company\_media\_media (media\_pk) — hot-path for Hasura reverse traversal. Both created in Migration20260416000000\_hasura\_and\_service\_hot\_path\_indexes\_round4. ## Example ```json theme={null} { "data": { "type": "company_media", "id": "a3f1c2e4-8b7d-4a9f-bc12-0d5e6f7a8b9c", "attributes": { "created_at": "2025-11-14T09:32:00.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "e1d2f3a4-5b6c-7d8e-9f0a-1b2c3d4e5f60" } }, "media": { "data": { "type": "media", "id": "f9e8d7c6-b5a4-3c2d-1e0f-9a8b7c6d5e4f" } } } } } ``` Source: `apps/api/src/database/entities/CompanyMedia.ts` · domain: financial-graph · tier: Supporting # CompanyPhone Source: https://docs.wellapp.ai/object-reference/company_phones CompanyPhone is a soft-deletable pivot (bridge) entity that links a Company to a Phone record, carrying contact-channel metadata (`is_primary`, `is_verify`, `la CompanyPhone is a soft-deletable pivot (bridge) entity that links a Company to a Phone record, carrying contact-channel metadata (`is_primary`, `is_verify`, `label`). It enables a company to hold multiple phone numbers while designating exactly one primary via a partial unique index. The entity is written exclusively by the connector sync pipeline and enrichment flows; there is no user-facing PATCH route, and `company_phones` does not appear in `EDITABLE_FIELDS`. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyPhone | | Resource type (JSON:API `type`) | `company_phone` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-phones` | | Entity class | `CompanyPhone` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/company-phones` | 🟡 Planned | | Retrieve | `GET /v1/company-phones/{id}` | 🟡 Planned | | Create | `POST /v1/company-phones` | 🟡 Planned | | Update | `PATCH /v1/company-phones/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-phones/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | is\_primary | boolean | ✅ Yes | Partial unique index `uniq_company_phones_primary_company`: only one row per `company_pk` may have `is_primary = TRUE` where `deleted_at IS NULL` | true \| false | Marks this phone as the canonical primary phone for the parent company. Enforced as unique per company among non-deleted rows. | | is\_verify | boolean | ✅ Yes | None beyond NOT NULL | true \| false | Indicates whether this phone number has been verified (e.g., via an automated or manual verification step). | | label | string | ✅ Yes | varchar(255), NOT NULL | Free text (e.g. 'main', 'mobile', 'support', 'billing') | Human-readable tag for this phone association (e.g. 'main', 'billing'). Set by the ingestion pipeline from connector metadata. | | created\_at | 🔒 system — Date (timestamptz) | ✅ Yes | NOT NULL; set via `onCreate` hook | — | Timestamp of record creation. Set automatically on insert; never updated. | | deleted\_at | Date \| null (timestamptz) | ⚪ No | Nullable; soft-delete sentinel. Partial unique index and forward composite index both include this column. | — | Soft-delete timestamp. NULL means the record is active. Set by the pipeline on logical removal; never hard-deleted. | ### Relationships | Name | Type | Required | Description | | ------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (ManyToOne) | ✅ Yes | The Company this phone belongs to. FK `company_pk` references `core_api.companies.pk` (ON UPDATE CASCADE). Composite index `idx_company_phones_company_deleted` covers `(company_pk, deleted_at)` for forward array-relationship traversals. | | phone | to-one (ManyToOne) | ✅ Yes | The Phone atomic record that carries the actual phone number string. FK `phone_pk` references `core_api.phones.pk` (ON UPDATE CASCADE). Index `idx_company_phones_phone` covers `(phone_pk)` for reverse traversals. | ### System-computed * pk — auto-increment serial primary key (internal, never exposed via API) * created\_at — set by MikroORM onCreate hook to `new Date()` on insert; no onUpdate hook (column does not change after creation) * deleted\_at — null at creation; set to current timestamp by soft-delete logic in the pipeline or repository layer; never hard-deleted under normal operation * No `updated_at` column on this entity (audit trail is creation + soft-delete only) * No `*_id` UUID public identifier on CompanyPhone itself — the pivot is referenced via its parent Company and Phone IDs in the API; internal pk is used for joins only * Partial unique index `uniq_company_phones_primary_company` is enforced by the database engine on `(company_pk) WHERE deleted_at IS NULL AND is_primary IS TRUE` — only one active primary phone per company is permitted at the DB level * Composite index `idx_company_phones_company_deleted (company_pk, deleted_at)` and reverse index `idx_company_phones_phone (phone_pk)` are maintained automatically; added by Migration20260416100000 to cover the forward and reverse array-relationship traversal patterns identified via production Query Insights ## Example ```json theme={null} { "data": { "type": "company_phone", "id": "a3f7c821-91be-4d02-b56a-3e1234567890", "attributes": { "is_primary": true, "is_verify": false, "label": "main", "created_at": "2025-10-14T08:23:11.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "9e2b1c44-0001-4321-abcd-000000000001" } }, "phone": { "data": { "type": "phone", "id": "bb44f109-dead-beef-cafe-123456789abc" } } } } } ``` Source: `apps/api/src/database/entities/CompanyPhone.ts` · domain: financial-graph · tier: Supporting # CompanyRelation Source: https://docs.wellapp.ai/object-reference/company_relations CompanyRelation models a directed link between two Company entities, capturing structural or commercial relationships (parent/subsidiary hierarchy, supplier cha CompanyRelation models a directed link between two Company entities, capturing structural or commercial relationships (parent/subsidiary hierarchy, supplier chains, client networks). Each tuple records a source company, a target company, and the typed nature of the link. The entity is workspace-adjacent — it is scoped through its Company endpoints rather than carrying a direct workspace FK. Soft-delete support is present via `deleted_at`; there is no `updated_at` column on this entity. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | CompanyRelation | | Resource type (JSON:API `type`) | `company_relation` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-relations` | | Entity class | `CompanyRelation` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------- | ---------- | | List | `GET /v1/company-relations` | 🟡 Planned | | Retrieve | `GET /v1/company-relations/{id}` | 🟡 Planned | | Create | `POST /v1/company-relations` | 🟡 Planned | | Update | `PATCH /v1/company-relations/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-relations/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------------------ | -------- | ----------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | link\_type | enum (link\_type\_enum — native PostgreSQL enum) | ✅ Yes | NOT NULL; native enum — invalid values rejected at DB level | parent \| supplier \| client | Classifies the directed relationship from source\_company to target\_company. Stored as a native PostgreSQL enum `core_api.link_type_enum`. | | created\_at | 🔒 system — Date (timestamptz) | ✅ Yes | NOT NULL; immutable after insert | — | Timestamp set once at row creation via MikroORM `onCreate` hook. No `updated_at` exists on this entity. | | deleted\_at | Date \| null (timestamptz) | ⚪ No | NULLABLE | — | Soft-delete sentinel. NULL means the relation is active; a non-null timestamp marks it as logically deleted. All queries must filter `deleted_at IS NULL` per platform convention. | ### Relationships | Name | Type | Required | Description | | --------------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | source\_company | to-one (ManyToOne) | ✅ Yes | The originating Company in the directed relationship (e.g. the subsidiary, the buyer, or the child entity). FK column: `source_company_pk` → `core_api.companies.pk`. A partial index on `source_company_pk` was added in Migration20260504120000 for hot-path query performance. Note: original migration used a typo column `souce_company_pk`; corrected to `source_company_pk` in Migration20251126135042. | | target\_company | to-one (ManyToOne) | ✅ Yes | The destination Company in the directed relationship (e.g. the parent, the supplier, or the client). FK column: `target_company_pk` → `core_api.companies.pk`. A partial index on `target_company_pk` was added in Migration20260504120000. | ### System-computed * created\_at — set to `new Date()` via MikroORM `onCreate` hook; never written by the caller * deleted\_at — set by soft-delete logic; never passed in on creation * pk — auto-increment serial primary key, internal only; never exposed in the public API * source\_company\_pk / target\_company\_pk — FK integers resolved from the ManyToOne relations; the public API surfaces these as relationship objects, not raw integers ## Example ```json theme={null} { "data": { "type": "company_relation", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "link_type": "supplier", "created_at": "2025-09-12T08:30:00.000Z", "deleted_at": null }, "relationships": { "source_company": { "data": { "type": "company", "id": "11111111-aaaa-bbbb-cccc-dddddddddddd" } }, "target_company": { "data": { "type": "company", "id": "22222222-aaaa-bbbb-cccc-eeeeeeeeeeee" } } } } } ``` Source: `apps/api/src/database/entities/CompanyRelation.ts` · domain: financial-graph · tier: Supporting # CompanyWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/company_workspace_connectors CompanyWorkspaceConnector is a per-row provenance junction table that records the relationship between a `Company` entity and a `WorkspaceConnector` instance, d CompanyWorkspaceConnector is a per-row provenance junction table that records the relationship between a `Company` entity and a `WorkspaceConnector` instance, discriminated by a `direction` field (`input` for data pulled from the connector, `output` for data pushed to it). It mirrors the `document_workspace_connectors` pattern and was introduced in W19 to replace a rejected single `ManyToOne` shape. Tenant scope is inherited indirectly through the referenced `Company.workspace` and `WorkspaceConnector.workspace` — there is deliberately no direct `workspace_pk` column on the junction itself. Multiple rows are permitted per (company, connector, direction) triple; deduplication logic is deferred to a future iteration. | Naming | Value | | ------------------------------- | ---------------------------------- | | Object | CompanyWorkspaceConnector | | Resource type (JSON:API `type`) | `company_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/company-workspace-connectors` | | Entity class | `CompanyWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------------------- | ---------- | | List | `GET /v1/company-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/company-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/company-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/company-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/company-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | direction | 🔒 system — enum (direction\_enum) | ✅ Yes | Native Postgres enum `core_api.direction_enum`; CHECK implicit via enum type; NOT NULL | "input" \| "output" | Discriminates the data-flow direction: `input` = record was pulled from this connector (source provenance); `output` = record was pushed to this connector (distribution provenance). | | created\_at | 🔒 system — datetime (timestamptz) | ✅ Yes | DEFAULT now(); set once on INSERT via MikroORM `onCreate` hook; NOT NULL | — | Timestamp when the provenance row was first created. Stamped automatically by the MikroORM lifecycle hook; never set by caller. | | updated\_at | 🔒 system — datetime (timestamptz) | ⚪ No | Nullable; set on INSERT and refreshed on every UPDATE via MikroORM `onCreate`/`onUpdate` hooks | — | Timestamp of the last mutation to this row. Managed automatically; null only if the row was never updated after creation in edge cases. | | deleted\_at | 🔒 system — datetime (timestamptz) | ⚪ No | Nullable; soft-delete sentinel; column-level nullable=true | — | Soft-delete timestamp. When non-null the row is logically deleted and excluded from active queries. Set by the connector sync orchestrator on revocation or re-link deduplication; never set directly by users. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | company | to-one (ManyToOne) | ✅ Yes | The Company entity whose sync provenance this row records. Foreign key `company_pk` → `core_api.companies.pk` ON UPDATE CASCADE. Tenant isolation is resolved through this relationship (Company carries `workspace_pk`). Declared on this entity as `@ManyToOne(() => Company)`. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The WorkspaceConnector instance that sourced (direction=input) or received (direction=output) this company record. Foreign key `workspace_connector_pk` → `core_api.workspace_connectors.pk` ON UPDATE CASCADE. Declared on this entity as `@ManyToOne(() => WorkspaceConnector)`. | ### System-computed * pk — serial primary key, auto-incremented by Postgres on INSERT; never exposed in API responses. * created\_at — stamped once at INSERT time via MikroORM onCreate: () => new Date(); no caller input accepted. * updated\_at — stamped at INSERT and refreshed on every UPDATE via MikroORM onUpdate: () => new Date(). * deleted\_at — soft-delete sentinel; set by the connector sync pipeline (not user-initiated); rows with non-null deleted\_at are filtered out of active queries. * No workspace\_pk column by design — tenant scope is inherited through the Company and WorkspaceConnector parent relationships; Hasura RLS traverses these relationships for row-level isolation. * No unique constraint on (company\_pk, workspace\_connector\_pk, direction) — duplicate rows are permitted by design; deduplication is deferred to a future iteration (stated explicitly in migration comments). * No external\_id field — connector-pointer provenance only; external-object identity tracking is iteration 4+ work. * Rows are created exclusively by the connector sync orchestrator during entity persistence; there is no user-facing resource PATCH route for this junction. ## Example ```json theme={null} { "data": { "type": "company_workspace_connector", "id": "3f8c2a1d-0e45-4b7f-9c3e-1a2b3c4d5e6f", "attributes": { "direction": "input", "created_at": "2026-05-10T14:32:00.000Z", "updated_at": "2026-05-10T14:32:00.000Z", "deleted_at": null }, "relationships": { "company": { "data": { "type": "company", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } } } } } ``` Source: `apps/api/src/database/entities/CompanyWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # ConnectorFilter Source: https://docs.wellapp.ai/object-reference/connector_filters ConnectorFilter defines the document-routing rules applied to output connectors during sync orchestration ConnectorFilter defines the document-routing rules applied to output connectors during sync orchestration. Each row is either a template filter (template=true, workspace=null — a platform-managed default shared across all workspaces for a given connector type) or a custom filter (template=false, workspace scoped — workspace-specific overrides). The `config` column carries a Hasura WHERE-clause JSONB predicate that gates which documents flow to the target connector, while `natural_language` provides a human-readable description for display in the workflow editor. ConnectorFilters are written exclusively by the seed pipeline (mikro:seed:connector-filters command and its migration equivalent) and surfaced read-only via the workspace-connectors JSON:API as a sideloaded included resource. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ConnectorFilter | | Resource type (JSON:API `type`) | `connector_filter` | | Collection / records root | — (not a records root) | | REST base | `/v1/connector-filters` | | Entity class | `ConnectorFilter` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------- | ---------- | | List | `GET /v1/connector-filters` | 🟡 Planned | | Retrieve | `GET /v1/connector-filters/{id}` | 🟡 Planned | | Create | `POST /v1/connector-filters` | 🟡 Planned | | Update | `PATCH /v1/connector-filters/{id}` | 🟡 Planned | | Delete | `DELETE /v1/connector-filters/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | --------------------- | -------------------- | -------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | connector\_filter\_id | string (UUID) | ✅ Yes | unique | Any valid UUID v4 | Public stable identifier for this filter. Generated by gen\_random\_uuid() on insert. Used as the JSON:API `id` field. | | template | boolean | ✅ Yes | default false | true \| false | Discriminator for filter kind. true = platform-managed template filter (workspace=null, shared across all workspaces for the connector type). false = workspace-specific custom filter. | | config | object (JSONB) | ✅ Yes | NOT NULL; no fixed schema — shape is dynamic per connector | Any valid Hasura WHERE clause JSON object, e.g. \{ "document\_type": \{ "\_in": \["380", "381"] } } | Filter predicate expressed as a Hasura WHERE-clause JSONB object. Gates which documents are routed to the output connector during sync. Shape mirrors Hasura boolean expression syntax (field → operator → value). | | natural\_language | string (text) | ✅ Yes | NOT NULL; no length limit (text column) | Free text | Human-readable description of what the filter does. Displayed in the workflow editor as the suggested default rule copy for the output connector. | | created\_at | 🔒 system — datetime | ✅ Yes | timestamptz(6); NOT NULL | ISO 8601 UTC datetime | Timestamp set automatically on row creation via MikroORM onCreate hook. Never updated after insert. | | updated\_at | 🔒 system — datetime | ⚪ No | timestamptz(6); nullable | ISO 8601 UTC datetime or null | Timestamp refreshed automatically on every row update via MikroORM onUpdate hook. NULL until the first update. | | deleted\_at | 🔒 system — datetime | ⚪ No | timestamptz(6); nullable | ISO 8601 UTC datetime or null | Soft-delete timestamp. NULL means the filter is active. When set, the filter is excluded from all sync queries. Set by the seed pipeline on logical removal. | ### Relationships | Name | Type | Required | Description | | --------- | ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | connector | to-one (ManyToOne) | Yes — NOT NULL FK | The Connector (provider definition) this filter is attached to. The connector FK is non-nullable and cascades on update. One connector may have at most one template filter (identity = connector\_pk + template=true + workspace=null + deleted\_at=null per seed idempotency contract) and any number of custom workspace filters. | | workspace | to-one (ManyToOne, nullable) | No — nullable FK (ON DELETE SET NULL) | The Workspace this custom filter belongs to. NULL for template filters (template=true). Set for workspace-specific custom filters (template=false). The FK cascades on update and sets null on workspace deletion. | ### System-computed * connector\_filter\_id — generated by gen\_random\_uuid() database default on INSERT; unique constraint ensures no collision * created\_at — set by MikroORM onCreate: () => new Date() hook; never modified after creation * updated\_at — set by both MikroORM onCreate and onUpdate hooks; reflects last modification time by the seed/migration pipeline * deleted\_at — soft-delete sentinel; set by the seed pipeline (syncConnectorFilters) when a filter is logically removed; never modified by user API calls * Template filter identity — the seed pipeline enforces a one-template-per-output-connector invariant: identity = (connector.pk, template=true, workspace=null, deleted\_at=null); subsequent seed runs update config and natural\_language in place rather than inserting duplicates * Indexes — idx\_connector\_filters\_workspace\_deleted (workspace, deleted\_at) for Hasura permission filter hot path; idx\_connector\_filters\_connector\_workspace (connector, workspace, template) for sync orchestration lookup; both created by Migration20260416000000\_hasura\_and\_service\_hot\_path\_indexes\_round4 ## Example ```json theme={null} { "data": { "type": "connector_filter", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "template": true, "config": { "document_type": { "_in": ["380", "381", "383", "384", "325", "326", "385", "386"] } }, "natural_language": "Routes invoices, credit notes, and debit notes (UN/CEFACT document type codes 380, 381, 383, 384, 325, 326, 385, 386).", "created_at": "2025-12-02T19:26:37.000Z", "updated_at": "2026-04-14T16:00:00.000Z" } } } ``` Source: `apps/api/src/database/entities/ConnectorFilter.ts` · domain: ingestion · tier: Infrastructure # ConnectorMapping Source: https://docs.wellapp.ai/object-reference/connector_mappings A `ConnectorMapping` row is the compiled JSONata expression that the connector sync pipeline uses to transform one MCP tool's response payload into one of twelv A `ConnectorMapping` row is the compiled JSONata expression that the connector sync pipeline uses to transform one MCP tool's response payload into one of twelve Well financial-graph entity types (company, invoice, transaction, account, etc.). One row is produced per unique `(workspace_connector, target_model, tool_name)` triple; the triple is enforced by a partial unique index on non-deleted rows. The record is entirely pipeline-owned: it is created, regenerated, and soft-deleted by `SyncConfigService` during each MCP sync cycle, and is never written by a user. It is associated upward to a `WorkspaceConnector` (the authenticated provider instance) and to a `Workspace`. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ConnectorMapping | | Resource type (JSON:API `type`) | `connector_mapping` | | Collection / records root | — (not a records root) | | REST base | `/v1/connector-mappings` | | Entity class | `ConnectorMapping` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------ | ---------- | | List | `GET /v1/connector-mappings` | 🟡 Planned | | Retrieve | `GET /v1/connector-mappings/{id}` | 🟡 Planned | | Create | `POST /v1/connector-mappings` | 🟡 Planned | | Update | `PATCH /v1/connector-mappings/{id}` | 🟡 Planned | | Delete | `DELETE /v1/connector-mappings/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------------- | -------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | connector\_mapping\_id | 🔒 system — UUID string | ✅ Yes | unique | UUID v4 | Public stable identifier for the mapping row. Generated via gen\_random\_uuid() at insert time. | | target\_model | 🔒 system — enum (target\_model\_enum) | ✅ Yes | native Postgres enum type target\_model\_enum; NOT NULL | company \| people \| invoice \| transaction \| account \| ledger\_account \| journal \| journal\_entry \| email \| payment\_means \| invoice\_transaction \| document | The Well financial-graph entity type this mapping expression produces. Part of the partial-unique triple (workspace\_connector\_pk, target\_model, tool\_name) WHERE deleted\_at IS NULL. | | expression | 🔒 system — text | ✅ Yes | NOT NULL; text (unbounded) | — | JSONata expression produced by the structured-slot-decision jury pipeline (SyncConfigService). Transforms a single MCP tool response payload into an array of Well entities matching target\_model. Never written directly by a user. | | user\_instructions | 🔒 system — text | ⚪ No | nullable | — | Optional free-text hints stored alongside the mapping at generation time to guide re-generation prompts. Written by the sync pipeline, not directly by end users. | | schema\_fingerprint | 🔒 system — varchar(128) | ⚪ No | nullable; max length 128 | — | Hash of the provider tool's schema at the time this mapping was generated. Compared on subsequent syncs via compareSchemaFingerprints(); a 'drift' result triggers needs\_regeneration = true. | | schema\_key\_paths | 🔒 system — jsonb (string\[]) | ⚪ No | nullable; jsonb | — | Ordered list of dotted key-paths extracted from the provider schema sample used during jury generation. Used by the panelists for vocab-bound source\_path validation. | | needs\_regeneration | 🔒 system — boolean | ✅ Yes | NOT NULL; default false | true \| false | Flag set to true by the sync pipeline when schema drift is detected or last\_persist\_count is 0 after a sync. When true, the next sync attempt regenerates the JSONata expression before executing. | | tool\_name | 🔒 system — text | ✅ Yes | NOT NULL; part of partial-unique index (workspace\_connector\_pk, target\_model, tool\_name) WHERE deleted\_at IS NULL | — | Name of the MCP tool whose response this mapping expression consumes. Added in Migration20260420100000; rows pre-dating multi-tool support were backfilled with '**legacy**'. | | tool\_args | 🔒 system — jsonb | ⚪ No | nullable; CHECK pg\_column\_size(tool\_args) \< 65536 (64 KB); added in Migration20260420100000 | — | Tool invocation arguments captured at AI tool-selection time. Rehydrated by the sync orchestrator to replay the exact MCP call. Read-only; never mutated post-creation. | | selected\_at | 🔒 system — timestamptz | ⚪ No | nullable; timestamptz | — | Timestamp recorded when the AI tool-selection phase picked this tool. Serves as an anchor for TTL-based re-selection logic in the sync pipeline. | | audit\_trail | 🔒 system — jsonb | ⚪ No | nullable; jsonb | — | Latest jury-run audit verdict for this mapping row. Written by JuryService after each jury run. Shape: \{ jury\_name, panelists\[], judge\_reasoning, disagreement\_score, duration\_ms, synthesized? }. Null on legacy rows predating the structured pipeline. | | pagination\_spec | 🔒 system — jsonb (ToolPagination) | ⚪ No | nullable; jsonb; added in Migration20260513110000 | — | LLM-discovered pagination specification for the tool. Null means single-page tool (no pagination). Discovered at mapping-generation time using the same schema sample as the JSONata jury. Shape is ToolPagination from services/mcp/pagination.types. | | last\_persist\_count | 🔒 system — int | ⚪ No | nullable; int | — | Count of entities successfully persisted in the most recent sync that used this mapping. Stamped by SyncConfigService.recordPersistCount after each sync. Zero triggers auto-regeneration (needs\_regeneration = true) on the next sync. Non-zero enables the regression-revert guard. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL; set by onCreate hook | — | Row creation timestamp. Set once by the MikroORM onCreate lifecycle hook; never updated. | | updated\_at | 🔒 system — timestamptz | ⚪ No | nullable; set by onUpdate hook | — | Row last-modification timestamp. Set by the MikroORM onUpdate hook on every flush. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | nullable | — | Soft-delete timestamp. When non-null the row is logically deleted and excluded from the partial-unique index on (workspace\_connector\_pk, target\_model, tool\_name). Allows a replacement row to be inserted under the same triple after re-generation. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The authenticated provider instance (WorkspaceConnector) this mapping belongs to. FK column: workspace\_connector\_pk. All active mappings for a connector are soft-deleted when the connector is disconnected. | | workspace | to-one (ManyToOne) | ✅ Yes | The owning workspace. FK column: workspace\_pk. Used for tenant scoping — every query against connector\_mappings filters by workspace\_pk. | ### System-computed * connector\_mapping\_id — generated via gen\_random\_uuid() at insert; unique constraint enforced at DB level * created\_at — set once by MikroORM onCreate lifecycle hook * updated\_at — set on every flush by MikroORM onUpdate lifecycle hook * deleted\_at — soft-delete; when set, the row is excluded from the partial unique index on (workspace\_connector\_pk, target\_model, tool\_name) WHERE deleted\_at IS NULL, allowing a fresh row to be inserted under the same triple * needs\_regeneration — auto-flipped to true by SyncConfigService when schema drift ('drift' result from compareSchemaFingerprints) is detected or last\_persist\_count == 0 after a sync * last\_persist\_count — stamped by SyncConfigService.recordPersistCount after each sync completes; zero value is the signal for automatic re-generation on the next sync cycle * expression — produced by the structured slot-decision jury pipeline (3-panelist OpenAI strict json\_schema → reconcileJuryDecisions → mergeSlotDecisions with YAML-pinned slots → compileSlotDecisionsToJsonata); regenerated when needs\_regeneration is true; subject to three-tier graceful degradation (reuse prior working expression → legacy jury → hard fail) * schema\_fingerprint — computed from the provider tool schema sample at generation time; compareSchemaFingerprints returns identical | additive\_only | drift; only 'drift' triggers regeneration * schema\_key\_paths — extracted from the provider schema sample during jury generation; used for vocab-bound source\_path validation by jury panelists * audit\_trail — written by JuryService after each jury run; null on rows predating the structured pipeline * pagination\_spec — discovered by the same schema-sample pass as the JSONata jury at generation time; null means single-page tool * tool\_args — captured once at AI tool-selection time (selected\_at anchor); never mutated post-creation; bounded to 64 KB by a CHECK constraint (Migration20260420100000) * Partial-unique index connector\_mappings\_wc\_target\_tool\_unique on (workspace\_connector\_pk, target\_model, tool\_name) WHERE deleted\_at IS NULL — enforces one active mapping per tool-per-target-per-connector; added in Migration20260420100000 replacing the legacy (workspace\_connector\_pk, target\_model) unique index ## Example ```json theme={null} { "data": { "type": "connector_mapping", "id": "a3f8c1d2-7b4e-4f2a-9c01-3e5d8f2a6b19", "attributes": { "connector_mapping_id": "a3f8c1d2-7b4e-4f2a-9c01-3e5d8f2a6b19", "target_model": "invoice", "expression": "$map(result.invoices, function($v) { { 'reference_number': $v.invoice_number, 'grand_total': $number($v.total_amount), 'issue_date': $v.created_at } })", "user_instructions": "Map invoice totals from EUR cents to decimal euros.", "schema_fingerprint": "sha256:3a9c1e7f2b4d", "schema_key_paths": ["result.invoices[].invoice_number", "result.invoices[].total_amount", "result.invoices[].created_at"], "needs_regeneration": false, "tool_name": "pennylane_list_invoices", "tool_args": { "page": 1, "per_page": 100 }, "selected_at": "2026-05-01T14:32:00.000Z", "audit_trail": { "jury_name": "slot-decision-v2", "panelists": ["gpt-5.4-0.4", "gpt-5.4-0.7", "gpt-5.4-1.0"], "disagreement_score": 0.12, "duration_ms": 4320, "synthesized": false }, "pagination_spec": { "type": "page_number", "page_param": "page", "per_page_param": "per_page", "per_page": 100 }, "last_persist_count": 87, "created_at": "2026-04-20T10:00:00.000Z", "updated_at": "2026-05-01T14:32:05.000Z", "deleted_at": null }, "relationships": { "workspace_connector": { "data": { "type": "workspace_connector", "id": "c7d4a2f1-1e3b-4a2c-8f01-9b2e7c3d5a11" } }, "workspace": { "data": { "type": "workspace", "id": "9f3e2b1a-4c7d-4f8e-b3a1-2c5d9e7f3b08" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/ConnectorMapping.ts` · domain: ingestion · tier: Infrastructure # ConnectorSyncDiagnostic Source: https://docs.wellapp.ai/object-reference/connector_sync_diagnostics ConnectorSyncDiagnostic is an infrastructure-layer audit log that captures structured failure and informational signals emitted by SyncConfigService during MCP ConnectorSyncDiagnostic is an infrastructure-layer audit log that captures structured failure and informational signals emitted by SyncConfigService during MCP connector sync execution. Each row records the exact tool invoked, the target entity model being synced, the reason the sync event was notable (a failure class or an informational signal such as jury telemetry or pipeline degradation), and a free-text detail message. Rows are tenant-scoped to a Workspace and linked to the specific WorkspaceConnector that triggered the sync. Retention is opportunistic: ConnectorSyncDiagnosticService.write() prunes rows older than 30 days for the current connector on approximately 1-in-20 writes — no separate scheduled job is needed. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ConnectorSyncDiagnostic | | Resource type (JSON:API `type`) | `connector_sync_diagnostic` | | Collection / records root | — (not a records root) | | REST base | `/v1/connector-sync-diagnostics` | | Entity class | `ConnectorSyncDiagnostic` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------------- | ---------- | | List | `GET /v1/connector-sync-diagnostics` | 🟡 Planned | | Retrieve | `GET /v1/connector-sync-diagnostics/{id}` | 🟡 Planned | | Create | `POST /v1/connector-sync-diagnostics` | 🟡 Planned | | Update | `PATCH /v1/connector-sync-diagnostics/{id}` | 🟡 Planned | | Delete | `DELETE /v1/connector-sync-diagnostics/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------- | ------------------------------------------------------------------------- | -------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | diagnostic\_id | string (UUID) | ✅ Yes | unique; default gen\_random\_uuid() | — | Public-facing UUID for this diagnostic row. Generated by the database on insert; never set by the caller. | | target\_model | enum (TargetModelEnum) — native DB enum target\_model\_enum | ✅ Yes | NOT NULL; values are the native DB enum | company \| people \| invoice \| transaction \| account \| ledger\_account \| journal \| journal\_entry \| email \| payment\_means \| invoice\_transaction \| document | The Well entity model the connector was attempting to sync when this diagnostic was recorded. | | tool\_name | string (text) | ⚪ No | nullable | — | Name of the MCP tool that was selected or attempted (e.g. pennylane\_get\_invoices). Null when the failure occurred before tool selection. | | reason | enum (SyncFailureReasonEnum) — native DB enum sync\_failure\_reason\_enum | ✅ Yes | NOT NULL; values are the native DB enum | no\_suitable\_tool \| tool\_not\_found \| auth\_scope\_insufficient \| tool\_execution\_failed \| empty\_result \| jsonata\_generation\_failed \| schema\_drift\_detected \| jury\_run \| jury\_partial \| jury\_disagreement \| jury\_quorum\_lost \| blueprint\_persistence\_failed \| mapped\_entities\_dropped \| mapped\_entities\_partial\_drop \| fk\_parent\_missing \| entity\_persist\_failed \| sync\_fk\_dag\_cycle \| regression\_reverted \| target\_skipped \| structured\_pipeline\_degraded \| structured\_pipeline\_fell\_back\_to\_legacy \| mapping\_generation\_hard\_fail \| tool\_selection\_pinned | Taxonomy code classifying the sync event. Values in INFORMATIONAL\_REASONS (jury\_run, jury\_disagreement, schema\_drift\_detected, target\_skipped, structured\_pipeline\_degraded, structured\_pipeline\_fell\_back\_to\_legacy, tool\_selection\_pinned) are non-blocking signals; all others are FAILURE class. | | http\_status | integer | ⚪ No | nullable | — | HTTP status code returned by the upstream MCP tool call, when available. Absent for failures that occur before an HTTP round-trip (e.g. mapping generation failures). | | detail | string (text) | ✅ Yes | NOT NULL | — | Human-readable description of the failure or informational event. Written by SyncConfigService at the point of detection; not machine-parsed. | | occurred\_at | timestamp with time zone | ✅ Yes | NOT NULL; default now(); set by onCreate hook | — | Wall-clock timestamp at which the sync event occurred. Set once on insert; not updated. Distinct from created\_at to support future back-dated ingestion of diagnostic events. | | created\_at | timestamp with time zone | ✅ Yes | NOT NULL; default now(); set by onCreate hook | — | Row creation timestamp. Set once by the MikroORM onCreate hook. | | updated\_at | timestamp with time zone | ⚪ No | set by onCreate and onUpdate hooks | — | Row last-updated timestamp. Maintained automatically by MikroORM. | | deleted\_at | timestamp with time zone | ⚪ No | nullable | — | Soft-delete timestamp. When non-null the row is logically deleted. Pruning in ConnectorSyncDiagnosticService.write() hard-deletes rows older than 30 days for the current connector. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The tenant workspace that owns this diagnostic. FK column workspace\_pk references core\_api.workspaces(pk) with ON UPDATE CASCADE ON DELETE CASCADE. Used to scope diagnostic queries per tenant. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The specific WorkspaceConnector instance (provider + auth config) whose sync run produced this diagnostic. FK column workspace\_connector\_pk references core\_api.workspace\_connectors(pk) with ON UPDATE CASCADE ON DELETE CASCADE. Retention pruning is scoped to this connector's rows. | ### System-computed * diagnostic\_id — generated by gen\_random\_uuid() at DB level; never supplied by the caller * occurred\_at — set to now() by MikroORM onCreate hook at insert time * created\_at — set to now() by MikroORM onCreate hook at insert time * updated\_at — set to now() by MikroORM onCreate and onUpdate hooks * deleted\_at — set by application-level soft-delete; also used as the threshold for opportunistic hard-delete pruning (rows older than 30 days for the current connector are pruned inside ConnectorSyncDiagnosticService.write() on approximately 1-in-20 writes) * pk (internal) — serial auto-increment; not exposed via the public API ## Example ```json theme={null} { "data": { "type": "connector_sync_diagnostic", "id": "a3f7e812-0c2e-4d1b-9b4a-001122334455", "attributes": { "diagnostic_id": "a3f7e812-0c2e-4d1b-9b4a-001122334455", "target_model": "invoice", "tool_name": "pennylane_get_invoices", "reason": "mapped_entities_dropped", "http_status": 200, "detail": "Validator rejected 12/12 mapped entities: required field 'issue_date' null after JSONata mapping for tool pennylane_get_invoices", "occurred_at": "2026-06-01T14:32:07.000Z", "created_at": "2026-06-01T14:32:07.000Z", "updated_at": "2026-06-01T14:32:07.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "wsp_abc123" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "wsc_xyz789" } } } } } ``` Source: `apps/api/src/database/entities/ConnectorSyncDiagnostic.ts` · domain: ingestion · tier: Infrastructure # Contribution Source: https://docs.wellapp.ai/object-reference/contributions A `Contribution` is an audit and progress-tracking record for a user-initiated blueprint contribution run — typically triggered when a workspace user submits or A `Contribution` is an audit and progress-tracking record for a user-initiated blueprint contribution run — typically triggered when a workspace user submits or publishes a new provider blueprint (e.g. building a connector for "amazon" or "qonto"). It captures the run lifecycle (status, started/ended timestamps, progress payload), the provider being built (`provider_slug`), the originating page (`source_url`), and the structured input data (`addresses`, `country`, `flow_json`). Each contribution is workspace-scoped via a nullable FK to `Workspace` that is stamped at creation time to prevent cross-tenant drift. The entity is written exclusively by the blueprint contribution pipeline (POST /v1/contribution) and updated by dedicated PATCH handlers for appending steps and transitioning status; no fields are user-editable via the records-table PATCH surface. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Contribution | | Resource type (JSON:API `type`) | `contribution` | | Collection / records root | — (not a records root) | | REST base | `/v1/contributions` | | Entity class | `Contribution` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ---------- | | List | `GET /v1/contributions` | 🟡 Planned | | Retrieve | `GET /v1/contributions/{id}` | 🟡 Planned | | Create | `POST /v1/contributions` | 🟡 Planned | | Update | `PATCH /v1/contributions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/contributions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | contribution\_id | string (UUID) 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() on INSERT | — | Public-facing UUID identifier for the contribution. Never editable after creation. | | name | string | ✅ Yes | NOT NULL (no explicit length limit in entity) | — | Human-readable label for the contribution run, typically set by the client at creation time. | | addresses | json (array of objects) | ✅ Yes | NOT NULL; stored as JSONB array | — | Array of address objects captured during the contribution run. Schema is open-ended (Record\\[]). | | country | string\[] (array) | ✅ Yes | NOT NULL; stored as Postgres text\[] | — | ISO-3166-1 alpha-2 country codes relevant to this contribution. Populated by the extension at run start. | | status | string (enum ContributionStatus) | ✅ Yes | NOT NULL; default 'initial'; stored as plain string (not native Postgres enum) | initial \| processing \| cancel \| done | Lifecycle status of the contribution run. Transitions are driven by PATCH /:id/status — not by the records-table PATCH surface. | | flow\_json | json (object) | ✅ Yes | NOT NULL; stored as JSONB | — | Structured blueprint-flow payload recorded during the contribution. Contains the sequence of steps (navigate, interact, extract) that the extension executed. | | ai\_historic | json (object) | ⚪ No | nullable | — | AI processing history for the contribution — model used, phases completed, token usage, and intermediate LLM outputs. Populated by the blueprint analyzer pipeline. | | provider\_slug | string | ⚪ No | nullable; max length 255 | — | Provider identifier (e.g. 'amazon', 'qonto') for the new blueprint being built by this contribution. Set by the extension at run start. | | source\_url | string | ⚪ No | nullable; max length 2048 | — | The page the user was on in the Well app when they initiated the contribution run. Useful for support and replay context. | | started\_at | datetime | ⚪ No | nullable; timestamptz | — | Timestamp when the extension began executing the contribution run. Set by PATCH /:id/status or the pipeline on first step. | | ended\_at | datetime | ⚪ No | nullable; timestamptz | — | Timestamp when the contribution run completed (status = done or cancel). Set by the pipeline or PATCH /:id/status. | | progress | json (ContributionProgress object) | ⚪ No | nullable; stored as JSONB | — | Live progress snapshot for the in-flight run. Fields: total (int), current (int), inserted (int), status (string), message (string), error (string). All sub-fields are optional. | | created\_at | datetime 🔒 system | ✅ Yes | NOT NULL; set once on INSERT via onCreate hook | — | Row creation timestamp. Stamped automatically by the MikroORM lifecycle hook; never supplied by the client. | | updated\_at | datetime 🔒 system | ✅ Yes | NOT NULL; updated on every write via onUpdate hook | — | Last-modified timestamp. Refreshed automatically by the MikroORM lifecycle hook on every flush. | | deleted\_at | datetime 🔒 system | ⚪ No | nullable; soft-delete sentinel | — | Soft-delete timestamp. NULL means the record is active. When set, the row is excluded from all workspace-scoped Hasura queries via the user-role RLS filter. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | No | The Workspace this contribution was initiated from. Nullable FK (`workspace_pk`) stamped at creation to prevent cross-tenant drift. Orphan rows whose workspace no longer resolves are excluded from scoped queries automatically by Hasura RLS. Target: `core_api.workspaces`. | ### System-computed * contribution\_id: generated by gen\_random\_uuid() on INSERT — never supplied by the client * created\_at: set once on INSERT by MikroORM onCreate hook (new Date()) * updated\_at: refreshed on every flush by MikroORM onUpdate hook (new Date()) * deleted\_at: soft-delete sentinel — NULL on active rows; set by a soft-delete operation, never by the client directly * status default: 'initial' set by the entity class initializer and the DB column default — no application code needs to supply it on creation * workspace\_pk: stamped at creation by ContributionService/route handler from req.workspace (JWT-resolved) so in-flight runs are always correctly tenant-scoped; nullable on pre-migration legacy rows ## Example ```json theme={null} { "data": { "type": "contribution", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "name": "Amazon blueprint contribution — 2026-05-01", "addresses": [ { "street": "410 Terry Ave N", "city": "Seattle", "country": "US" } ], "country": ["US"], "status": "done", "flow_json": { "steps": [ { "type": "navigate", "url": "https://www.amazon.com/gp/b2b/manage" } ] }, "ai_historic": { "model": "gemini-3-flash-preview", "phases": ["navigation", "exploration", "generation"], "token_usage": 12400 }, "provider_slug": "amazon", "source_url": "https://app.wellapp.ai/workspaces/ws_abc/settings/connectors", "started_at": "2026-05-01T09:12:00.000Z", "ended_at": "2026-05-01T09:14:35.000Z", "progress": { "total": 8, "current": 8, "inserted": 8, "status": "complete", "message": "Blueprint published successfully" }, "created_at": "2026-05-01T09:11:55.000Z", "updated_at": "2026-05-01T09:14:35.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "9f3a2b1c-0000-4abc-8def-000000000001" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/Contribution.ts` · domain: intelligence · tier: Activity # Custom Column Value Source: https://docs.wellapp.ai/object-reference/custom_column_values A Custom Column Value is one computed-or-entered cell: the value of a Custom Column for a single record A Custom Column Value is one computed-or-entered cell: the value of a Custom Column for a single record. It is joined to its target record by the record's public UUID (record\_id) rather than a typed foreign key, which is what lets the same custom-column mechanism work across every records root. AI-computed values are written by the enrichment worker as structured JSON. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Custom Column Value | | Resource type (JSON:API `type`) | `custom_column_value` | | Collection / records root | — (not a records root) | | REST base | `/v1/custom-column-values` | | Entity class | `CustomColumnValue` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------- | ---------- | | List | `GET /v1/custom-column-values` | 🟡 Planned | | Retrieve | `GET /v1/custom-column-values/{id}` | 🟡 Planned | | Create | `POST /v1/custom-column-values` | 🟡 Planned | | Update | `PATCH /v1/custom-column-values/{id}` | 🟡 Planned | | Delete | `DELETE /v1/custom-column-values/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | ------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | value\_id | string, UUID, 🔒 system | ✅ Yes | unique; gen\_random\_uuid() | — | Public identifier of the value row. | | record\_id | string (UUID) | ✅ Yes | max 255 chars; indexed with custom\_column | — | Public UUID of the target record (e.g. a company\_id). NOT a typed FK — a root-agnostic join key, so one mechanism serves all roots. | | root | string | ✅ Yes | max 50 chars; denormalized | A records root | The records root of the target record (denormalized from the column for fast filtering). | | value | jsonb | ⚪ No | nullable | — | The cell value. AI stores structured output: \{ value: "high" } for text/select, \{ value: 42, currency: "EUR" } for amount. | | created\_at | ISO 8601, 🔒 system | — | onCreate hook | — | Creation timestamp. | | updated\_at | ISO 8601, 🔒 system | — | onCreate + onUpdate hooks; nullable | — | Last-update timestamp (recompute overwrites it). | ### Relationships | Name | Type | Required | Description | | -------------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | custom\_column | to-one (custom\_column) | ✅ Yes | The column definition this value belongs to. deleteRule: cascade — deleting the column deletes its values. | ### System-computed * value\_id generated by gen\_random\_uuid() * created\_at via onCreate; updated\_at via onCreate + onUpdate hooks * NO deleted\_at — values are not soft-deleted; they cascade-delete with their custom\_column (deleteRule: cascade) * Indexed on (custom\_column, record\_id) * record\_id is the target record's PUBLIC UUID (root-agnostic join key) — no FK to the underlying entity table * value shape is written by the enrichment worker: or for amount columns ## Example ```json theme={null} { "data": { "type": "custom_column_value", "id": "e4d5c6b7-1111-4111-8111-aaaabbbbcccc", "attributes": { "value_id": "e4d5c6b7-1111-4111-8111-aaaabbbbcccc", "root": "companies", "record_id": "3fa1c2d4-87ae-4b10-a9f3-ec5d1234abcd", "value": { "value": "high" }, "created_at": "2026-05-20T09:15:30.000Z", "updated_at": "2026-05-28T11:03:00.000Z" } }, "relationships": { "custom_column": { "data": { "type": "custom_column", "id": "7c1f9a02-4d3b-4e21-9a77-2f0c11aa33bc" } } } } ``` Source: `apps/api/src/database/entities/CustomColumnValue.ts` · domain: records / data-views · tier: Infrastructure # Custom Column Source: https://docs.wellapp.ai/object-reference/custom_columns A Custom Column is a per-workspace, per-root virtual column a user adds to a records table (e.g A Custom Column is a per-workspace, per-root virtual column a user adds to a records table (e.g. a 'Risk level' column on companies). It holds the column definition only — name, type, position, and the AI/formula/format config — while the computed cell values live in custom\_column\_values. Custom columns are workspace-scoped and namespaced by records root; they are NOT Hasura columns and NOT governed DATA\_VIEW\_ROOTS, so they sit behind the records table as an overlay rather than as a queryable entity of their own. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Custom Column | | Resource type (JSON:API `type`) | `custom_column` | | Collection / records root | — (not a records root) | | REST base | `/v1/custom-columns` | | Entity class | `CustomColumn` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/custom-columns` | 🟡 Planned | | Retrieve | `GET /v1/custom-columns/{id}` | 🟡 Planned | | Create | `POST /v1/custom-columns` | 🟡 Planned | | Update | `PATCH /v1/custom-columns/{id}` | 🟡 Planned | | Delete | `DELETE /v1/custom-columns/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ----------------------------- | -------- | -------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | custom\_column\_id | string, UUID, 🔒 system | ✅ Yes | unique; gen\_random\_uuid() | — | Public identifier of the column definition. Internal pk is never exposed. | | root | string | ✅ Yes | max 50 chars; part of partial-unique key | A records root (companies, invoices, transactions, …) | The data-view root this column is attached to. Scopes the column to one records table. | | name | string | ✅ Yes | max 255 chars | — | Human-readable column header shown in the records table. | | field\_key | string | ✅ Yes | max 100 chars; part of partial-unique key; slugified from name | — | Stable machine key for the column, slugified from name at creation. Unique per (workspace, root). | | type | string (CustomColumnTypeEnum) | ✅ Yes | native enum custom\_column\_type\_enum; default 'text' | text, number, amount, single\_select, multi\_select, ai | Column data type. 'ai' (and rules\_config-bearing columns) are auto-computed; others may be manually entered. | | position | number (int) | ⚪ No | default 0 | — | Display order of the column within the records table. | | icon | string | ⚪ No | max 100 chars; nullable | — | Optional icon identifier rendered in the column header. | | rules\_config | jsonb | ⚪ No | nullable | — | AI compute rules. When set, the column is auto-computed by the enrichment pipeline (Anthropic Haiku) for every record. | | formula\_config | jsonb | ⚪ No | nullable | — | Formula configuration for computed (non-AI) columns. | | format\_config | jsonb | ⚪ No | nullable | — | Display/format configuration (e.g. select options, number format). | | created\_at | ISO 8601, 🔒 system | — | onCreate hook | — | Creation timestamp. | | updated\_at | ISO 8601, 🔒 system | — | onCreate + onUpdate hooks | — | Last-update timestamp. | | deleted\_at | ISO 8601, 🔒 system | — | nullable; soft delete | — | Soft-delete tombstone; null when active. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------------------- | -------- | ----------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | Owning workspace — the per-workspace scope of the column. | | values | to-many (custom\_column\_value) | ⚪ No | Computed/entered cell values for this column, one per record (cascade-deleted with the column). | ### System-computed * custom\_column\_id generated by gen\_random\_uuid() * field\_key slugified from name at creation (slugifyFieldKey) * Partial unique index on (workspace\_pk, root, field\_key) WHERE deleted\_at IS NULL — managed in a raw-SQL migration, not @Index (MikroORM cannot express partial indexes) * type defaults to 'text'; position defaults to 0 * created\_at via onCreate; updated\_at via onCreate + onUpdate hooks * Soft-delete via deleted\_at * On create: autoProvisionPresets(workspace, root) may seed preset AI columns; fire-and-forget recompute over existing records * On rules\_config / formula\_config / format\_config change: enqueueRecompute → recompute all records for the column * AI columns (type='ai' or rules\_config present) compute via EnrichmentTask (CUSTOM\_COLUMN) → Cloud Task → Anthropic Haiku, with anti-prompt-injection guards ## Example ```json theme={null} { "data": { "type": "custom_column", "id": "7c1f9a02-4d3b-4e21-9a77-2f0c11aa33bc", "attributes": { "custom_column_id": "7c1f9a02-4d3b-4e21-9a77-2f0c11aa33bc", "root": "companies", "name": "Risk level", "field_key": "risk_level", "type": "single_select", "position": 3, "icon": "shield-alert", "rules_config": "{\"prompt\":\"Classify the company risk as low, medium, or high based on its industry and country.\"}", "formula_config": null, "format_config": "{\"options\":[\"low\",\"medium\",\"high\"]}", "created_at": "2026-05-20T09:14:00.000Z", "updated_at": "2026-05-28T11:02:00.000Z", "deleted_at": null } }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-0000-4000-8000-000000000001" } } } } ``` Source: `apps/api/src/database/entities/CustomColumn.ts` · domain: records / data-views · tier: Infrastructure # DocumentExtraction Source: https://docs.wellapp.ai/object-reference/document_extractions DocumentExtraction stores the parsed-text output of one parser run against one Document DocumentExtraction stores the parsed-text output of one parser run against one Document. Each row represents a single (document, parser\_name, parser\_version) execution result and acts as a caching layer: the extraction orchestrator checks for an existing active row whose parser\_name + parser\_version + source\_checksum match the current request and reuses the stored text instead of calling the remote parser (LlamaParse, LiteParse) again. One document may accumulate multiple rows when different parsers or parser versions are used; only one row per (document\_pk, parser\_name, parser\_version, COALESCE(source\_checksum,'')) is active at any time, enforced by a partial unique index on deleted\_at IS NULL. The entity is workspace-scoped for tenant isolation and cleanup queries. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | DocumentExtraction | | Resource type (JSON:API `type`) | `document_extraction` | | Collection / records root | — (not a records root) | | REST base | `/v1/document-extractions` | | Entity class | `DocumentExtraction` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------- | ---------- | | List | `GET /v1/document-extractions` | 🟡 Planned | | Retrieve | `GET /v1/document-extractions/{id}` | 🟡 Planned | | Create | `POST /v1/document-extractions` | 🟡 Planned | | Update | `PATCH /v1/document-extractions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/document-extractions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document\_extraction\_id | string (UUID) | ✅ Yes | UNIQUE; NOT NULL; generated by gen\_random\_uuid() | Any valid UUID v4 | Public UUID identifying this extraction record. Generated automatically by the database default gen\_random\_uuid(). | | parser\_name | string | ✅ Yes | VARCHAR(64); NOT NULL | Any string up to 64 characters | Identifies the parser engine used for this extraction run (e.g. 'llamaparse', 'litepdf'). Part of the dedup key. | | parser\_version | string | ✅ Yes | VARCHAR(32); NOT NULL | Any string up to 32 characters | Semantic version of the parser at extraction time. Combined with parser\_name and source\_checksum to determine cache validity. | | text | string (text) | ✅ Yes | TEXT; NOT NULL | Any text | Plain-text output produced by the parser. This is the primary extraction artifact consumed by downstream AI extraction services. | | markdown | string (text) | ⚪ No | TEXT; nullable | Any text or null | Optional Markdown-formatted representation of the extracted content, provided when the parser supports structured markdown output. | | page\_count | integer | ⚪ No | INT; nullable | Any positive integer or null | Number of pages processed by the parser, when reported by the parser response. | | used\_ocr | boolean | ⚪ No | BOOLEAN; nullable | true / false / null | Whether the parser applied OCR during this extraction run. Null when the parser did not report OCR usage. | | llamaparse\_job\_id | string | ⚪ No | VARCHAR(128); nullable | Any string up to 128 characters or null | External job identifier returned by the LlamaParse API for this parsing job. Useful for troubleshooting or re-fetching results from the provider. | | source\_checksum | string | ⚪ No | VARCHAR(64); nullable; participates in partial unique index uniq\_document\_extraction\_per\_parser\_checksum (document\_pk, parser\_name, parser\_version, COALESCE(source\_checksum,'')) WHERE deleted\_at IS NULL | Any string up to 64 characters or null | Content-addressable checksum of the source document binary at the time of extraction. Together with parser\_name and parser\_version, forms the cache key for dedup. A NULL checksum is treated as an empty string in the partial unique index. | | created\_at | 🔒 system — datetime | ✅ Yes | TIMESTAMPTZ; NOT NULL; default NOW() | — | Timestamp when this extraction record was created. Set once at insert via onCreate hook; never updated. | | updated\_at | 🔒 system — datetime | ⚪ No | TIMESTAMPTZ; nullable | — | Timestamp of the last in-place update to this record. Set by onUpdate hook; null until the first update after initial insert. | | deleted\_at | 🔒 system — datetime | ⚪ No | TIMESTAMPTZ; nullable | — | Soft-delete timestamp. When non-null the record is logically deleted. The partial unique index on the cache key is scoped to WHERE deleted\_at IS NULL, allowing a new extraction to replace a soft-deleted one. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document | to-one (ManyToOne) | Yes — FK NOT NULL, ON DELETE CASCADE | The Document this extraction was produced from. Foreign key document\_pk references core\_api.documents(pk). Cascade-deletes this row if the parent Document is hard-deleted. | | workspace | to-one (ManyToOne) | Yes — FK NOT NULL, ON DELETE CASCADE | The Workspace that owns this extraction, used for tenant isolation and cleanup queries. Foreign key workspace\_pk references core\_api.workspaces(pk). Cascade-deletes this row if the Workspace is hard-deleted. | ### System-computed * document\_extraction\_id — generated by gen\_random\_uuid() database default at INSERT; never supplied by the caller * created\_at — set once via MikroORM onCreate hook (new Date()); immutable after creation * updated\_at — set by MikroORM onUpdate hook (new Date()) on every subsequent flush; null until the first update * deleted\_at — written by the extraction orchestrator or backfill services to soft-delete superseded rows; the partial unique index is scoped to deleted\_at IS NULL so a new parser run can replace a deleted cache entry without a constraint violation * Cache-dedup logic — DocumentExtractionRepository (or the orchestrator) queries for an existing active row matching (document\_pk, parser\_name, parser\_version, source\_checksum) before calling the remote parser; a hit returns the stored text without an external API call * source\_checksum coalesce sentinel — the partial unique index uses COALESCE(source\_checksum, '') so a null checksum does not cause PostgreSQL to treat every null-checksum row as distinct (PostgreSQL treats NULLs as distinct in unique indexes by default) * Workspace scope — workspace\_pk is always set to req.workspace.pk at write time by the extraction pipeline; never accepted from client input * parser\_name and parser\_version are stamped at extraction time from the calling service constant and the deployed parser SDK version; not user-supplied ## Example ```json theme={null} { "data": { "type": "document_extraction", "id": "3f8e2c14-0a71-4b9e-a632-1c7de05f8b24", "attributes": { "document_extraction_id": "3f8e2c14-0a71-4b9e-a632-1c7de05f8b24", "parser_name": "llamaparse", "parser_version": "2.3.0", "text": "Invoice\nDate: 2026-04-15\nTotal: €1 250,00\n...", "markdown": "# Invoice\n**Date:** 2026-04-15 \n**Total:** €1 250,00\n\n...", "page_count": 2, "used_ocr": false, "llamaparse_job_id": "lp_job_a1b2c3d4e5f6", "source_checksum": "sha256:aabbcc1122334455", "created_at": "2026-05-28T09:14:32.000Z", "updated_at": "2026-05-28T09:14:55.000Z", "deleted_at": null }, "relationships": { "document": { "data": { "type": "document", "id": "9c4a7b23-1f5d-4e88-b731-2d6ef19a0c47" } }, "workspace": { "data": { "type": "workspace", "id": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/DocumentExtraction.ts` · domain: ingestion · tier: Infrastructure # DocumentStructuredExtraction Source: https://docs.wellapp.ai/object-reference/document_structured_extractions A `document_structured_extraction` record is the persisted output of one LLM structured-extraction pass over a parsed document A `document_structured_extraction` record is the persisted output of one LLM structured-extraction pass over a parsed document. It is separate from `document_extractions` (which stores raw OCR/parser text): this table stores the typed, schema-validated JSON produced after classification and field mapping. The cache key — a composite of `document_pk`, `extraction_family`, `schema_name`, `schema_version`, `prompt_version`, `model_policy`, `source_checksum`, and `evidence_checksum` — ensures that retries and backfills reuse existing output without bypassing stale-source or stale-schema guards. Records are scoped to a `workspace` and hang off a parent `document`; soft-deletion is used to invalidate superseded cache entries while preserving the audit trail. | Naming | Value | | ------------------------------- | ------------------------------------- | | Object | DocumentStructuredExtraction | | Resource type (JSON:API `type`) | `document_structured_extraction` | | Collection / records root | — (not a records root) | | REST base | `/v1/document-structured-extractions` | | Entity class | `DocumentStructuredExtraction` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------------------- | ---------- | | List | `GET /v1/document-structured-extractions` | 🟡 Planned | | Retrieve | `GET /v1/document-structured-extractions/{id}` | 🟡 Planned | | Create | `POST /v1/document-structured-extractions` | 🟡 Planned | | Update | `PATCH /v1/document-structured-extractions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/document-structured-extractions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------------------ | ------------------------------------ | -------- | ------------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document\_structured\_extraction\_id | string (UUID) | ✅ Yes | unique | — | Public UUID identifier, auto-generated by gen\_random\_uuid(). This is the stable external reference surfaced in the API. The internal pk is never exposed. | | extraction\_family | string | ✅ Yes | max length 32; included in partial-unique-on-deleted\_at index | — | High-level category of the extraction pass (e.g. 'invoice', 'receipt', 'contract'). Part of the cache-key composite unique index. | | schema\_name | string | ✅ Yes | max length 96; included in partial-unique-on-deleted\_at index | — | Name of the Zod/JSON schema used to validate and shape the extraction output. Part of the cache-key composite unique index. | | schema\_version | string | ✅ Yes | max length 48; included in partial-unique-on-deleted\_at index | — | Semver string of the schema. Bump forces a new extraction even if all other cache-key components are unchanged. | | prompt\_version | string | ✅ Yes | max length 48; included in partial-unique-on-deleted\_at index | — | Version identifier of the LLM prompt template used. Part of the cache-key composite unique index. | | model\_policy | string | ✅ Yes | max length 64; included in partial-unique-on-deleted\_at index | — | Identifies the model-selection policy in effect when the extraction ran (e.g. a model alias or routing policy name). Part of the cache-key composite unique index. | | source\_checksum | string | ✅ Yes | max length 64; included in partial-unique-on-deleted\_at index | — | Checksum of the raw source content (OCR text / parsed PDF bytes) fed into the extraction. Stale-source guard: if the document's parsed text changes, this checksum changes and a new extraction is required. | | evidence\_checksum | string | ✅ Yes | max length 64; included in partial-unique-on-deleted\_at index | — | Checksum of the structured evidence slice passed to the LLM (may differ from source\_checksum when evidence is preprocessed or truncated). Part of the cache-key composite unique index. | | classification\_json | jsonb | ✅ Yes | NOT NULL | — | LLM output from the classification phase: document type, confidence scores, locale, and any routing signals that determined which schema to apply in subsequent passes. | | core\_extraction\_json | jsonb | ✅ Yes | NOT NULL | — | LLM output from the core extraction phase: the mandatory, high-confidence fields (header-level invoice fields such as issuer, reference, date, totals). Always present even when detail extraction is skipped. | | detail\_extraction\_json | jsonb | ⚪ No | nullable | — | LLM output from the optional detail extraction phase: line items, tax breakdowns, and other structured sub-arrays that require a secondary prompt. NULL when detail extraction was not requested or failed gracefully. | | final\_extraction\_json | jsonb | ✅ Yes | NOT NULL | — | Merged, post-processed extraction output combining classification, core, and detail phases. This is the authoritative input used by downstream invoice mapping and persistence services. | | invoice\_mapped\_json | jsonb | ⚪ No | nullable | — | The result of mapping final\_extraction\_json onto Well's internal invoice schema (entity PKs resolved, field names normalised). NULL when the document is not an invoice or mapping has not yet run. | | selected\_provider | string | ⚪ No | max length 32; nullable | — | The AI provider selected at runtime (e.g. 'openai', 'anthropic'). NULL when the model policy does not record per-extraction provider choice. | | selected\_model | string | ⚪ No | max length 128; nullable | — | The specific model identifier resolved from the model\_policy at runtime (e.g. 'gpt-5.4'). NULL when not recorded. | | quality\_flags | jsonb | ⚪ No | nullable | — | Post-extraction quality signals: low-confidence fields, OCR warnings, schema-validation failures, and any flags the extraction pipeline chose to surface for downstream review. NULL when no quality issues were detected. | | created\_at | 🔒 system — timestamp with time zone | ✅ Yes | NOT NULL; defaultRaw: NOW() | — | Row creation timestamp, set once by the onCreate hook. Reflects when the extraction pipeline persisted this result. | | updated\_at | 🔒 system — timestamp with time zone | ⚪ No | nullable | — | Row update timestamp, managed by the onUpdate hook. NULL until the first update after creation. | | deleted\_at | 🔒 system — timestamp with time zone | ⚪ No | nullable; excluded from uniq\_document\_structured\_extraction\_active when non-NULL | — | Soft-delete timestamp. When set, the row is excluded from the composite partial-unique index (uniq\_document\_structured\_extraction\_active), allowing a fresh extraction with the same cache key to be inserted. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document | to-one (ManyToOne) | ✅ Yes | The parent Document this extraction was produced from. The document\_pk FK carries ON DELETE CASCADE, so deleting a document hard-deletes all its structured extractions. | | workspace | to-one (ManyToOne) | ✅ Yes | The tenant workspace that owns this extraction record. Used for all workspace-scoped queries and Hasura RLS enforcement. workspace\_pk FK carries ON DELETE CASCADE. | ### System-computed * document\_structured\_extraction\_id — auto-generated by gen\_random\_uuid() database default; never supplied by the caller * created\_at — set to NOW() on INSERT via MikroORM onCreate hook; never updated * updated\_at — set to NOW() on UPDATE via MikroORM onUpdate hook; NULL on creation * deleted\_at — set by the extraction pipeline soft-delete path; when set, the partial-unique index uniq\_document\_structured\_extraction\_active no longer covers the row, enabling a replacement extraction with the same cache key to be inserted * Cache-key deduplication — the partial unique index (document\_pk, extraction\_family, schema\_name, schema\_version, prompt\_version, model\_policy, source\_checksum, evidence\_checksum) WHERE deleted\_at IS NULL guarantees at-most-one live structured extraction per distinct extraction context; the pipeline soft-deletes the old row before inserting a fresh one on invalidation * Row written exclusively by the LLM extraction pipeline (ExtractPersistenceService / document structured-extraction service); no user-facing PATCH route exists for this entity ## Example ```json theme={null} { "data": { "type": "document_structured_extraction", "id": "d3a1e8f2-5b4c-4e2f-9aab-0123456789ab", "attributes": { "extraction_family": "invoice", "schema_name": "invoice_v2_fr", "schema_version": "2.4.0", "prompt_version": "p1.3", "model_policy": "gpt-5_structured_v1", "source_checksum": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "evidence_checksum": "sha256:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3", "classification_json": { "document_type": "invoice", "confidence": 0.98, "locale": "fr-FR" }, "core_extraction_json": { "issuer_name": "Acme SAS", "reference_number": "FAC-2026-0042", "issue_date": "2026-05-15", "grand_total": 1200.00, "currency": "EUR" }, "detail_extraction_json": { "line_items": [ { "description": "Consulting services", "quantity": 1, "unit_price": 1000.00 }, { "description": "Expenses", "quantity": 1, "unit_price": 200.00 } ] }, "final_extraction_json": { "issuer_name": "Acme SAS", "reference_number": "FAC-2026-0042", "issue_date": "2026-05-15", "grand_total": 1200.00, "currency": "EUR", "line_items": [ { "description": "Consulting services", "quantity": 1, "unit_price": 1000.00 }, { "description": "Expenses", "quantity": 1, "unit_price": 200.00 } ] }, "invoice_mapped_json": { "issuer_pk": 482917, "receiver_pk": 482918, "grand_total": 1200.00, "local_currency": "EUR", "status": "unpaid" }, "selected_provider": "openai", "selected_model": "gpt-5.4", "quality_flags": { "low_confidence_fields": ["due_date"], "ocr_warnings": [] }, "created_at": "2026-05-28T14:32:10.000Z", "updated_at": "2026-05-28T14:32:11.000Z", "deleted_at": null }, "relationships": { "document": { "data": { "type": "document", "id": "c7f2a1e0-0001-0001-0001-000000000001" } }, "workspace": { "data": { "type": "workspace", "id": "9f3b2d00-aaaa-bbbb-cccc-000000000001" } } } } } ``` Source: `apps/api/src/database/entities/DocumentStructuredExtraction.ts` · domain: ingestion · tier: Infrastructure # DocumentWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/document_workspace_connectors DocumentWorkspaceConnector is a junction table that records the relationship between a Document and the WorkspaceConnector responsible for ingesting or distribu DocumentWorkspaceConnector is a junction table that records the relationship between a Document and the WorkspaceConnector responsible for ingesting or distributing it. Each row captures which connector handled a given document, in which direction (input = ingested from a source, output = routed to a destination), and when the link was created. It is the provenance ledger for the document sync pipeline: every document that enters or leaves Well through a connector has at least one DWC row, and the table is the primary surface queried during sync-status range checks (filtered by workspace\_connector\_pk + created\_at BETWEEN). The entity carries no public UUID key — it is an internal infrastructure join record, not a user-facing resource. | Naming | Value | | ------------------------------- | ----------------------------------- | | Object | DocumentWorkspaceConnector | | Resource type (JSON:API `type`) | `document_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/document-workspace-connectors` | | Entity class | `DocumentWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------------------- | ---------- | | List | `GET /v1/document-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/document-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/document-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/document-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/document-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------- | -------- | -------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | direction | string (native enum: direction\_enum) | ✅ Yes | NOT NULL; stored as PostgreSQL native enum core\_api.direction\_enum | 'input' \| 'output' | Flow direction of the document through the connector. 'input' means the connector ingested the document into Well (source connector). 'output' means Well routed the document outward through the connector (destination connector). | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL; set by onCreate: () => new Date() | — | Timestamp when the DWC link was created. Set once via onCreate hook; also set explicitly by DocumentWorkspaceConnectorService. Used as the right-hand operand in sync-status range queries (indexed via idx\_dwc\_wc\_created\_at and idx\_dwc\_input\_active\_doc\_created). | | updated\_at | 🔒 system — timestamptz | ⚪ No | NULLABLE | — | Timestamp of the last mutation. Set by onCreate and onUpdate hooks; also set explicitly by the service methods. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | NULLABLE | — | Soft-delete timestamp. NULL means the link is active. Set by application code; never populated by the normal create/stage paths. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document | to-one (ManyToOne) | ✅ Yes | The Document this connector link is associated with. Foreign key references core\_api.documents(pk) ON UPDATE CASCADE. Indexed as the leading column of idx\_dwc\_input\_active\_doc\_created (document\_pk, created\_at) for document-first access patterns. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The WorkspaceConnector (activated connector instance for a workspace) that ingested or distributed this document. Foreign key references core\_api.workspace\_connectors(pk) ON UPDATE CASCADE. Indexed as the leading column of idx\_dwc\_wc\_created\_at (workspace\_connector\_pk, created\_at) for sync-status range queries. | ### System-computed * created\_at — set once by onCreate: () => new Date(); also set explicitly to new Date() by DocumentWorkspaceConnectorService.createDocumentWorkspaceConnector() and .stage() * updated\_at — set by onCreate and onUpdate hooks; also set explicitly by the service methods * deleted\_at — soft-delete sentinel; NULL on creation; not written by the pipeline's normal create/stage paths * pk — auto-increment serial primary key assigned by Postgres; internal join target only, never exposed in the public API * No public UUID (\*\_id) column — this is a pure infrastructure junction record with no user-facing stable identifier beyond pk ## Example ```json theme={null} { "data": { "type": "document_workspace_connector", "attributes": { "direction": "input", "created_at": "2026-01-15T09:23:11.000Z", "updated_at": "2026-01-15T09:23:11.000Z", "deleted_at": null }, "relationships": { "document": { "data": { "type": "document", "id": "e3f1a2b4-cc90-4d5e-b8f7-123456789abc" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "a9b2c3d4-1111-2222-3333-abcdef012345" } } } } } ``` Source: `apps/api/src/database/entities/DocumentWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # Document Source: https://docs.wellapp.ai/object-reference/documents A Document represents a file (PDF, image, or other binary) uploaded to or ingested into a workspace A Document represents a file (PDF, image, or other binary) uploaded to or ingested into a workspace. It is the raw file substrate for the invoice extraction pipeline — every Invoice may reference one Document, and every Transaction may be linked to at most one active Document via the TransactionDocument junction. Documents arrive either through direct user upload (multipart POST to /v1/documents), ambient capture from email connectors, or connector sync (tracked via DocumentWorkspaceConnector with a direction of input or output). The entity carries GCS storage coordinates (bucket, path), MIME type, file size, a content-fingerprint checksum for cross-format deduplication, and an AI-classified document type code drawn from the UN/EDIFACT document type taxonomy. | Naming | Value | | ------------------------------- | --------------- | | Object | Document | | Resource type (JSON:API `type`) | `document` | | Collection / records root | `documents` | | REST base | `/v1/documents` | | Entity class | `Document` | ## API operations | Operation | Method & path | Status | | --------- | --------------------------- | ------------- | | List | `GET /v1/documents` | ✅ Implemented | | Retrieve | `GET /v1/documents/{id}` | ✅ Implemented | | Create | `POST /v1/documents` | ✅ Implemented | | Update | `PATCH /v1/documents/{id}` | 🟡 Planned | | Delete | `DELETE /v1/documents/{id}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------- | ------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | document\_id | string, UUID, 🔒 system | ✅ Yes | gen\_random\_uuid() default; UNIQUE | — | Public identifier for the document exposed in all API responses. Never the internal pk. | | path | string | ✅ Yes | — | — | GCS object path within the bucket. Fully qualified key used to download the file from Cloud Storage (e.g. workspaces/\/documents/\/\/\). | | filename | string | ✅ Yes | — | — | Human-readable original filename as provided at upload time. Used as the display label in records-table views (overrides.yml display\_type: file). | | bucket | string | ✅ Yes | — | — | GCS bucket name where the file is stored. Varies by environment (prod vs. staging). Combined with path to resolve a signed download URL. | | type | string | ✅ Yes | — | — | MIME type of the uploaded file as detected or declared at upload time (e.g. application/pdf, image/jpeg, image/png). | | size | number (integer, bytes) | ✅ Yes | — | — | File size in bytes at upload time. | | content\_checksum | string (hex SHA-256) | ⚪ No | length: 64; indexed; partial unique index on (workspace\_pk, content\_checksum) WHERE deleted\_at IS NULL AND content\_checksum IS NOT NULL — enforces L2 content-aware deduplication per workspace | — | SHA-256 hash of the file after stripping format-specific metadata (EXIF for JPEG, tEXt/iTXt/zTXt/tIME/iCCP for PNG, /Info+/CreationDate+/ModDate+/ID for PDF). Equals rawChecksum for unsupported types. Used to reject duplicate uploads of the same logical document even when metadata differs (L2 dedup). NULL on legacy documents uploaded before Migration20260211100000. | | document\_type | string (DocumentTypeCodeEnum), @Enrichable | ⚪ No | nativeEnumName: document\_type\_code\_enum; nullable; AI-classified via @Enrichable decorator | 380 (commercial invoice), 381 (credit note), 383 (debit note), 384 (corrected invoice), 325 (proforma invoice), 326 (partial invoice), 385 (consolidated invoice), 386 (prepayment invoice), 387 (hire invoice), 388 (tax invoice), 389 (self-billing invoice), 390 (delcredere invoice), 391 (factored invoice), 392 (lease invoice), 393 (consignment invoice), 394 (factored credit note), 395 (consignment credit note), 396 (factored debit note), 397 (consignment debit note), 220 (order), 221 (blanket order), 222 (spot order), 230 (purchase order), 231 (blanket purchase order), 232 (spot purchase order), 235 (repair purchase order), 236 (call-off purchase order), 310 (RFQ), 311 (RFP), 312 (price quote), 315 (contract award), 320 (certified invoice), 322 (freight invoice), 327 (price variation invoice), 328 (tax point invoice), 329 (sole agent invoice), 440 (payment order), 441 (wage payment order), 446 (tax payment order), 447 (customs payment order), 450 (payment advice), 451 (credit advice), 452 (debit advice), 456 (remittance advice), 460 (financial statement of account), 270 (packing list), 271 (certified packing list), 550 (despatch advice), 551 (goods receipt), 552 (ultimate goods receipt), 622 (road consignment note), 623 (house bill of lading), 705 (bill of lading), 740 (air waybill), 741 (master air waybill), 743 (house air waybill), 610 (customs declaration SAD), 611 (goods import declaration), 612 (goods export declaration), 615 (customs invoice), 617 (tax certificate), 618 (tax assessment), 619 (tax demand), 700 (certificate of origin), 701 (UNESCO coupon), 702 (forwarder certificate of receipt), 770 (insurance policy), 775 (insurance certificate), 805 (inventory report), 810 (stock report), 815 (financial statement), 820 (balance sheet), 825 (trial balance), 830 (P\&L statement), 835 (tax return), 840 (payroll), 845 (timesheet), 850 (expense report), 901 (utility bill), 902 (expense receipt), 903 (bank statement), 904 (subscription billing statement), 999 (other) | UN/EDIFACT-based document type code. Classified by the AI extraction pipeline (marked @Enrichable). NULL until classification runs. The ingestion pipeline uses INVOICE\_DOCUMENT\_TYPES, NON\_INVOICE\_BILLING\_DOCUMENT\_TYPES, and PAYMENT\_RELATED\_DOCUMENT\_TYPES sub-sets to route documents through the correct extraction flow. | | local\_file\_name | string | ⚪ No | nullable | — | Internal filename used during temporary local storage or processing steps. Populated by connectors that buffer the file to disk before uploading to GCS. NULL in most cases. | | uploaded\_at | Date (timestamptz) | ✅ Yes | — | — | Timestamp when the file was originally uploaded or ingested. May differ from created\_at for connector-sourced documents (set to the provider's original creation time). Indexed via idx\_documents\_uploaded\_active for time-sorted listing. | | created\_at | Date (timestamptz), 🔒 system | ✅ Yes | Set by onCreate lifecycle hook | — | Row creation timestamp set once by MikroORM onCreate hook. Used in the partial index idx\_documents\_workspace\_created\_active for sorted workspace-scoped listing (WHERE deleted\_at IS NULL). | | updated\_at | Date (timestamptz), 🔒 system | ⚪ No | Set by onCreate and onUpdate lifecycle hooks | — | Last modification timestamp. Updated automatically on every ORM flush that mutates the row. | | deleted\_at | Date (timestamptz) | ⚪ No | nullable; soft-delete sentinel — all active queries filter deleted\_at IS NULL | — | Soft-delete timestamp. Set to the deletion instant; NULL for active documents. The partial unique content\_checksum index, the workspace+created\_at listing index, and the workspace+deleted\_at composite index all gate on this column. | ### Relationships | Name | Type | Required | Description | | ---------------------------- | ------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No (nullable) | The workspace that owns this document. Nullable to support legacy rows and certain edge-case uploads, but every new document created via CollectService or a connector must have workspace set. Workspace scoping in Hasura RLS uses this FK. | | collect | to-one (collect) | ⚪ No (nullable) | The Collect run that produced this document. Collect represents a document-retrieval session (e.g. a Gmail blueprint fetch). NULL for manually uploaded documents or connector-synced documents that bypass the collect flow. | | source\_workspace\_connector | to-one (workspace\_connector) | ⚪ No (nullable) | The WorkspaceConnector instance that ingested this document. NULL for user-uploaded documents. Populated by connector sync flows to track provenance. Used in the composite\_sourced\_from display (composites.yml sourceWorkspaceConnector.composite\_connector\_logo\_name) and the records-page connector source column. | | invoices | to-many (invoice) | — | Invoices that reference this document as their source file. Typically zero or one Invoice per Document (a PDF invoice maps to one Invoice row after extraction). The Invoice.document FK points back here; this collection is the inverse side. | | transaction\_documents | to-many (transaction\_document) | — | Junction rows linking this document to Transactions. TransactionDocument enforces a partial unique index (one active attachment per transaction: uq\_transaction\_documents\_one\_active\_per\_transaction WHERE deleted\_at IS NULL), so a given transaction carries at most one active document. A single document may be attached to multiple transactions. | ### System-computed * document\_id is generated by gen\_random\_uuid() at INSERT time and is the public API identifier. The internal pk (auto-increment integer) is never exposed. * created\_at is set once via MikroORM onCreate: () => new Date() and never changed thereafter. * updated\_at is set by both onCreate and onUpdate hooks, so it reflects the latest ORM flush against this row. * deleted\_at is the soft-delete sentinel. Active queries must filter deleted\_at IS NULL. No physical row deletion occurs; the column is set to the deletion timestamp. * content\_checksum is computed by the upload service after stripping format-specific metadata bytes (EXIF for JPEG, metadata chunks for PNG, /Info+date+ID for PDF), then taking SHA-256. It equals the raw file SHA-256 for unsupported types. The partial unique index idx\_documents\_workspace\_content\_checksum\_active enforces per-workspace L2 deduplication: (workspace\_pk, content\_checksum) WHERE deleted\_at IS NULL AND content\_checksum IS NOT NULL. * document\_type is classified by the AI enrichment pipeline. The @Enrichable decorator marks this field for the enrichment worker. Until classification completes, the field is NULL. normalizeDocumentTypeCode() maps unrecognised values to DocumentTypeCodeEnum.OTHER (999). * uploaded\_at is set by the caller (upload controller or connector sync) and may represent the file's original creation time from the provider rather than the time of ingest into Well. * The partial index idx\_documents\_workspace\_created\_active (workspace\_pk, created\_at DESC WHERE deleted\_at IS NULL) optimises the default records-page sort for the documents root. * sourceWorkspaceConnector carries ingestion provenance. NULL means user-originated upload; non-NULL means the document was created by a connector sync flow. * DocumentWorkspaceConnector junction rows are appended (never mutated) to track each connector that ingested (direction=input) or received (direction=output) the document. A single Document can have multiple DWC rows from different connectors. Note: the inverse collection is NOT declared on the Document entity — DWC rows are accessed via the DocumentWorkspaceConnector repository, not via a collection on Document. * DocumentExtraction rows (one per document × parser pair) cache parsed text output to avoid redundant LlamaParse / LiteParse calls. The partial unique index uniq\_document\_extraction\_per\_parser\_checksum on (document\_pk, parser\_name, parser\_version, COALESCE(source\_checksum, '')) WHERE deleted\_at IS NULL enforces one extraction row per parser version per document content state. * DocumentStructuredExtraction rows extend the extraction pipeline with structured field output beyond raw text (see Migration20260528120000\_document\_structured\_extractions). ## Example ```json theme={null} { "data": { "type": "document", "id": "c7e3f2a1-84d5-4b9e-a012-3f6c8d9e1b47", "attributes": { "document_id": "c7e3f2a1-84d5-4b9e-a012-3f6c8d9e1b47", "path": "workspaces/9f3a7c21-e8b4-4d0f-b3c1-2a5d8e6f0c19/documents/2026/05/facture-fournisseur-mai.pdf", "filename": "facture-fournisseur-mai.pdf", "bucket": "well-app-documents-prod", "type": "application/pdf", "size": 348291, "content_checksum": "a3f8d1c2e7b049561a84f2c3d6e9b0a1f5c2d8e4b7a3c6f9d2e1b4a7c0f3e6d9", "document_type": "380", "local_file_name": null, "uploaded_at": "2026-05-14T09:23:11.000Z", "created_at": "2026-05-14T09:23:12.341Z", "updated_at": "2026-05-14T09:24:05.882Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "9f3a7c21-e8b4-4d0f-b3c1-2a5d8e6f0c19" } }, "collect": { "data": null }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "b2d4e6a8-c0f2-4e8d-a6b0-c2e4f6a8d0b2" } }, "invoices": { "data": [ { "type": "invoice", "id": "d9e1b3a5-c7f9-4d2e-b4a6-c8d0e2f4b6a8" } ] }, "transaction_documents": { "data": [ { "type": "transaction_document", "id": "e1f3a5b7-d9c1-4e3f-a5b7-d9e1f3a5b7c9" } ] } } } } ``` Source: `apps/api/src/database/entities/Document.ts` · domain: ingestion · tier: Main # EmailEvent Source: https://docs.wellapp.ai/object-reference/email_events EmailEvent is an append-only event log for the outbound email lifecycle EmailEvent is an append-only event log for the outbound email lifecycle. One row is written per observed state transition — both internal (MailService called the provider) and provider-side (delivery and engagement webhooks from SendGrid). Multiple rows per logical send are correlated via `message_id`, a UUID generated at send time and echoed back by the provider in every webhook. The table is provider-agnostic: no column names SendGrid directly, so future provider swaps require no schema change. The entity is associated to a Workspace via a nullable SET NULL foreign key so bounce/suppression history outlives workspace deletion. | Naming | Value | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Object | EmailEvent | | Resource type (JSON:API `type`) | `email_event` | | Collection / records root | — (not a records root) | | REST base | `/v1/email-events` | | Entity class | `Migration20260507094935_email_events, Migration20260507120000_email_events_workspace_set_null, Migration20260507121000_email_events_drop_created_at` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------ | ---------- | | List | `GET /v1/email-events` | 🟡 Planned | | Retrieve | `GET /v1/email-events/{id}` | 🟡 Planned | | Create | `POST /v1/email-events` | 🟡 Planned | | Update | `PATCH /v1/email-events/{id}` | 🟡 Planned | | Delete | `DELETE /v1/email-events/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | --------------------- | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | email\_event\_id | uuid (🔒 system) | ✅ Yes | unique | — | Public UUID for this event row. Generated at insert by gen\_random\_uuid(); never set by callers. | | message\_id | uuid | ✅ Yes | — | — | Logical send correlator. Assigned by MailService at send time and embedded in provider customArgs so every provider webhook event for the same logical send echoes the same UUID. Ties the internal 'sent' row to all downstream provider lifecycle rows. | | email\_type | text | ⚪ No | — | welcome, sync\_complete, weekly\_digest, document\_forward, invitation, member\_welcome | Email category from MailService. Stored as TEXT (no DB CHECK) so adding a new template requires only a new entry in EMAIL\_TYPES, no migration. NULL for provider webhook events that arrive before the corresponding 'sent' row is correlated. | | recipient\_email | text | ✅ Yes | length \<= 254 (CHECK email\_events\_recipient\_length) | — | Email address of the recipient. Denormalised on every row so suppression queries (bounce list, re-invite guard) keep working even when workspace is NULL after workspace deletion. | | status | text | ✅ Yes | CHECK email\_events\_status\_values: one of the 13 allowed values | sent, failed, processed, dropped, deferred, delivered, bounced, opened, clicked, spam\_reported, unsubscribed, group\_unsubscribed, group\_resubscribed | Lifecycle status of the email at the moment this event row was written. Two categories: internal (sent, failed — written by MailService) and provider (all others — written from provider webhooks). A DB CHECK constraint enforces the vocabulary to prevent silent typo inserts. | | occurred\_at | timestamptz | ✅ Yes | — | — | When the event happened. Provider timestamp for webhook events; wall clock for internal events. Paired with received\_at to compute webhook delivery latency. | | received\_at | timestamptz (🔒 system) | ✅ Yes | default now() | — | When this row was written to the database (wall clock, set by onCreate hook). Paired with occurred\_at; the difference is webhook delivery latency. Defaults to now() at insert. | | provider | text | ⚪ No | — | sendgrid | Active email provider for this event. NULL for internal-only events that never reached a provider (status=failed before provider call). The only current value is 'sendgrid'; the vocabulary lives in EMAIL\_PROVIDERS. | | provider\_message\_id | text | ⚪ No | — | — | Provider's per-message identifier. Useful for support tickets and cross-referencing against the provider's own dashboards. NULL for internal events. | | provider\_event\_id | text | ⚪ No | partial unique index idx\_email\_events\_provider\_event\_id\_unique on (provider, provider\_event\_id) WHERE provider\_event\_id IS NOT NULL | — | Provider's per-event idempotency key. A partial unique index on (provider, provider\_event\_id) WHERE provider\_event\_id IS NOT NULL prevents duplicate webhook inserts. Internal events have this as NULL and are exempt from the constraint. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (Workspace) | No | The workspace that owns this email event. Nullable: SET NULL on workspace deletion so bounce/spam/unsubscribe history outlives the workspace and prevents re-emailing a previously hard-bounced recipient after re-invitation. Rows with workspace=NULL are naturally hidden from workspace-scoped Hasura RLS queries. | ### System-computed * email\_event\_id — gen\_random\_uuid() default at insert, never caller-supplied * received\_at — set by MikroORM onCreate hook to new Date(); represents the DB write timestamp * workspace FK on delete SET NULL — workspace deletion orphans rows rather than cascading deletes, preserving suppression history * Partial unique index on (provider, provider\_event\_id) WHERE provider\_event\_id IS NOT NULL — idempotency guard for provider webhook retries; index maintained by Postgres, not application code * No created\_at / updated\_at / deleted\_at — this entity is append-only (no soft-delete, no update semantics); created\_at was dropped in Migration20260507121000 as redundant with received\_at ## Example ```json theme={null} { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "email_event", "attributes": { "email_event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "email_type": "invitation", "recipient_email": "alice@example.com", "status": "delivered", "occurred_at": "2026-05-07T14:32:10.000Z", "received_at": "2026-05-07T14:32:11.243Z", "provider": "sendgrid", "provider_message_id": "sg-msg-00aabbcc", "provider_event_id": "sg-evt-112233445566" }, "relationships": { "workspace": { "data": { "id": "9f3e4a71-1234-5678-abcd-000000000001", "type": "workspace" } } } } } ``` Source: `apps/api/src/database/entities/EmailEvent.ts — migrations: apps/api/src/database/migrations/Migration20260507094935_email_events.ts, Migration20260507120000_email_events_workspace_set_null.ts, Migration20260507121000_email_events_drop_created_at.ts` · domain: platform · tier: Activity # Email Source: https://docs.wellapp.ai/object-reference/emails The `email` resource is a canonically-stored email address shared across the financial graph The `email` resource is a canonically-stored email address shared across the financial graph. It functions as an atomic contact channel rather than a top-level business entity: a single `Email` row holds exactly one RFC 5321 address and is linked to companies via the `CompanyEmail` pivot and to people via the `PersonEmail` pivot — both pivots carry metadata (`is_primary`, `is_verify`, `label`). The record is workspace-scoped through an optional `@ManyToOne` to `Workspace`, and is deduplicated per workspace via a find-or-create pattern keyed on `(email, workspace_pk, deleted_at IS NULL)`. | Naming | Value | | ------------------------------- | ------------ | | Object | Email | | Resource type (JSON:API `type`) | `email` | | Collection / records root | `emails` | | REST base | `/v1/emails` | | Entity class | `Email` | **Read access today:** this object is readable via the universal `POST /v1/records/query` endpoint with `root: "emails"`. A dedicated `GET /v1/emails` endpoint is **Planned**. ## API operations | Operation | Method & path | Status | | --------------- | -------------------------------------------------------------- | ------------- | | List (nested) | `GET /v1/companies/{id}/emails` · `GET /v1/people/{id}/emails` | ✅ Implemented | | Retrieve | `GET /v1/emails/{id}` | ✅ Implemented | | Create (nested) | `POST /v1/companies/{id}/emails` | ✅ Implemented | | Update | `PATCH /v1/emails/{id}` | 🟡 Planned | | Delete (nested) | `DELETE /v1/companies/{id}/emails/{subId}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | email\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() on insert | — | Public immutable identifier for the email address. Used in all API and GraphQL responses; internal `pk` is never exposed. | | email | string | ✅ Yes | max 320 characters (RFC 5321: 64 local-part + 1 @ + 255 domain); no uniqueness constraint at table level — uniqueness is enforced per-workspace by the find-or-create query scoped on (email, workspace\_pk, deleted\_at IS NULL) | — | The normalized email address string, stored trimmed and lower-cased by the repository. Widened from varchar(50) to varchar(320) in migration 20260427 to handle long modern addresses. | | created\_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | set on insert via MikroORM onCreate lifecycle hook; never null | — | Timestamp at which the email address record was first created. | | updated\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | set on insert and on every update via MikroORM onCreate/onUpdate hooks; nullable in entity declaration | — | Timestamp of the most recent mutation to this record. | | deleted\_at | string (ISO 8601 datetime) \| null | ⚪ No | nullable; soft-delete sentinel — set to current timestamp by EmailRepository.delete(); all live queries must filter deleted\_at IS NULL | — | Soft-delete timestamp. When non-null the record is considered logically deleted and is excluded from Hasura user-role queries and from the find-or-create deduplication key. | ### Relationships | Name | Type | Required | Description | | --------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No | Optional link to the Workspace that owns this email address. Nullable because emails imported from global enrichment sources (or created before the workspace-scope requirement was introduced) may not carry a workspace reference. When present, it forms part of the uniqueness key for find-or-create deduplication. Targets `core_api.workspaces`. | | company\_emails | to-many (company\_email) | — | Pivot records linking this email address to one or more Company rows. Each CompanyEmail carries `is_primary`, `is_verify`, and `label` metadata. A partial unique index on `company_emails (company_pk) WHERE deleted_at IS NULL AND is_primary IS TRUE` enforces at most one primary per company. Targets `core_api.company_emails`. | | person\_emails | to-many (person\_email) | — | Pivot records linking this email address to one or more People rows. Each PersonEmail carries `is_primary`, `is_verify`, and `label` (enum: work / personal / other) metadata. Partial unique index `uniq_person_emails_primary_person` enforces at most one primary per person. Targets `core_api.person_emails`. | ### System-computed * email\_id is generated by gen\_random\_uuid() at the database default level and also explicitly set in EmailRepository.findOrCreate() via Node.js randomUUID() to ensure the value is available before flush. * created\_at is set on insert by MikroORM onCreate: () => new Date(). * updated\_at is set on both insert and update by MikroORM onCreate/onUpdate lifecycle hooks. * deleted\_at is null on creation; set to the current timestamp by EmailRepository.delete() as a soft-delete operation. Live queries must always filter deleted\_at: null. * Find-or-create deduplication: EmailRepository.findOrCreate() normalizes the input address via .trim().toLowerCase(), then queries (email, workspace, deleted\_at: null). When a workspace is not provided the scope falls back to (email, deleted\_at: null) globally. A new Email row is only created when no matching live record is found. * Composite views: the `emails` records root exposes two composites — `composite_companies_list` (relation\_list of company\_id + company.name, via company\_emails reverse) and `composite_people_list` (relation\_list of person\_id + full\_name, via person\_emails reverse). These are defined in composites.yml and reconstructed at query time. * Hasura RLS for the user role filters deleted\_at IS NULL and resolves workspace tenancy indirectly: an email is visible if any linked company\_email or person\_email traces to a workspace matching X-Hasura-Workspace-Id (direct or parent-workspace cascade via workspace\_group). ## Example ```json theme={null} { "data": { "type": "email", "id": "b3f2a1c4-9d7e-4f01-8a2b-3c5d6e7f8a9b", "attributes": { "email": "sophie.martin@qonto.com", "created_at": "2025-11-14T09:32:11.000Z", "updated_at": "2025-11-14T09:32:11.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "company_emails": { "data": [ { "type": "company_email", "id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a" } ] }, "person_emails": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/Email.ts` · domain: financial-graph · tier: Supporting # EnrichmentTask Source: https://docs.wellapp.ai/object-reference/enrichment_tasks EnrichmentTask is the durable work-item record for Well's asynchronous enrichment pipeline EnrichmentTask is the durable work-item record for Well's asynchronous enrichment pipeline. Each row represents one unit of background AI or data-processing work — logo resolution, AI company/people/invoice extraction, OCR, bank-sync reconciliation, provider scoring, custom column compute, field rule evaluation, or monthly invoice closure — targeted at an arbitrary entity identified by the polymorphic (`entity_type`, `entity_id`) pair. Tasks are scoped to a `Workspace`, can be grouped into a `batch_id` (one Magic-button press), and support parent/subtask nesting via a self-referencing `parent_task` relationship. The enrichment pipeline writes and advances all lifecycle states; users and operators observe tasks read-only. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | EnrichmentTask | | Resource type (JSON:API `type`) | `enrichment_task` | | Collection / records root | — (not a records root) | | REST base | `/v1/enrichment-tasks` | | Entity class | `EnrichmentTask` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------- | ---------- | | List | `GET /v1/enrichment-tasks` | 🟡 Planned | | Retrieve | `GET /v1/enrichment-tasks/{id}` | 🟡 Planned | | Create | `POST /v1/enrichment-tasks` | 🟡 Planned | | Update | `PATCH /v1/enrichment-tasks/{id}` | 🟡 Planned | | Delete | `DELETE /v1/enrichment-tasks/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | enrichment\_task\_id | UUID (🔒 system) | ✅ Yes | UNIQUE | — | Public stable identifier. Auto-generated by `gen_random_uuid()` at insert. This is the `id` exposed in JSON:API responses; the internal `pk` is never surfaced. | | entity\_type | string (varchar 100) | ✅ Yes | max 100 chars; NOT NULL; composite index with entity\_id (`enrichment_tasks_entity_idx`) | — | Polymorphic discriminator identifying the kind of entity this task targets (e.g. `company`, `person`, `invoice`, `document`, `transaction`). Pairs with `entity_id` to form the generic entity reference introduced in Migration20260323100000 to replace per-type FK columns. | | entity\_id | string (varchar 255) | ✅ Yes | max 255 chars; NOT NULL; composite index with entity\_type; partial unique index on (`entity_type`, `entity_id`, `enrichment_type`) WHERE `status = 'pending' AND deleted_at IS NULL AND enrichment_type = 'monthly_close'` enforces one active monthly\_close task per target month | — | Public UUID of the target entity (the `*_id` column of the referenced row, e.g. `company_id`, `person_id`, `invoice_id`). Stored as a string to support multiple entity types without per-type FK constraints. Composite index with `entity_type`. | | status | enum (🔒 system) — native Postgres type `enrichment_task_status_enum` | ✅ Yes | DEFAULT 'pending'; NOT NULL | pending \| processing \| completed \| awaiting\_approval \| failed \| rejected | Lifecycle state of the enrichment task. Default `pending`. Transitions are driven exclusively by the enrichment pipeline workers; users cannot write this field. | | enrichment\_type | string (text) — backed by Postgres type `enrichment_type_enum` historically but stored as text on the column | ✅ Yes | NOT NULL | logo \| ai\_company \| ai\_people \| ai\_invoice \| ocr\_extract \| reconcile \| provider\_score \| document\_reconciliation\_backfill \| monthly\_close \| monthly\_close\_backfill \| custom\_column \| field\_rule | Identifies the enrichment worker that should process this task. Determines which Cloud Tasks handler is dispatched. See `EnrichmentTypeEnum` for the full controlled vocabulary. | | input | jsonb | ⚪ No | nullable | — | Worker input payload. Shape is worker-specific. For `monthly_close` the dedup key is `input->>'record_id'` (month label); a JSONB expression partial index (`idx_enrichment_tasks_input_record_id`) covers lookups on that path within a workspace + status window. | | output | jsonb | ⚪ No | nullable | — | Worker output payload written on task completion. Shape is worker-specific. For `monthly_close_backfill`, holds aggregate fan-out counts and request parameters. | | target\_fields | jsonb (string\[]) | ⚪ No | nullable | — | Optional list of field names the enrichment worker should populate or re-evaluate. Used by `custom_column` and `field_rule` workers to scope work to a subset of columns. | | error | text | ⚪ No | nullable | — | Error message or stack trace written by the worker when the task transitions to `failed`. Null on success. | | source\_channel | enum — native Postgres type `source_channel_enum` | ⚪ No | nullable | company\_create \| person\_create \| document\_upload \| ocr\_completion \| bank\_sync \| email\_import \| mcp\_hub \| cell\_edit \| manual | Records which product surface triggered the task. Used for attribution and observability. Added in Migration20260319120000. | | input\_hash | text | ⚪ No | nullable; index `enrichment_tasks_input_hash_idx` | — | Deduplication fingerprint of the `input` payload. Allows workers to detect duplicate enqueues with identical inputs and skip redundant work. Added in Migration20260319120000. | | description | text | ⚪ No | nullable | — | Human-readable markdown summary of the enrichment task, populated by the worker or the pipeline orchestrator for display in the UI. Added in Migration20260327100000. | | batch\_id | UUID | ⚪ No | nullable; index `enrichment_tasks_batch_id_idx`; composite index `idx_enrichment_tasks_batch_status` on (batch\_id, status) | — | Groups tasks triggered by a single user action (Magic button press). All tasks sharing a `batch_id` were enqueued together and can be tracked collectively. Index `idx_enrichment_tasks_batch_status` covers hot-path batch completion detection. Added in Migration20260327100000. | | created\_at | timestamptz (🔒 system) | ✅ Yes | NOT NULL; set once on insert | — | Row creation timestamp set by MikroORM `onCreate` hook. Not writable after insert. | | updated\_at | timestamptz (🔒 system) | ⚪ No | nullable; updated on every write | — | Last modification timestamp, maintained automatically by MikroORM `onUpdate` hook on every flush. | | deleted\_at | timestamptz | ⚪ No | nullable | — | Soft-delete timestamp. When set the row is logically deleted and filtered out of standard queries. The partial unique index on `monthly_close` tasks is scoped to `deleted_at IS NULL`. | | completed\_at | timestamptz | ⚪ No | nullable | — | Timestamp written by the worker when the task reaches `completed`, `failed`, or `rejected`. Distinct from `updated_at` to allow precise pipeline latency measurement. | ### Relationships | Name | Type | Required | Description | | ------------ | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace that owns this enrichment task. All task queries are tenant-scoped via this relationship. `deleteRule: cascade` — deleting a workspace removes all its tasks. FK column `workspace_pk`. | | parent\_task | to-one (ManyToOne, self-referencing) | ⚪ No | Optional reference to a parent `EnrichmentTask`. Used by the Monthly Invoice Closure pipeline to link per-counterparty sub-tasks to their macro parent task. FK column `parent_task_pk`. `deleteRule: set null` — deleting a parent task nullifies the FK on children without cascading deletion. Added in Migration20260327100000. | | subtasks | to-many (OneToMany, self-referencing) | — | Inverse collection of child `EnrichmentTask` rows that reference this task as their `parent_task`. Populated for macro tasks in the Monthly Invoice Closure pipeline. Mapped by `parent_task` on the child side. | ### System-computed * enrichment\_task\_id — auto-generated by gen\_random\_uuid() Postgres default at INSERT; never set by application code * created\_at — set by MikroORM onCreate hook; never writable after insert * updated\_at — set and maintained by MikroORM onUpdate hook on every flush * deleted\_at — soft-delete; set by the pipeline or admin tooling, never by user PATCH * status — default 'pending' at creation; all state transitions (pending → processing → completed / failed / rejected / awaiting\_approval) are driven exclusively by enrichment pipeline workers via Cloud Tasks handlers * input\_hash — computed by the enqueuing service as a fingerprint of the input payload for dedup detection; not computed by the database * batch\_id — assigned at enqueue time by the orchestrator when multiple tasks are triggered together (Magic button); not user-assignable * completed\_at — written by the worker on task terminal state; not set by application business logic outside the worker * entity\_type / entity\_id — set at enqueue time from the triggering entity's public UUID; the generic polymorphic reference replaced per-type FK columns (company\_pk, person\_pk, document\_pk, invoice\_pk, transaction\_pk) in Migration20260323100000 * subtasks collection — populated by ORM from the OneToMany inverse of parent\_task; not a stored column ## Example ```json theme={null} { "data": { "type": "enrichment_task", "id": "c3a1f9e2-4b7d-4e2a-8f1c-9a0b3d5e7f21", "attributes": { "enrichment_task_id": "c3a1f9e2-4b7d-4e2a-8f1c-9a0b3d5e7f21", "entity_type": "company", "entity_id": "a1b2c3d4-0000-0000-0000-000000000001", "status": "completed", "enrichment_type": "ai_company", "input": { "company_id": "a1b2c3d4-0000-0000-0000-000000000001" }, "output": { "domain": "acme.com", "registered_name": "Acme Corp" }, "target_fields": ["domain", "registered_name"], "error": null, "source_channel": "company_create", "input_hash": "sha256:abc123", "description": null, "batch_id": "f7e6d5c4-3b2a-1908-7654-321098fedcba", "created_at": "2026-05-15T10:30:00.000Z", "updated_at": "2026-05-15T10:31:05.000Z", "deleted_at": null, "completed_at": "2026-05-15T10:31:05.000Z" }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws-uuid-0001" } }, "parent_task": { "data": null } } } } ``` Source: `apps/api/src/database/entities/EnrichmentTask.ts` · domain: ingestion · tier: Infrastructure # Exchange Rate Source: https://docs.wellapp.ai/object-reference/exchange_rates ExchangeRate represents a daily FX conversion rate between two ISO 4217 currencies for a given date ExchangeRate represents a daily FX conversion rate between two ISO 4217 currencies for a given date. It is used primarily by the multi-currency accounting pipeline: invoices and invoice-transactions reference an ExchangeRate row via a FK to convert document-currency amounts into the workspace's accounting base currency. The workspace relation is nullable, allowing for global market-rate rows that are not scoped to a specific tenant, while workspace-specific overrides carry a workspace FK. It is a Supporting entity in the records graph, surfaced as the `exchange_rates` root and referenced as a relation from `invoices` and `invoice_transactions`. | Naming | Value | | ------------------------------- | -------------------- | | Object | Exchange Rate | | Resource type (JSON:API `type`) | `exchange_rate` | | Collection / records root | `exchange_rates` | | REST base | `/v1/exchange-rates` | | Entity class | `ExchangeRate` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ------------- | | List | `GET /v1/exchange-rates` | ✅ Implemented | | Retrieve | `GET /v1/exchange-rates/{id}` | ✅ Implemented | | Create | `POST /v1/exchange-rates` | 🟡 Planned | | Update | `PATCH /v1/exchange-rates/{id}` | 🟡 Planned | | Delete | `DELETE /v1/exchange-rates/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | exchange\_rate\_id | string, UUID | ✅ Yes | unique; default gen\_random\_uuid() | — | Public stable identifier for this exchange rate row. Generated server-side via gen\_random\_uuid(); never set by the client. | | source\_currency | string (CurrencyCodeEnum) | ✅ Yes | NOT NULL; must differ from target\_currency (CHECK source\_currency != target\_currency); maps to Postgres native enum currency\_code\_enum | ISO 4217 codes: USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, BGN, ... (full CurrencyCodeEnum, \~180+ values) | The currency being converted from. Together with target\_currency and rate\_date, forms part of the composite uniqueness key (scoped to workspace). | | target\_currency | string (CurrencyCodeEnum) | ✅ Yes | NOT NULL; must differ from source\_currency (CHECK source\_currency != target\_currency); maps to Postgres native enum currency\_code\_enum | ISO 4217 codes: USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, BGN, ... (full CurrencyCodeEnum, \~180+ values) | The currency being converted to. The rate expresses how many target\_currency units equal one source\_currency unit. | | rate | string (decimal, 18,8 precision) | ⚪ No | nullable; DECIMAL(18,8); CHECK rate > 0 (enforced in migration DDL) | — | The conversion rate from source\_currency to target\_currency on rate\_date. Stored as a decimal string to preserve full precision. A null value indicates the rate was recorded but the numeric value is not yet resolved. | | rate\_date | Date (date column) | ✅ Yes | NOT NULL; columnType date (date-only, no time component); part of the composite uniqueness key (workspace, source\_currency, target\_currency, rate\_date) | — | The calendar date for which this rate is valid. One row per (workspace, source\_currency, target\_currency, date) tuple. | | source | string | ⚪ No | nullable; length 100 | — | Free-text provenance label for the rate — e.g. the feed name, connector slug, or manual entry identifier. Used to distinguish ECB daily rates from connector-sourced or user-overridden rates. | | created\_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | set by MikroORM onCreate lifecycle hook; never writable after creation | — | Timestamp when the row was first persisted. Set server-side only. | | updated\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | set by MikroORM onCreate and onUpdate lifecycle hooks | — | Timestamp of the last mutation to this row. Auto-maintained by the ORM. | | deleted\_at | string (ISO 8601 datetime) \| null | ⚪ No | nullable; soft-delete sentinel; queries must filter deleted\_at IS NULL | — | Soft-delete timestamp. Non-null means this rate has been logically removed. All standard queries must include the deleted\_at IS NULL predicate. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No (nullable) | The Workspace that owns this exchange rate row. Nullable: a null workspace\_pk indicates a global market-rate record not scoped to a specific tenant. When set, combined with source\_currency, target\_currency, and rate\_date forms the unique constraint. FK: exchange\_rates.workspace\_pk → workspaces.pk. Index: idx\_exchange\_rates\_workspace\_date on (workspace\_pk, rate\_date). | ### System-computed * exchange\_rate\_id is generated server-side via gen\_random\_uuid() as the Postgres column default, with a JavaScript fallback randomUUID() set at entity construction time in the MikroORM entity. Never supplied by the client. * created\_at is set by the MikroORM onCreate lifecycle hook (new Date()) and never subsequently updated. * updated\_at is set by both onCreate and onUpdate lifecycle hooks, reflecting the timestamp of the most recent mutation. * deleted\_at is null by default. Soft-delete is applied by setting this field to a non-null timestamp; hard deletes are not used. All read queries must filter deleted\_at IS NULL. * Composite uniqueness constraint: UNIQUE(workspace\_pk, source\_currency, target\_currency, rate\_date) — enforced at the database level. This makes the table an upsert target: the FX pipeline can safely attempt INSERT ... ON CONFLICT (workspace\_pk, source\_currency, target\_currency, rate\_date) DO UPDATE. * rate carries a database-level CHECK (rate > 0) constraint added in Migration20260306100000 DDL, even though the column is nullable — when rate is non-null, it must be strictly positive. * The CHECK (source\_currency != target\_currency) constraint is enforced at the database level (Migration20260306100000). The MikroORM entity does not replicate this at the application layer; it is a DB-only guard. * The workspace relation is nullable (nullable: true on the @ManyToOne decorator), allowing global rates to exist without a workspace owner. Workspace-scoped rates take precedence over global rates in the FX resolution service. * Index idx\_exchange\_rates\_workspace\_date on (workspace\_pk, rate\_date) supports the FX rate lookup pattern: given a workspace and a date, resolve the applicable rate for a currency pair. * Invoices reference ExchangeRate via invoices.exchange\_rate\_pk FK (added in Migration20260306100000). InvoiceTransaction rows reference ExchangeRate via invoice\_transactions.exchange\_rate\_pk FK (added in Migration20260311100000\_data\_model\_v2\_accounting). ExchangeRate rows are pipeline-written; they are not created through the standard REST API by end users. * The composites.yml file defines two composites for the exchange\_rates root: composite\_invoices\_list (relation\_list of linked invoices) and composite\_invoice\_transactions\_list (relation\_list of linked invoice\_transactions). These are read-only aggregate views surfaced in the Records table. * The workspace relation on exchange\_rates is not guarded by the standard workspace-scoped Hasura RLS filter because the nullable workspace\_pk means global rows have no workspace. This is an intentional architectural exception documented in the entity design. ## Example ```json theme={null} { "data": { "type": "exchange_rate", "id": "c3e1b2f4-09d7-4a8e-b5e0-7f2a1d3c6890", "attributes": { "exchange_rate_id": "c3e1b2f4-09d7-4a8e-b5e0-7f2a1d3c6890", "source_currency": "USD", "target_currency": "EUR", "rate": "0.92150000", "rate_date": "2026-05-15", "source": "ecb-daily-feed", "created_at": "2026-05-15T08:00:14.000Z", "updated_at": "2026-05-15T08:00:14.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-1111-2222-3333-444455556666" } } } } } ``` Source: `apps/api/src/database/entities/ExchangeRate.ts` · domain: financial-graph · tier: Supporting # ExtractPrompt Source: https://docs.wellapp.ai/object-reference/extract_prompts ExtractPrompt stores named, versioned AI extraction prompt templates that drive the document-extraction pipeline when ingesting invoices and documents ExtractPrompt stores named, versioned AI extraction prompt templates that drive the document-extraction pipeline when ingesting invoices and documents. Each record is scoped to a `type` (e.g. the extraction category), carries the raw prompt text, a monotone `version` counter, an `is_active` flag, and an `is_default` flag marking the system-wide fallback. Records are either workspace-scoped (a workspace-level customisation) or global (no workspace FK, `is_default = true`). The entity is owned exclusively by the extraction pipeline and seed layer; no user-facing PATCH endpoint exists. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ExtractPrompt | | Resource type (JSON:API `type`) | `extract_prompt` | | Collection / records root | — (not a records root) | | REST base | `/v1/extract-prompts` | | Entity class | `ExtractPrompt` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------- | ---------- | | List | `GET /v1/extract-prompts` | 🟡 Planned | | Retrieve | `GET /v1/extract-prompts/{id}` | 🟡 Planned | | Create | `POST /v1/extract-prompts` | 🟡 Planned | | Update | `PATCH /v1/extract-prompts/{id}` | 🟡 Planned | | Delete | `DELETE /v1/extract-prompts/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | string (UUID) — 🔒 system | ✅ Yes | PK; gen\_random\_uuid() default; immutable after creation | Any valid UUID v4 | Public stable identifier for the prompt record. Exposed as `id` in JSON:API; backed by the `pk` UUID column. | | type | string | ✅ Yes | varchar(100); NOT NULL; single-column index; composite indexes (type, is\_active) and (type, version) | Free-form string — conventionally an extraction category slug such as `invoice`, `receipt`, `bank_statement` | Categorisation key grouping prompt versions under a logical extraction pipeline stage. Used by the extraction service to look up the current active prompt for a given category. | | prompt | text | ✅ Yes | PostgreSQL TEXT; NOT NULL; no length cap | Any non-null string (typically a multi-line AI system or user prompt) | The raw LLM prompt text sent to the extraction model. May contain field-extraction instructions, output-format directives, or chain-of-thought scaffolding. | | version | integer | ✅ Yes | NOT NULL; DEFAULT 1; composite index (type, version); increments on each prompt revision | Positive integer ≥ 1 | Monotone version counter within a `type`. Allows the pipeline to track prompt iterations and roll back to a prior version by `is_active` flag manipulation. | | is\_active | boolean | ✅ Yes | NOT NULL; DEFAULT false; composite index (type, is\_active) | true \| false | Indicates whether this is the currently active prompt for its `type`. The extraction service queries `type + is_active = true` to select the live prompt. Only one record per (type, workspace) combination should be active at a time — enforced by application logic, not a DB constraint. | | is\_default | boolean | ✅ Yes | NOT NULL; DEFAULT false | true \| false | Marks the system-wide default prompt for its `type`. Default prompts have no `workspace` FK (`workspace = null`). Workspace-scoped prompts shadow the default when `is_active = true`. | | created\_at | timestamptz — 🔒 system | ✅ Yes | NOT NULL; set once via `onCreate` hook; never updated | ISO 8601 timestamp | Wall-clock UTC timestamp of row creation. Set automatically by MikroORM's `onCreate` lifecycle hook. | | updated\_at | timestamptz — 🔒 system | ⚪ No | NULLABLE; set on first write via `onCreate`, refreshed on every subsequent write via `onUpdate` | ISO 8601 timestamp or null | Wall-clock UTC timestamp of the last update. NULL until the first update after creation. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ⚪ No — nullable | Optional FK to the `workspaces` table (`workspace_pk` int). When NULL the prompt is a global default accessible to all workspaces. When set, the prompt is a workspace-level customisation that overrides the global default for that workspace's extraction pipeline. FK constraint: ON UPDATE CASCADE, ON DELETE SET NULL. | ### System-computed * id (pk): generated by gen\_random\_uuid() at INSERT time; immutable. * version: defaults to 1; incremented by the extraction pipeline or seed layer on prompt revision — not auto-incremented by the DB. * is\_active: defaults to false; toggled by the pipeline when promoting a new prompt version. * is\_default: defaults to false; set by the seed layer for system-wide baseline prompts. * created\_at: set by MikroORM onCreate hook; never subsequently mutated. * updated\_at: set by MikroORM onCreate hook on first persist; refreshed on every subsequent onUpdate event. * workspace FK: set null automatically by the DB (ON DELETE SET NULL) when the parent workspace is deleted. ## Example ```json theme={null} { "data": { "type": "extract_prompt", "id": "a3e1c7f2-84b0-4d2e-9c51-0f3b2d7e1a88", "attributes": { "type": "invoice", "prompt": "Extract the following fields from the invoice: issue_date, due_date, grand_total, tax_total, vendor_name, invoice_number. Return structured JSON only.", "version": 3, "is_active": true, "is_default": false, "created_at": "2025-09-21T17:50:44.000Z", "updated_at": "2025-10-14T09:12:03.000Z" }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "9f3b2d7e-1a88-4d2e-9c51-0f3b2d7e1a88" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/ExtractPrompt.ts` · domain: ingestion · tier: Infrastructure # FieldRuleValue Source: https://docs.wellapp.ai/object-reference/field_rule_values FieldRuleValue stores the computed or user-supplied value for a single custom-column rule applied to a single record FieldRuleValue stores the computed or user-supplied value for a single custom-column rule applied to a single record. Each row binds a FieldRule (the definition of a custom AI column) to a specific record by its string identifier and record root type, holding the computed JSONB value. It is the output store for the AI custom-column enrichment pipeline: the pipeline writes rows here after computing values; nothing else creates or modifies them via user interaction. There is no soft-delete column — rows are hard-deleted when their parent FieldRule is cascade-deleted. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | FieldRuleValue | | Resource type (JSON:API `type`) | `field_rule_value` | | Collection / records root | — (not a records root) | | REST base | `/v1/field-rule-values` | | Entity class | `FieldRuleValue` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------- | ---------- | | List | `GET /v1/field-rule-values` | 🟡 Planned | | Retrieve | `GET /v1/field-rule-values/{id}` | 🟡 Planned | | Create | `POST /v1/field-rule-values` | 🟡 Planned | | Update | `PATCH /v1/field-rule-values/{id}` | 🟡 Planned | | Delete | `DELETE /v1/field-rule-values/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | value\_id | string (UUID) — 🔒 system | ✅ Yes | UNIQUE; defaultRaw gen\_random\_uuid() | — | Public UUID identifier for the row. Generated by the database on insert; never writable by callers. | | record\_id | string | ✅ Yes | max length 255; composite UNIQUE with field\_rule (field\_rule\_values\_rule\_record\_unique) | — | The public UUID string of the record (e.g. a company\_id or person\_id) to which this computed value applies. Together with field\_rule this pair is unique in the table, enabling race-condition-safe upserts. | | root | string | ✅ Yes | max length 50; part of composite hot-path index idx\_field\_rule\_values\_root\_record\_rule | — | The record root type (e.g. 'companies', 'people') to which this value belongs. Denormalized from FieldRule.root for hot-path batch queries in the AI column-context assembly pipeline. | | value | jsonb (nullable) | ⚪ No | nullable | — | The computed value for the custom column on this record, stored as arbitrary JSONB. Null when the pipeline has not yet produced a value or when the result was explicitly null. Shape is determined by the parent FieldRule's column\_type. | | created\_at | datetime — 🔒 system | ✅ Yes | set by onCreate hook; not nullable | — | Timestamp when this value row was first written by the enrichment pipeline. | | updated\_at | datetime — 🔒 system | ⚪ No | nullable; set by onCreate and onUpdate hooks | — | Timestamp of the most recent update to this row by the pipeline. Null if the row has never been updated after creation. | ### Relationships | Name | Type | Required | Description | | ----------- | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | field\_rule | to-one (ManyToOne) | Yes — NOT NULL; deleteRule: cascade | The FieldRule that defines the custom column whose computed value this row stores. Cascade deletion means all FieldRuleValue rows are hard-deleted when their parent FieldRule is removed. Part of the composite unique constraint (field\_rule, record\_id). | ### System-computed * value\_id: auto-generated UUID via gen\_random\_uuid() on insert * created\_at: set by MikroORM onCreate hook; not user-settable * updated\_at: set by MikroORM onCreate and onUpdate hooks; not user-settable * No soft-delete (deleted\_at column absent): rows are hard-deleted on cascade from FieldRule deletion * Hot-path composite index idx\_field\_rule\_values\_root\_record\_rule (root, record\_id, field\_rule\_pk) added in Migration20260416000000 for AI custom-column batch context assembly * Unique constraint field\_rule\_values\_rule\_record\_unique (field\_rule\_pk, record\_id) added in Migration20260330153340 to enable race-condition-safe upserts from the enrichment pipeline ## Example ```json theme={null} { "data": { "type": "field_rule_value", "id": "a3f1c2d4-8e5b-4a7f-92b1-0123456789ab", "attributes": { "value_id": "a3f1c2d4-8e5b-4a7f-92b1-0123456789ab", "record_id": "c7e9a1b2-3d4f-4e5a-8b9c-0a1b2c3d4e5f", "root": "companies", "value": { "text": "B2B SaaS company focused on developer tooling", "confidence": 0.92 }, "created_at": "2026-04-02T14:23:11.000Z", "updated_at": "2026-04-02T14:23:11.000Z" }, "relationships": { "field_rule": { "data": { "type": "field_rule", "id": "f8a2b3c4-1d2e-3f4a-5b6c-7d8e9f0a1b2c" } } } } } ``` Source: `apps/api/src/database/entities/FieldRuleValue.ts` · domain: workspace · tier: Infrastructure # FieldRule Source: https://docs.wellapp.ai/object-reference/field_rules A `FieldRule` stores a workspace-scoped AI grounding configuration for a single column (field_key) within a record root (e.g A `FieldRule` stores a workspace-scoped AI grounding configuration for a single column (field\_key) within a record root (e.g. `invoices`, `companies`). It carries three optional JSONB payloads — `rules_config`, `formula_config`, and `format_config` — each represented as a rich-text document tree (Tiptap/ProseMirror) or a plain-text prompt string. The entity is owned by the user: it is created or replaced via `PUT /v1/field-rules/:root/:fieldKey` and soft-deleted via `DELETE` on the same path. A partial unique index on `(workspace_pk, root, field_key) WHERE deleted_at IS NULL` enforces one active rule per field per workspace. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | FieldRule | | Resource type (JSON:API `type`) | `field_rule` | | Collection / records root | — (not a records root) | | REST base | `/v1/field-rules` | | Entity class | `FieldRule` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------- | ---------- | | List | `GET /v1/field-rules` | 🟡 Planned | | Retrieve | `GET /v1/field-rules/{id}` | 🟡 Planned | | Create | `POST /v1/field-rules` | 🟡 Planned | | Update | `PATCH /v1/field-rules/{id}` | 🟡 Planned | | Delete | `DELETE /v1/field-rules/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | --------------- | -------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | field\_rule\_id | string (UUID) — 🔒 system | ✅ Yes | unique; default gen\_random\_uuid() | — | Public stable identifier for the field rule. Generated by the database on creation. | | root | string | ✅ Yes | max length 50; part of partial-unique index (workspace\_pk, root, field\_key) WHERE deleted\_at IS NULL | — | The record root this rule applies to (e.g. 'invoices', 'companies', 'transactions'). Matches the root values accepted by /v1/records/query. | | field\_key | string | ✅ Yes | max length 255; part of partial-unique index (workspace\_pk, root, field\_key) WHERE deleted\_at IS NULL | — | Dot-notation path identifying the target column within the root (e.g. 'invoices.issuer.name'). Together with root and workspace, uniquely identifies one active rule. | | column\_name | string | ✅ Yes | max length 255 | — | Human-readable display label for the column this rule targets, used for context in AI grounding prompts. | | column\_type | string | ✅ Yes | max length 50 | — | Data type of the target column (e.g. 'text', 'number', 'date'). Used to guide the AI extraction format. | | rules\_config | jsonb (FieldRuleConfig — RichTextNode \| string \| null) | ⚪ No | nullable | — | AI grounding instructions for extraction rules. Stored as a Tiptap/ProseMirror rich-text document tree (RichTextNode) or a plain-text prompt string. Supports @-mention nodes in the document tree. | | formula\_config | jsonb (FieldRuleConfig — RichTextNode \| string \| null) | ⚪ No | nullable | — | AI grounding configuration for formula-based computation of the field value. Same shape as rules\_config — rich-text or plain string. | | format\_config | jsonb (FieldRuleConfig — RichTextNode \| string \| null) | ⚪ No | nullable | — | AI grounding configuration for output formatting instructions (e.g. currency symbol placement, date format). Same shape as rules\_config. | | created\_at | datetime — 🔒 system | ✅ Yes | set on insert via onCreate hook; not null | — | Timestamp of row creation. Set automatically by the MikroORM onCreate lifecycle hook. | | updated\_at | datetime — 🔒 system | ✅ Yes | set on insert and update via onCreate/onUpdate hooks; not null | — | Timestamp of last modification. Set automatically on every write. | | deleted\_at | datetime \| null — 🔒 system | ⚪ No | nullable; soft-delete sentinel; included in the partial-unique index condition (WHERE deleted\_at IS NULL) | — | Soft-delete timestamp. When set, the rule is logically deleted and excluded from the active partial-unique index, allowing a new rule for the same (workspace, root, field\_key) to be created. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace this field rule belongs to. FK: field\_rules.workspace\_pk → workspaces.pk. Enforces multi-tenant isolation — all queries are scoped to req.workspace. ON UPDATE CASCADE. | ### System-computed * field\_rule\_id: generated by gen\_random\_uuid() on insert; never client-provided * created\_at: set by MikroORM onCreate hook to new Date() at insert time * updated\_at: set by MikroORM onCreate and onUpdate hooks; reflects last write timestamp * deleted\_at: written by FieldRuleService.deleteRule() for soft deletion; not a client-settable field * Partial unique index idx\_field\_rules\_workspace\_root\_field\_key enforces exactly one active rule per (workspace, root, field\_key) when deleted\_at IS NULL; soft-deleting a rule releases the slot for re-creation * Upsert semantics in FieldRuleService.upsertRule(): finds existing active rule by (root, field\_key, workspace); updates in-place if found, creates a new row otherwise * On upsert, FieldRuleService.fireAndForgetRecompute() is triggered automatically to re-run AI extraction for affected records — this is a side effect not visible in the API response ## Example ```json theme={null} { "data": { "id": "a3f1b2c4-8e12-4d5a-9b3f-1c2e3d4f5a6b", "type": "field_rule", "attributes": { "field_rule_id": "a3f1b2c4-8e12-4d5a-9b3f-1c2e3d4f5a6b", "root": "invoices", "field_key": "invoices.issuer.name", "column_name": "Issuer", "column_type": "text", "rules_config": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Extract the legal company name from the document header." } ] } ] }, "formula_config": null, "format_config": null, "created_at": "2026-04-01T10:30:00.000Z", "updated_at": "2026-05-15T14:22:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "id": "7e4c9a12-3b5d-4e6f-8a1b-2c3d4e5f6a7b", "type": "workspace" } } } } } ``` Source: `apps/api/src/database/entities/FieldRule.ts` · domain: workspace · tier: Infrastructure # Invoice Item Source: https://docs.wellapp.ai/object-reference/invoice_items InvoiceItem represents a single line item on an invoice — a discrete charge for a product or service with its own quantity, price, tax, and period metadata InvoiceItem represents a single line item on an invoice — a discrete charge for a product or service with its own quantity, price, tax, and period metadata. It belongs exclusively to one Invoice via a mandatory many-to-one relation. Key associations are: Invoice (parent document), LedgerAccount (accounting chart of accounts classification), TaxRate (the well-catalog tax rate that was applied), and Media (an optional supporting document such as an image or receipt). The accounting\_classification JSONB field carries the AI-produced Well-taxonomy posting intent used by the journal-entry builder to classify the line for double-entry accounting. | Naming | Value | | ------------------------------- | ------------------- | | Object | Invoice Item | | Resource type (JSON:API `type`) | `invoice_item` | | Collection / records root | `invoice_items` | | REST base | `/v1/invoice-items` | | Entity class | `InvoiceItem` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ------------- | | List | `GET /v1/invoice-items` | ✅ Implemented | | Retrieve | `GET /v1/invoice-items/{id}` | ✅ Implemented | | Create | `POST /v1/invoice-items` | 🟡 Planned | | Update | `PATCH /v1/invoice-items/{id}` | 🟡 Planned | | Delete | `DELETE /v1/invoice-items/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------------- | ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invoice\_item\_id | string, UUID | ✅ Yes | unique; generated by gen\_random\_uuid() at creation | — | Public stable identifier for this invoice line item. Used in all API surfaces and references. | | line\_id | string | ✅ Yes | max length 50; table-wide composite-unique with invoice (no two lines — active or deleted — on the same invoice share a line\_id) | — | Provider-assigned or pipeline-generated identifier for this line within the parent invoice. Used to deduplicate re-ingested lines. The unique constraint is not partial: a soft-deleted line\_id blocks reuse on the same invoice until the row is hard-deleted. | | sku | string | ⚪ No | max length 100 | — | Stock-keeping unit or product code from the originating system (ERP, e-commerce, MCP connector). | | name | string | ✅ Yes | max length 255 | — | Human-readable label for the line item. Mapped directly from the originating document's line description. | | description | string | ⚪ No | max length 1000 | — | Extended free-text description of the line item, supplying additional context beyond the name. | | unit\_price | decimal(15,2) | ✅ Yes | CHECK unit\_price >= 0 | — | Price per unit in the invoice's currency, before any discount or tax. Always non-negative. | | currency | string, enum (CurrencyCodeEnum) | ✅ Yes | PostgreSQL native enum currency\_code\_enum | ISO 4217 three-letter codes, e.g. USD, EUR, GBP, JPY, CHF, CAD, AUD, … | Currency of unit\_price, line\_total, tax\_amount, discount, and quantity-derived amounts on this line. | | unit | string, enum (InvoiceLineUnitEnum) | ⚪ No | PostgreSQL native enum invoice\_line\_unit\_enum; nullable | EA, PC, SET, PR, DZ, C62, MIL, KT, PK, BX, HUR, MIN, SEC, DAY, WEE, MON, ANN, QT, KGM, GRM, TNE, LBR, ONZ, CWT, STN, LTN, MTR, CMT, MMT, KMT, INH, FOT, YRD, SMI, MTK, CMK, INK, FTK, YDK, ACR, HAR, LTR, MLT, MTQ, CMQ, INQ, FTQ, YDQ, GAL, PT, BYT, KBY, MBY, GBY, TBY, BIT, KBI, MBI, GBI, KWH, MWH, WHR, KWT, MWT, BTU, CAL, CEL, FAH, KEL, BAR, PSI, PAL, SRV, LIC, USR, SES, TXN, REQ, PAG, VIS, CLI, IMP, PTC, BPS, SHR, LOT, PNT, TOL, MOL, PPM, PPB, PH, UNT, OTH, NA | UN/CEFACT unit-of-measure code for the quantity on this line. Covers physical quantities, time, data, and service units. | | quantity | decimal(15,2) | ⚪ No | CHECK quantity >= 0; nullable | — | Number of units. Stored as decimal(15,2) to accommodate fractional service quantities. Populated by the pipeline ingestion from connector data. | | min\_quantity | decimal(15,2) | ⚪ No | CHECK min\_quantity >= 0; nullable | — | Minimum purchasable quantity for this line, used when the invoice carries tiered or min/max quantity brackets. | | max\_quantity | decimal(15,2) | ⚪ No | CHECK max\_quantity >= min\_quantity; nullable | — | Maximum purchasable quantity for this line. Must be >= min\_quantity when both are present. | | line\_total | decimal(15,2) | ⚪ No | CHECK line\_total >= 0; nullable | — | Total amount for this line (unit\_price × quantity − discount), excl. tax. Populated by the pipeline; may differ from a client-computed product when rounding rules apply. | | discount | decimal(15,2) | ⚪ No | CHECK discount >= 0; nullable; default 0 | — | Discount amount applied to this line, expressed as an absolute monetary value in the line's currency. Always non-negative. | | tax\_rate | decimal(5,2) | ⚪ No | CHECK tax\_rate >= 0 AND tax\_rate \<= 100; nullable | 0.00 – 100.00 | Percentage rate of tax applied to this line, e.g. 20.00 for 20% VAT. Distinct from the applied\_tax\_rate relationship which links to the well-catalog tax rate entry. | | tax\_category | string, enum (TaxCategoryEnum) | ⚪ No | PostgreSQL native enum tax\_category\_enum; nullable | standard, reduced, super\_reduced, zero\_rated, exempt, reverse\_charge, out\_of\_scope, government, municipal, regulatory, statutory, administrative, medical\_exempt, medical\_reduced, medical\_standard, pharmaceutical, hospital, dental, veterinary, education\_exempt, education\_reduced, books, cultural, research, library, food\_basic, food\_standard, food\_luxury, beverages\_non\_alcoholic, beverages\_alcoholic, restaurant, catering, property\_residential, property\_commercial, construction\_new, construction\_renovation, land, property\_management, transport\_public, transport\_passenger, transport\_freight, vehicle\_sales, vehicle\_parts, fuel, parking, utilities\_domestic, utilities\_commercial, water, sewage, waste\_management, telecommunications, energy\_renewable, financial\_exempt, insurance\_exempt, investment, banking, credit, foreign\_exchange, software\_license, software\_saas, digital\_services, cloud\_computing, data\_processing, telecommunications\_digital, electronic\_delivery, manufacturing, industrial\_equipment, raw\_materials, chemicals, mining, agriculture, forestry, entertainment, sports, gambling, tourism, hospitality, recreation, export, import, intrastat, customs, free\_trade\_zone, diplomatic, legal\_services, accounting, consulting, professional, notary, mixed\_rate, threshold\_based, seasonal, promotional, margin\_scheme, reverse\_auction, unknown, pending, other, not\_applicable | Semantic classification of the tax treatment on this line, aligned to EU/global tax-code taxonomy. | | tax\_scheme | string, enum (TaxSchemeEnum) | ⚪ No | PostgreSQL native enum tax\_scheme\_enum; nullable | VAT, EU\_VAT, UK\_VAT, MOSS\_VAT, OSS\_VAT, IOSS\_VAT, GST, AU\_GST, CA\_GST, CA\_HST, CA\_PST, CA\_QST, IN\_GST, IN\_CGST, IN\_SGST, IN\_IGST, IN\_UTGST, SG\_GST, MY\_GST, MY\_SST, NZ\_GST, SALES\_TAX, US\_STATE\_TAX, US\_LOCAL\_TAX, US\_USE\_TAX, CA\_RETAIL\_TAX, NY\_SALES\_TAX, TX\_SALES\_TAX, JCT, KR\_VAT, CN\_VAT, RU\_VAT, BR\_ICMS, BR\_IPI, BR\_PIS\_COFINS, MX\_IVA, AR\_IVA, CL\_IVA, EXCISE\_TAX, LUXURY\_TAX, SIN\_TAX, CARBON\_TAX, FUEL\_TAX, TOBACCO\_TAX, ALCOHOL\_TAX, DIGITAL\_TAX, CORPORATE\_TAX, WITHHOLDING\_TAX, BRANCH\_PROFITS\_TAX, TURNOVER\_TAX, GROSS\_RECEIPTS\_TAX, CUSTOMS\_DUTY, IMPORT\_DUTY, EXPORT\_DUTY, ANTI\_DUMPING\_DUTY, COUNTERVAILING\_DUTY, TARIFF, PROPERTY\_TAX, TRANSFER\_TAX, STAMP\_DUTY, INHERITANCE\_TAX, GIFT\_TAX, WEALTH\_TAX, PAYROLL\_TAX, SOCIAL\_SECURITY\_TAX, UNEMPLOYMENT\_TAX, DISABILITY\_TAX, MEDICARE\_TAX, MUNICIPAL\_TAX, CITY\_TAX, COUNTY\_TAX, DISTRICT\_TAX, TOURIST\_TAX, OCCUPANCY\_TAX, FINANCIAL\_TRANSACTION\_TAX, BANK\_TAX, INSURANCE\_PREMIUM\_TAX, TELECOM\_TAX, UTILITY\_TAX, AVIATION\_TAX, SHIPPING\_TAX, ENVIRONMENTAL\_TAX, PLASTIC\_TAX, PACKAGING\_TAX, WASTE\_TAX, CONGESTION\_TAX, OTHER, MIXED, UNKNOWN, NONE, PENDING | The tax framework under which the line is taxed, e.g. EU\_VAT, GST, US\_STATE\_TAX. Covers 90+ global tax schemes. | | tax\_amount | decimal(15,2) | ⚪ No | CHECK tax\_amount >= 0; nullable | — | Absolute tax amount for this line in the line's currency, derived from unit\_price × quantity × tax\_rate / 100. | | accounting\_unit\_price | decimal(15,2) | ⚪ No | nullable | — | Unit price converted to the workspace's functional accounting currency. Set by the FX-matching pipeline when the invoice currency differs from the workspace currency. | | accounting\_line\_total | decimal(15,2) | ⚪ No | nullable | — | Line total converted to the workspace's functional accounting currency. Parallel to accounting\_unit\_price for full multi-currency double-entry support. | | period\_start | timestamp | ⚪ No | nullable | — | Start of the service period covered by this line item. Used for subscription, retainer, and recurring-service invoices. | | period\_end | timestamp | ⚪ No | nullable | — | End of the service period covered by this line item. Paired with period\_start for accrual accounting period allocation. | | accounting\_classification | jsonb | ⚪ No | nullable; partial functional index on (accounting\_classification->>'status') WHERE deleted\_at IS NULL (idx\_invoice\_items\_accounting\_classification\_status — defined in Migration20260525101000 only, not expressible via MikroORM decorators) | \{ status: 'ready', wellCoaVersion: string, intent: WellPostingIntent, rawFacts: InvoiceItemAccountingLLMFacts } \| \{ status: 'needs\_review', wellCoaVersion: string, reason: InvoiceItemAccountingReviewReason, rawFacts?: ..., details?: string } | AI-produced Well-taxonomy accounting classification for this line. Written by the journal-entry draft builder. status='ready' means a valid WellPostingIntent was resolved and the line can be posted to the ledger. status='needs\_review' carries a reason code (missing\_accounting\_facts, llm\_requested\_review, unknown\_semantic\_role, unsupported\_invoice\_item\_posting\_kind, unsupported\_invoice\_item\_transfer\_role, invalid\_accounting\_qualifier, posting\_intent\_halt) and blocks automatic posting. | | created\_at | timestamp, 🔒 system | ✅ Yes | set once on insert via onCreate lifecycle hook | — | ISO 8601 timestamp recording when the invoice item row was first persisted. | | updated\_at | timestamp, 🔒 system | ⚪ No | set on insert and refreshed on every update via onCreate/onUpdate lifecycle hooks | — | ISO 8601 timestamp of the most recent write to this row. | | deleted\_at | timestamp | ⚪ No | nullable; soft-delete sentinel; partial indexes exclude rows where deleted\_at IS NOT NULL | — | When set, marks this line as logically deleted. All active-record queries filter deleted\_at IS NULL. Cleared only by an explicit restore operation. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invoice | to-one (invoice) | ✅ Yes | The parent invoice to which this line belongs. Non-nullable ManyToOne to the Invoice entity. A line cannot exist without its parent. Indexed via idx\_invoice\_items\_invoice\_deleted (invoice + deleted\_at) and the partial index idx\_invoice\_items\_invoice\_active (invoice\_pk WHERE deleted\_at IS NULL) for hot UPDATE paths during invoice-merge operations. | | ledger\_account | to-one (ledger\_account) | ⚪ No | Optional link to the LedgerAccount (chart-of-accounts entry) that this line item has been assigned to. Set by the accounting classification pipeline or by a user override. Indexed via idx\_invoice\_items\_ledger\_account. | | applied\_tax\_rate | to-one (tax\_rate) | ⚪ No | Optional link to the Well-catalog TaxRate entry that was applied when computing the tax on this line. Distinct from the scalar tax\_rate column which stores the raw percentage. Indexed via idx\_invoice\_items\_applied\_tax\_rate. | | media | to-one (media) | ⚪ No | Optional supporting document or image attached to this line (e.g. a product image, receipt scan, or delivery note). ManyToOne to the Media entity. Indexed via idx\_invoice\_items\_media. | ### System-computed * invoice\_item\_id is generated by PostgreSQL gen\_random\_uuid() at insert time and is immutable thereafter. * created\_at is set once by the MikroORM onCreate lifecycle hook; it is never updated. * updated\_at is set by both onCreate and onUpdate hooks — it reflects the wall-clock time of the most recent write. * deleted\_at is null on creation. Setting it to a non-null timestamp constitutes a soft delete. The partial indexes idx\_invoice\_items\_invoice\_active and idx\_invoice\_items\_accounting\_classification\_status both carry WHERE deleted\_at IS NULL to exclude deleted rows from hot read paths. * The composite unique constraint (invoice, line\_id) is table-wide (not partial): it enforces that no two lines — active or soft-deleted — on the same invoice share the same line\_id. A soft-deleted line\_id cannot be reused for a new line on the same invoice without first hard-deleting the old row. * accounting\_unit\_price and accounting\_line\_total are derived by the FX-matching pipeline (invoice.service.ts + fx-rate.service.ts) when the line currency differs from the workspace's functional currency. They are never computed by the API layer on a write request. * accounting\_classification is written exclusively by the invoice-journal-entry-draft builder (services/accounting/invoice-journal-entry-draft.builder.ts) and classifyInvoiceItemAccounting(). It is never set by a direct API mutation. Its status field is indexed via a JSONB functional partial index (migration-only; MikroORM decorators cannot express JSONB key expressions or partial predicates, so schema:fresh diverges from production on this index). * discount defaults to 0 at the database level when not supplied by the connector. ## Example ```json theme={null} { "data": { "type": "invoice_item", "id": "a3f7b2c1-84d9-4e10-b6e0-2f1d5c839740", "attributes": { "invoice_item_id": "a3f7b2c1-84d9-4e10-b6e0-2f1d5c839740", "line_id": "line-001", "sku": "SVC-CONSULTING-2026", "name": "Strategic consulting — Q2 2026", "description": "Monthly advisory retainer covering financial modelling and board prep.", "unit_price": "4500.00", "currency": "EUR", "unit": "MON", "quantity": "1.00", "min_quantity": null, "max_quantity": null, "line_total": "4500.00", "discount": "0.00", "tax_rate": "20.00", "tax_category": "standard", "tax_scheme": "EU_VAT", "tax_amount": "900.00", "accounting_unit_price": "4500.00", "accounting_line_total": "4500.00", "period_start": "2026-04-01T00:00:00.000Z", "period_end": "2026-06-30T23:59:59.000Z", "accounting_classification": { "status": "ready", "wellCoaVersion": "1.0.0", "intent": { "semanticRole": "operating_expense", "postingKind": "invoice_accrual", "documentPolarity": "purchase", "wellCoaVersion": "1.0.0" }, "rawFacts": { "well_semantic_role": "operating_expense", "posting_kind": "invoice_accrual", "document_polarity": "purchase", "tax_behavior": "exclusive", "confidence": 0.94, "classifier_model": "claude-opus-4-6" } }, "created_at": "2026-04-30T09:14:22.000Z", "updated_at": "2026-05-02T11:07:55.000Z", "deleted_at": null }, "relationships": { "invoice": { "data": { "type": "invoice", "id": "d8e1f4a2-3c7b-4b85-9e2d-6a0f7c1e4821" } }, "ledger_account": { "data": { "type": "ledger_account", "id": "f1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d" } }, "applied_tax_rate": { "data": { "type": "tax_rate", "id": "b9c8d7e6-f5a4-3b2c-1d0e-9f8e7d6c5b4a" } }, "media": { "data": null } } } } ``` Source: `apps/api/src/database/entities/InvoiceItem.ts` · domain: financial-graph · tier: Supporting # Invoice Payment Means Source: https://docs.wellapp.ai/object-reference/invoice_payment_means InvoicePaymentMeans is a junction (pivot) relation that associates one Invoice with one PaymentMeans, recording the payment instrument through which an invoice InvoicePaymentMeans is a junction (pivot) relation that associates one Invoice with one PaymentMeans, recording the payment instrument through which an invoice is expected to be or has been settled. It acts as the supporting link between the invoicing ledger and the payment-means graph, allowing a single invoice to be associated with a specific IBAN, card, or other payment method. The relation is soft-deleted rather than hard-deleted so that historical payment-means associations remain auditable even after unlinking. Agents and the records data-view surface this entity as an array relationship from both Invoice and PaymentMeans, and it is exposed as the `invoice_payment_means` records root with composites into payment-means summary and invoice totals. | Naming | Value | | ------------------------------- | --------------------------- | | Object | Invoice Payment Means | | Resource type (JSON:API `type`) | `invoice_payment_means` | | Collection / records root | `invoice_payment_means` | | REST base | `/v1/invoice-payment-means` | | Entity class | `InvoicePaymentMeans` | **Read access today:** this object is readable via the universal `POST /v1/records/query` endpoint with `root: "invoice_payment_means"`. A dedicated `GET /v1/invoice-payment-means` endpoint is **Planned**. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------------- | -------------------------------------------------- | | List | `GET /v1/invoice-payment-means` | 🟡 Planned via `POST /v1/records/query` | | Retrieve | `GET /v1/invoice-payment-means/{id}` | 🟡 Planned | | Create | `POST /v1/invoice-payment-means` | 🟡 Planned | | Update | `PATCH /v1/invoice-payment-means/{id}` | 🟡 Planned | | Delete | `DELETE /v1/invoice-payment-means/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | id | string, UUID, 🔒 system | ✅ Yes | unique; gen\_random\_uuid() default | — | Public identifier for this invoice–payment-means link. Stable across soft-delete lifecycle. Used as the canonical id in JSON:API responses and in the composite\_invoice\_payment\_means\_list source fields on both Invoice and PaymentMeans records views. | | invoice\_pk | integer, FK (internal) | ✅ Yes | NOT NULL; FK → core\_api.invoices(pk) ON UPDATE CASCADE; indexed via idx\_invoice\_payment\_means\_invoice and partial idx\_invoice\_payment\_means\_invoice\_active (WHERE deleted\_at IS NULL) | — | Internal surrogate key pointing to the parent Invoice. Implicit FK backing column generated by MikroORM from the @ManyToOne invoice relation. Exposed externally through the invoice relationship, not surfaced as a bare attribute in API responses. | | payment\_means\_pk | integer, FK (internal) | ✅ Yes | NOT NULL; FK → core\_api.payment\_means(pk) ON UPDATE CASCADE; indexed via idx\_invoice\_payment\_means\_payment\_means | — | Internal surrogate key pointing to the linked PaymentMeans row. Implicit FK backing column generated by MikroORM from the @ManyToOne payment\_means relation. Exposed externally through the payment\_means relationship. | | created\_at | datetime, 🔒 system | ✅ Yes | NOT NULL; set once on insert via MikroORM onCreate lifecycle hook | — | Timestamp recording when this invoice–payment-means association was created. Used as the sort\_proxy basis for the composite\_invoice\_payment\_means\_list aggregate ordering on both Invoice and PaymentMeans data views. | | updated\_at | datetime, 🔒 system | ⚪ No | nullable (timestamptz null in DDL); set on insert via MikroORM onCreate hook and refreshed on every update via onUpdate hook | — | Timestamp of the most recent mutation to this row. Set on INSERT by the onCreate hook, so it is not null after creation. Refreshed on every subsequent UPDATE. | | deleted\_at | datetime | ⚪ No | nullable; NULL = active; non-NULL = soft-deleted. Partial index idx\_invoice\_payment\_means\_invoice\_active filters WHERE deleted\_at IS NULL on the invoice\_pk column. | — | Soft-delete timestamp. When set, this junction row is considered unlinked but is preserved for audit purposes. All data-view and Hasura queries must predicate on deleted\_at IS NULL to exclude unlinked associations. | ### Relationships | Name | Type | Required | Description | | -------------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | invoice | to-one (invoice) | ✅ Yes | The Invoice this junction row links to. FK: invoice\_payment\_means.invoice\_pk → invoices.pk ON UPDATE CASCADE. The invoices entity carries issuer\_pk, receiver\_pk, invoice\_number, grand\_total, and currency. Hasura exposes an array\_relationship invoice\_payment\_means on the invoices table, enabling the composite\_invoice\_payment\_means\_list on the invoices records view. | | payment\_means | to-one (payment\_means) | ✅ Yes | The PaymentMeans instrument (IBAN, card, check, or other) associated with the invoice. FK: invoice\_payment\_means.payment\_means\_pk → payment\_means.pk ON UPDATE CASCADE. The PaymentMeans entity carries name, account, card, and company associations. Hasura exposes an array\_relationship invoice\_payment\_means on the payment\_means table, enabling the composite\_invoice\_payment\_means\_list on the payment\_means records view. | ### System-computed * id is generated via gen\_random\_uuid() PostgreSQL function, set as the column defaultRaw in the MikroORM entity definition. It is unique and stable for the lifetime of the row, including after soft-delete. * created\_at is set exactly once on INSERT via MikroORM onCreate: () => new Date(). It is never updated after creation. * updated\_at is set on INSERT (onCreate: () => new Date()) and refreshed on every UPDATE (onUpdate: () => new Date()). Because onCreate is present, updated\_at is never null after row creation; the column is declared nullable in the DDL (timestamptz null) to allow the optional TypeScript type, but the onCreate hook ensures it is populated on insert. * deleted\_at is null on active rows. Setting it to a non-null timestamp constitutes a soft-delete. Hard DELETE is never used; the junction row is retained for audit and historical query purposes. The partial index idx\_invoice\_payment\_means\_invoice\_active (WHERE deleted\_at IS NULL) is maintained to optimize hot Hasura array\_relationship traversals on the invoice\_pk column while excluding soft-deleted rows from the index. * Three indexes are maintained on the table: idx\_invoice\_payment\_means\_invoice (invoice\_pk, full) and idx\_invoice\_payment\_means\_payment\_means (payment\_means\_pk, full) were added in Migration20260415150000\_invoice\_payment\_means\_and\_transactions\_indexes. The partial index idx\_invoice\_payment\_means\_invoice\_active (invoice\_pk, WHERE deleted\_at IS NULL) was added later in Migration20260506140000\_perf\_indexes\_round7 during a hot-path performance round. All three are declared via @Index decorators on the entity class. * The entity is a pure junction table with no workspace\_pk column of its own. Tenant isolation is enforced transitively: Hasura RLS on the invoice or payment\_means parent tables scopes access, and application-layer queries always join through the parent entity carrying the workspace predicate. * The entity has no workspace-direct FK by design (it is scoped through its Invoice parent). It is registered in allEntities in database/entities/index.ts. * The records root invoice\_payment\_means exposes composites via composites.yml: invoice.composite\_total\_amount\_currency (display\_type: currency\_amount, source\_fields: invoice.invoice\_id, invoice.grand\_total, invoice.local\_currency) and payment\_means.composite\_payment\_means\_summary (display\_type: payment\_means\_summary) and payment\_means.company.composite\_logo\_name (display\_type: company\_logo\_name). These composites are read-only, pipeline-resolved at query time by the composite-reconstructor. ## Example ```json theme={null} { "data": { "type": "invoice_payment_means", "id": "c3e8a1f2-6d47-4b3e-9a12-0f8b7c2d5e94", "attributes": { "created_at": "2025-11-04T09:22:10.000Z", "updated_at": "2025-11-04T09:22:10.000Z", "deleted_at": null }, "relationships": { "invoice": { "data": { "type": "invoice", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "payment_means": { "data": { "type": "payment_means", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" } } } } } ``` Source: `apps/api/src/database/entities/InvoicePaymentMeans.ts` · domain: financial-graph · tier: Supporting # Invoice Transaction Source: https://docs.wellapp.ai/object-reference/invoice_transactions InvoiceTransaction is the reconciliation pivot record that links one Invoice to one Transaction, recording the exact amount (and optional accounting-currency eq InvoiceTransaction is the reconciliation pivot record that links one Invoice to one Transaction, recording the exact amount (and optional accounting-currency equivalent) that the bank movement satisfies on the invoice. It is written exclusively by the reconciliation pipeline and by connector sync mapping targets; it is not mutated directly by the user-facing API. Key associations are Workspace (tenant scope), Invoice, Transaction, and optionally Subscription (for subscription-driven payments) and ExchangeRate (for multi-currency accounting conversion). | Naming | Value | | ------------------------------- | -------------------------- | | Object | Invoice Transaction | | Resource type (JSON:API `type`) | `invoice_transaction` | | Collection / records root | `invoice_transactions` | | REST base | `/v1/invoice-transactions` | | Entity class | `InvoiceTransaction` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------- | ------------- | | List | `GET /v1/invoice-transactions` | ✅ Implemented | | Retrieve | `GET /v1/invoice-transactions/{id}` | ✅ Implemented | | Create | `POST /v1/invoice-transactions` | 🟡 Planned | | Update | `PATCH /v1/invoice-transactions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/invoice-transactions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | --------------------------------- | -------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invoice\_transaction\_id | string, UUID | ✅ Yes | unique; generated by gen\_random\_uuid() on insert | — | Public immutable identifier for this reconciliation link. Exposed on all API responses; the internal pk is never surfaced. | | amount | string (decimal 12,2) | ✅ Yes | CHECK amount > 0; max 12 digits total, 2 decimal places | — | The portion of the invoice satisfied by this transaction link, expressed in the transaction's original currency. Always positive — direction is implied by the invoice/transaction relationship. | | currency | string, enum (CurrencyCodeEnum) | ✅ Yes | Must be a valid ISO 4217 code present in currency\_code\_enum Postgres native enum | USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, … (full ISO 4217 set defined in CurrencyCodeEnum) | ISO 4217 currency code for the amount field. Matches the transaction's instructed currency at reconciliation time. | | accounting\_amount | string (decimal 12,2) | ⚪ No | nullable; max 12 digits total, 2 decimal places | — | Amount converted to the workspace accounting currency using the linked exchange\_rate. Null when no FX conversion was required (amount currency already equals accounting currency) or when the exchange rate was not available at reconciliation time. | | accounting\_currency | string, enum (CurrencyCodeEnum) | ⚪ No | nullable; same Postgres native enum currency\_code\_enum | USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, … (full ISO 4217 set) | Currency code for accounting\_amount. Populated only when a multi-currency FX conversion was performed. Null when accounting\_amount is null. | | is\_partial | boolean | ✅ Yes | default false | true, false | Legacy flag indicating the transaction only partially covers the invoice. Superseded by allocation\_type; kept for one release cycle for backward compatibility with the reconciliation persister. Will be dropped once allocation\_type (W19-P13) is fully rolled out. | | allocation\_type | string, enum (AllocationTypeEnum) | ⚪ No | nullable; default 'full'; Postgres native enum allocation\_type\_enum | full, partial, overpayment, fee\_deduction | Structured classification of how the transaction satisfies the invoice. 'full' means complete payment; 'partial' means the transaction covers only part of the outstanding amount; 'overpayment' means the payment exceeds the invoice total; 'fee\_deduction' means a service fee was deducted from the gross payment before crediting the invoice. Replaces is\_partial. Null and 'full' are treated identically by the reconciliation P5 recompute service. | | created\_at | string, datetime, 🔒 system | ✅ Yes | set on insert via onCreate lifecycle hook; never updated | — | ISO 8601 timestamp of when this reconciliation link was created by the pipeline or connector sync. | | updated\_at | string, datetime, 🔒 system | ⚪ No | set on insert and on every update via onCreate/onUpdate lifecycle hooks | — | ISO 8601 timestamp of the last modification to this record. Updated automatically by MikroORM on every flush. | | deleted\_at | string, datetime | ⚪ No | nullable; null means active record; non-null means soft-deleted | — | Soft-delete timestamp. All active-record queries must filter deleted\_at IS NULL. A partial index idx\_invoice\_transactions\_invoice\_active on (invoice\_pk) WHERE deleted\_at IS NULL exists specifically to support the invoice-merge UPDATE path that rewrites links without a workspace bound. | ### Relationships | Name | Type | Required | Description | | -------------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (workspace) | ✅ Yes | Tenant boundary. Every InvoiceTransaction belongs to exactly one Workspace. Two composite indexes (workspace + invoice, workspace + transaction) are declared at the entity level for tenant-scoped lookup performance. | | invoice | to-one (invoice) | ✅ Yes | The invoice this bank transaction is reconciled against. FK invoice\_pk. A partial index idx\_invoice\_transactions\_invoice\_active covers invoice\_pk WHERE deleted\_at IS NULL to support invoice-merge rewrites that filter solely on invoice\_pk without a workspace predicate. | | transaction | to-one (transaction) | ✅ Yes | The bank Transaction (debit or credit movement) that satisfies part or all of the linked invoice. FK transaction\_pk. | | subscription | to-one (subscription) | ⚪ No | Optional link to a Subscription when the payment originates from a recurring subscription contract. Null for one-off invoice payments. FK subscription\_pk nullable. | | exchange\_rate | to-one (exchange\_rate) | ⚪ No | The ExchangeRate snapshot used to compute accounting\_amount / accounting\_currency during multi-currency reconciliation. Null when no FX conversion was needed. FK exchange\_rate\_pk nullable. | ### System-computed * invoice\_transaction\_id is generated via gen\_random\_uuid() as the Postgres column default and mirrored by randomUUID() in the TypeScript initializer — both guarantee a unique UUID on every insert without application-level collision risk. * created\_at is set to new Date() by an onCreate MikroORM lifecycle hook on the entity property initializer; it is never updated after insert. * updated\_at is set by both onCreate and onUpdate hooks, so it reflects the timestamp of the most recent flush for any property change. * deleted\_at is null on creation and is set to a non-null Date by soft-delete callers (reconciliation pipeline / connector sync unlink). All active-record queries must include deleted\_at: null in their filter predicates. * is\_partial defaults to false at the TypeScript level; this maps to DEFAULT false in Postgres. It is a deprecated surrogate for allocation\_type and will be dropped in a future migration once W19-P13 is complete. * allocation\_type defaults to AllocationTypeEnum.FULL ('full') at both the TypeScript level and the Postgres column default (DEFAULT 'full'). The reconciliation P5 recompute service treats NULL and 'full' as semantically identical. * accounting\_amount and accounting\_currency are pipeline-computed from the linked ExchangeRate at reconciliation time; they are null when the transaction currency already matches the workspace accounting currency or when FX data was unavailable. * This entity is one of the 12 target models in the MCP connector sync mapping pipeline (ConnectorMapping target model: invoice\_transaction). Records may be created or soft-deleted by the connector sync persister as well as the Well reconciliation agent. * Three indexes are declared at the entity class level: composite (workspace\_pk, invoice\_pk), composite (workspace\_pk, transaction\_pk), and a partial expression index idx\_invoice\_transactions\_invoice\_active on (invoice\_pk) WHERE deleted\_at IS NULL — the last index is required specifically for the invoice-merge UPDATE path that rewrites foreign keys without a workspace bound. ## Example ```json theme={null} { "data": { "type": "invoice_transaction", "id": "a3f7c291-84e2-4b10-9d2e-1f5b0c3e8a47", "attributes": { "invoice_transaction_id": "a3f7c291-84e2-4b10-9d2e-1f5b0c3e8a47", "amount": "2500.00", "currency": "EUR", "accounting_amount": "2712.50", "accounting_currency": "USD", "is_partial": false, "allocation_type": "full", "created_at": "2026-04-14T09:23:11.000Z", "updated_at": "2026-04-14T09:23:11.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "b1c2d3e4-f5a6-7890-abcd-ef1234567890" } }, "invoice": { "data": { "type": "invoice", "id": "c9d8e7f6-a5b4-3c2d-1e0f-9a8b7c6d5e4f" } }, "transaction": { "data": { "type": "transaction", "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90" } }, "subscription": { "data": null }, "exchange_rate": { "data": { "type": "exchange_rate", "id": "e1f2a3b4-c5d6-7890-abcd-123456789abc" } } } } } ``` Source: `apps/api/src/database/entities/InvoiceTransaction.ts` · domain: financial-graph · tier: Supporting # InvoiceWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/invoice_workspace_connectors InvoiceWorkspaceConnector is a direction-discriminated junction row recording which WorkspaceConnector was responsible for creating or routing a given Invoice InvoiceWorkspaceConnector is a direction-discriminated junction row recording which WorkspaceConnector was responsible for creating or routing a given Invoice. Each row carries a `direction` that distinguishes whether the connector was an input source (data collected into the invoice) or an output router (data distributed from the invoice). Tenant scope is inherited transitively through the Invoice's workspace relationship and the WorkspaceConnector's workspace. The table follows the `document_workspace_connectors` pattern established as the canonical per-entity provenance junction, replacing an earlier rejected `targetWorkspaceConnector` ManyToOne shape; it ships in parallel with the legacy `source_workspace_connector_pk` column on the Invoice entity, which will be backfilled and eventually dropped in a future migration. | Naming | Value | | ------------------------------- | ---------------------------------- | | Object | InvoiceWorkspaceConnector | | Resource type (JSON:API `type`) | `invoice_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/invoice-workspace-connectors` | | Entity class | `InvoiceWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------------------- | ---------- | | List | `GET /v1/invoice-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/invoice-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/invoice-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/invoice-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/invoice-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------ | -------- | ----------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | direction | enum (native: direction\_enum) | ✅ Yes | NOT NULL; must be one of the native enum values | "input" \| "output" | Discriminates whether the linked WorkspaceConnector was an input source (data collected into the Invoice) or an output router (data distributed from the Invoice). Stored as the native PostgreSQL enum `core_api.direction_enum`. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL; DEFAULT now() | — | Timestamp set automatically on row creation via MikroORM `onCreate` hook. Never user-modifiable. | | updated\_at | 🔒 system — timestamptz | ⚪ No | NULL allowed | — | Timestamp set automatically on creation and refreshed on every update via MikroORM `onCreate`/`onUpdate` hooks. Nullable — will be null until the first update after creation in practice. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | NULL allowed | — | Soft-delete timestamp. When set, the row is logically deleted. All queries must filter `deleted_at IS NULL`. Set by the connector sync pipeline or administrative cleanup; never directly user-settable. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invoice | to-one (ManyToOne) | ✅ Yes | The Invoice this provenance row belongs to. FK `invoice_pk` → `core_api.invoices.pk` with ON UPDATE CASCADE. Provides the tenant scope for this row via `invoice.workspace`. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The WorkspaceConnector responsible for creating or routing this Invoice. FK `workspace_connector_pk` → `core_api.workspace_connectors.pk` with ON UPDATE CASCADE. Together with `direction`, uniquely characterises the connector's role (input vs. output) for this Invoice. | ### System-computed * created\_at — set by MikroORM onCreate hook to the current timestamp at insert time; never written by the user * updated\_at — set by MikroORM onCreate hook at insert and refreshed by onUpdate hook on every subsequent save; never written by the user * deleted\_at — soft-delete marker set by the connector sync pipeline or an administrative cleanup pass; not user-settable * pk — auto-incremented serial primary key assigned by PostgreSQL at insert; internal join key never exposed on the public API * No public UUID (\*\_id) field exists on this entity — the public API references rows through their relationships (invoice\_id + workspace\_connector\_id), not a standalone junction UUID * Rows are created exclusively by the connector sync pipeline when an Invoice is ingested by or routed through a WorkspaceConnector; no user-facing PATCH route exists for this entity ## Example ```json theme={null} { "data": { "type": "invoice_workspace_connector", "id": "1047382", "attributes": { "direction": "input", "created_at": "2026-05-10T14:23:11.000Z", "updated_at": "2026-05-10T14:23:11.000Z", "deleted_at": null }, "relationships": { "invoice": { "data": { "type": "invoice", "id": "f3a1e2b4-9c87-4d62-b105-002de8f1a234" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "a9c34f21-1234-4abc-9def-5678def01234" } } } } } ``` Source: `apps/api/src/database/entities/InvoiceWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # Invoice Source: https://docs.wellapp.ai/object-reference/invoices Invoice is the core billing document entity in Well, representing receivable and payable documents (commercial invoices, credit notes, purchase orders, receipts Invoice is the core billing document entity in Well, representing receivable and payable documents (commercial invoices, credit notes, purchase orders, receipts, and related document types) extracted from uploaded files or ingested via connector syncs. It links two Company parties (issuer and receiver) within a Workspace, carries multi-currency financial totals with FX normalization, and tracks both a lifecycle status (draft → issued → paid → canceled) and an orthogonal payment dimension (payment\_status) owned exclusively by the recompute service. Key associations include the source Document, line-item InvoiceItems, matched bank transactions via InvoiceTransaction pivot rows, payment instruments via InvoicePaymentMeans, and the originating WorkspaceConnector for provenance. | Naming | Value | | ------------------------------- | -------------- | | Object | Invoice | | Resource type (JSON:API `type`) | `invoice` | | Collection / records root | `invoices` | | REST base | `/v1/invoices` | | Entity class | `Invoice` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ------------- | | List | `GET /v1/invoices` | ✅ Implemented | | Retrieve | `GET /v1/invoices/{id}` | ✅ Implemented | | Create | `POST /v1/invoices` | 🟡 Planned | | Update | `PATCH /v1/invoices/{id}` | ✅ Implemented | | Delete | `DELETE /v1/invoices/{id}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invoice\_id | string, UUID | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public immutable identifier for the invoice. Exposed in all API responses; used in external references. | | status | string (InvoiceStatusEnum) | ✅ Yes | default: draft; nativeEnumName: invoice\_status\_enum | draft, issued, paid, canceled | Lifecycle stage of the invoice. Managed by the document extraction and reconciliation pipeline. Do NOT extend — payment dimension is handled by payment\_status. | | payment\_status | string (PaymentStatusEnum) | ✅ Yes | default: unknown; nativeEnumName: payment\_status\_enum; 🔒 written exclusively by the recompute service | unpaid, partial, paid, overpaid, unknown | Orthogonal payment dimension tracking how much of the invoice has been settled against matched bank transactions. Owned by the payment-recompute service; not user-writable through Hasura update permissions. | | issue\_date | Date (timestamp), nullable | ⚪ No | columnType: timestamp; @Enrichable | — | The date the invoice was formally issued, as stated on the document. Populated by the AI extraction pipeline. | | due\_date | Date (date), nullable | ⚪ No | columnType: date; @Enrichable | — | Payment deadline as stated on the invoice. Stored as a calendar date (no time component). | | grand\_total | number (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); @Enrichable | — | Total invoice amount in local\_currency, inclusive of taxes. Surfaced via composite\_total\_amount\_currency on the records page. | | items\_total | number (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); @Enrichable | — | Sum of all line-item amounts before taxes, in local\_currency. Surfaced via composite\_items\_amount\_currency. | | tax\_total | number (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); @Enrichable | — | Total tax amount in local\_currency. Surfaced via composite\_tax\_amount\_currency. | | local\_currency | string (CurrencyCodeEnum), nullable | ⚪ No | nativeEnumName: currency\_code\_enum; @Enrichable | ISO 4217 currency codes (EUR, USD, GBP, …) — full list in packages/shared/src/constants/currency-codes.ts | Currency in which the invoice was issued. grand\_total, items\_total, and tax\_total are denominated in this currency. | | invoice\_number | string, nullable | ⚪ No | no explicit length cap; @Enrichable; partial unique index on (workspace\_pk, invoice\_number) WHERE deleted\_at IS NULL AND invoice\_number IS NOT NULL | — | Human-readable invoice reference number as printed on the document (e.g. INV-2026-00412). Used as a deduplication key during import within a workspace. | | reference\_number | string, nullable | ⚪ No | length: 100; @Enrichable | — | External reference or PO number annotated on the invoice by the issuer. Distinct from purchase\_order\_number (which is the buyer-side PO). | | purchase\_order\_number | string, nullable | ⚪ No | length: 100 | — | Buyer-side purchase order number linked to this invoice. | | document\_type\_code | string (DocumentTypeCodeEnum), nullable | ⚪ No | nativeEnumName: document\_type\_code\_enum | UN/EDIFACT D.16A document type codes: 380 (COMMERCIAL\_INVOICE), 381 (CREDIT\_NOTE), 383 (DEBIT\_NOTE), 384 (CORRECTED\_INVOICE), 385 (CONSOLIDATED\_INVOICE), 386 (PREPAYMENT\_INVOICE), 325 (PROFORMA\_INVOICE), 326 (PARTIAL\_INVOICE), and many others — full list in apps/api/src/constants/document-type-code.const.ts | UN/EDIFACT D.16A document type code classifying the nature of the commercial document. Used to distinguish invoices from credit notes, purchase orders, remittance advices, utility bills, expense receipts, etc. | | billing\_context | string (BillingContextEnum), nullable | ⚪ No | nativeEnumName: billing\_context\_enum | subscription, recurring, periodic, installment, retainer, usage\_based, consumption, metered, volume\_based, overage, project, milestone, hourly, fixed\_price, time\_materials, one\_time, event\_based, commission, bonus, reimbursement, maintenance, support, consulting, training, professional\_services, contract, license, rental, lease, franchise, adjustment, refund, credit, penalty, discount, deposit, advance\_payment, escrow, insurance, tax, promotional, trial, freemium, setup, activation, other, mixed, unknown | Commercial context or billing model under which the invoice was raised. | | description | string, nullable | ⚪ No | @Enrichable; display\_type: long\_text\_area in overrides.yml | — | Free-text description of the goods or services billed, as extracted from the document. | | terms | string, nullable | ⚪ No | length: 300; @Enrichable; display\_type: long\_text\_area in overrides.yml | — | Payment terms as stated on the invoice (e.g. Net 30, early payment discount clauses). | | accounting\_currency | string (CurrencyCodeEnum), nullable | ⚪ No | nativeEnumName: currency\_code\_enum | ISO 4217 currency codes — same enum as local\_currency | Workspace functional / reporting currency into which totals have been converted by the FX matching pipeline. Present when local\_currency differs from the workspace accounting currency. | | accounting\_grand\_total | string (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); stored as string to preserve precision | — | grand\_total converted to accounting\_currency via the linked exchange\_rate. Written by the invoice FX matching service. | | accounting\_items\_total | string (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); stored as string | — | items\_total converted to accounting\_currency. | | accounting\_tax\_total | string (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); stored as string | — | tax\_total converted to accounting\_currency. | | paid\_amount | string (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); stored as string; 🔒 written by recompute service | — | Cumulative amount matched and allocated against this invoice from bank transactions, in local\_currency. Written exclusively by the payment-recompute service. | | balance\_due | string (decimal 12,2), nullable | ⚪ No | columnType: decimal(12,2); stored as string; 🔒 written by recompute service | — | Remaining unpaid amount (grand\_total minus paid\_amount), in local\_currency. Derived and written by the payment-recompute service. | | last\_payment\_allocation\_date | Date (timestamptz), nullable | ⚪ No | columnType: timestamptz; 🔒 written by recompute service | — | Timestamp of the most recent payment allocation event processed by the recompute service. | | override\_version | number (integer) | ✅ Yes | default: 0; 🔒 incremented by recompute service on every write; selectable but not user-writable via Hasura update permissions | — | Optimistic concurrency counter incremented by the payment-recompute service on each write. Used for observability and conflict detection; not a user-settable field. | | shadow\_from\_receipt | boolean | ✅ Yes | default: false | true, false | True when this invoice row was synthesized from a payment\_receipt document via the mapReceiptToShadowFlat (R1) pipeline rather than extracted from a real bill. Lets the UI distinguish promoted receipts from genuine outstanding invoices. | | created\_at | Date, 🔒 system | ✅ Yes | onCreate: () => new Date() | — | Timestamp when the invoice row was created. Set once on insert via MikroORM lifecycle hook. | | updated\_at | Date, 🔒 system | ⚪ No | onCreate + onUpdate: () => new Date() | — | Timestamp of the last modification. Automatically maintained by MikroORM lifecycle hooks. Declared optional in the entity; absent on rows that have never been updated after initial insert. | | deleted\_at | Date, nullable | ⚪ No | nullable; all active-row queries must filter deleted\_at IS NULL | — | Soft-delete timestamp. Non-null means the invoice is logically deleted and must not appear in active listings. Partial indexes on workspace + created\_at and workspace + source\_connector include WHERE deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ------------------------------ | --------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | issuer | to-one (company) | ⚪ No (nullable) | The Company that issued (sent) this invoice — typically the vendor or service provider. Index: idx\_invoices\_issuer. Used as a hot-path FK rewrite target during company-merge operations. | | receiver | to-one (company) | ⚪ No (nullable) | The Company that received (was billed by) this invoice — typically the workspace's own company for payables, or the customer for receivables. Index: idx\_invoices\_receiver. Also a hot-path FK rewrite target during company-merge. | | document | to-one (document) | ⚪ No (nullable) | The source Document from which this invoice was extracted (contains the original file, filename, and organisation/partner slugs). Indexed jointly with deleted\_at via idx\_invoices\_document\_deleted. | | workspace | to-one (workspace) | ⚪ No (nullable) | Tenant boundary. Every active invoice belongs to exactly one Workspace. Partial indexes for active-row listing and source-connector listing both scope to workspace\_pk. | | source\_workspace\_connector | to-one (workspace\_connector) | ⚪ No (nullable) | The WorkspaceConnector instance that ingested this invoice, when it was created via a connector sync. Null for manually uploaded or email-extracted invoices. Partial index idx\_invoices\_workspace\_source\_connector\_active covers (workspace\_pk, source\_workspace\_connector\_pk) WHERE deleted\_at IS NULL. | | exchange\_rate | to-one (exchange\_rate) | ⚪ No (nullable) | FK-normalised ExchangeRate row used for multi-currency conversion. Carries rate, source\_currency, target\_currency, rate\_date, and source. Used by the composite\_fx\_rate cell renderer and by accounting\_\* total computation. | | subscription | to-one (subscription) | ⚪ No (nullable) | Optional link to a Subscription record when the invoice was generated from a recurring subscription context. | | invoice\_items | to-many (invoice\_item) | — | Line items (InvoiceItem) belonging to this invoice. Contains per-line quantity, unit price, description, and tax rates. Owned by the invoice — cascade-deleted when the invoice is hard-deleted. | | invoice\_transactions | to-many (invoice\_transaction) | — | Pivot rows (InvoiceTransaction) linking this invoice to matched bank transactions. Written by the reconciliation / payment-recompute service. The existence and amounts of these rows drive paid\_amount and balance\_due. | | payment\_means | to-many (invoice\_payment\_means) | — | Payment instrument links (InvoicePaymentMeans) associating bank account / payment method information stated on the invoice (e.g. IBAN to pay to). | | invoice\_workspace\_connectors | to-many (invoice\_workspace\_connector) | — | Connector provenance pivot rows (InvoiceWorkspaceConnector) recording which WorkspaceConnector instances have touched or are aware of this invoice. Used for deduplication and cross-connector identity resolution. | ### System-computed * invoice\_id is generated automatically at insert by Postgres via defaultRaw: gen\_random\_uuid(). It is immutable after creation. * created\_at is set once on INSERT via MikroORM onCreate: () => new Date() lifecycle hook. * updated\_at is set on both INSERT and UPDATE via onCreate + onUpdate: () => new Date() lifecycle hooks. It is declared optional in the entity (updated\_at?: Date) and may be absent on rows that have never been modified after creation. * deleted\_at is null on creation and is set to a Date value by soft-delete logic. All active-row queries must predicate on deleted\_at IS NULL. No hard delete should be issued directly. * payment\_status (and paid\_amount, balance\_due, last\_payment\_allocation\_date, override\_version) are owned exclusively by the payment-recompute service. Application code and API clients must treat these as read-only. override\_version is an optimistic concurrency counter incremented on every recompute write. * shadow\_from\_receipt defaults to false. It is set to true only by the mapReceiptToShadowFlat (R1) pipeline when synthesizing an invoice row from a payment\_receipt document. * accounting\_grand\_total, accounting\_items\_total, accounting\_tax\_total, and accounting\_currency are written by the invoice FX matching service (fx-rate.service.ts + invoice.service.ts) when the invoice local\_currency differs from the workspace accounting currency. * source\_workspace\_connector (sourceWorkspaceConnector) is set at creation time when the invoice is ingested through a connector sync; it remains null for manually uploaded or email-extracted invoices. * Fields decorated with @Enrichable (issue\_date, grand\_total, local\_currency, reference\_number, terms, items\_total, tax\_total, description, invoice\_number, due\_date) are eligible for AI enrichment via the enrichment pipeline workers. * Partial index idx\_invoices\_workspace\_invoice\_number\_active enforces uniqueness of (workspace\_pk, invoice\_number) among non-deleted rows where invoice\_number IS NOT NULL, enabling efficient deduplication on import. * Partial index idx\_invoices\_workspace\_source\_connector\_active on (workspace\_pk, source\_workspace\_connector\_pk) WHERE deleted\_at IS NULL supports efficient tenant + connector listing queries. * Partial index idx\_invoices\_workspace\_created\_active on (workspace\_pk, created\_at DESC) WHERE deleted\_at IS NULL mirrors the records-page default sort order to avoid full-table scans. ## Example ```json theme={null} { "data": { "type": "invoice", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "attributes": { "invoice_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "issued", "payment_status": "partial", "issue_date": "2026-04-15T00:00:00.000Z", "due_date": "2026-05-15", "grand_total": "12450.00", "items_total": "10375.00", "tax_total": "2075.00", "local_currency": "EUR", "invoice_number": "INV-2026-00412", "reference_number": "PO-8821-ACME", "purchase_order_number": "PO-8821", "document_type_code": "380", "billing_context": "subscription", "description": "Annual SaaS platform licence — Enterprise tier", "terms": "Net 30. Late payments subject to 1.5 % monthly interest.", "accounting_currency": "USD", "accounting_grand_total": "13489.35", "accounting_items_total": "11241.00", "accounting_tax_total": "2248.35", "paid_amount": "5000.00", "balance_due": "7450.00", "last_payment_allocation_date": "2026-05-01T14:32:00.000Z", "override_version": 3, "shadow_from_receipt": false, "created_at": "2026-04-16T09:17:42.000Z", "updated_at": "2026-05-01T14:32:01.000Z", "deleted_at": null }, "relationships": { "issuer": { "data": { "type": "company", "id": "a1b2c3d4-0001-4000-8000-111111111111" } }, "receiver": { "data": { "type": "company", "id": "a1b2c3d4-0002-4000-8000-222222222222" } }, "document": { "data": { "type": "document", "id": "d0c00000-0000-4000-8000-999999999999" } }, "workspace": { "data": { "type": "workspace", "id": "w0000000-0000-4000-8000-000000000001" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "c0000000-0000-4000-8000-000000000001" } }, "exchange_rate": { "data": { "type": "exchange_rate", "id": "e0000000-0000-4000-8000-000000000001" } }, "subscription": { "data": null }, "invoice_items": { "data": [ { "type": "invoice_item", "id": "b1000000-0000-4000-8000-000000000001" } ] }, "invoice_transactions": { "data": [ { "type": "invoice_transaction", "id": "t2000000-0000-4000-8000-000000000001" } ] }, "payment_means": { "data": [ { "type": "invoice_payment_means", "id": "pm000000-0000-4000-8000-000000000001" } ] }, "invoice_workspace_connectors": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/Invoice.ts` · domain: financial-graph · tier: Main # Journal Entry Source: https://docs.wellapp.ai/object-reference/journal_entries A JournalEntry represents one accounting entry inside a Journal — the atomic unit of double-entry bookkeeping on Well A JournalEntry represents one accounting entry inside a Journal — the atomic unit of double-entry bookkeeping on Well. It belongs to a workspace-scoped Journal (e.g. a SALES, BANK, or PURCHASES journal), carries a human-readable entry number unique within the fiscal year, and is composed of one or more JournalEntryLine child rows that hold the actual debit/credit amounts against ledger accounts. Journal entries flow from two automated posting pipelines: the invoice-journal-entry builder (which creates DRAFT entries when an invoice is extracted) and the payment-journal-entry builder (which creates DRAFT settlement entries from matched bank transactions). Entries can also be written by MCP connectors via the sync mapping pipeline, in which case sourceWorkspaceConnector carries provenance. The lifecycle progresses from DRAFT → VALIDATED → LOCKED; once VALIDATED the entry\_number and fiscal context are immutable at the service layer. | Naming | Value | | ------------------------------- | --------------------- | | Object | Journal Entry | | Resource type (JSON:API `type`) | `journal_entry` | | Collection / records root | `journal_entries` | | REST base | `/v1/journal-entries` | | Entity class | `JournalEntry` | ## API operations | Operation | Method & path | Status | | --------- | --------------------------------- | ------------- | | List | `GET /v1/journal-entries` | ✅ Implemented | | Retrieve | `GET /v1/journal-entries/{id}` | ✅ Implemented | | Create | `POST /v1/journal-entries` | 🟡 Planned | | Update | `PATCH /v1/journal-entries/{id}` | 🟡 Planned | | Delete | `DELETE /v1/journal-entries/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------- | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | journal\_entry\_id | string, UUID | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public identifier for the journal entry. Generated by PostgreSQL on insert. Used in all external API references. | | entry\_number | string | ✅ Yes | length ≤ 50; composite unique (workspace\_pk, entry\_number, fiscal\_year). Immutable once status = VALIDATED (enforced at service layer, not DB). | — | Human-readable accounting reference number assigned by the posting pipeline or connector (e.g. VTE-2026-0042). Must be unique within the workspace × fiscal\_year pair. | | entry\_date | string (ISO date, YYYY-MM-DD) | ✅ Yes | columnType: date | — | Accounting date of the entry. Determines which fiscal period and year it falls into. Indexed together with workspace for date-range queries. | | label | string | ⚪ No | length ≤ 500; nullable | — | Free-text description of the entry (e.g. invoice reference, payment description). Shown in the accounting ledger UI. | | status | string (enum: JournalEntryStatusEnum) | ✅ Yes | default: DRAFT; nativeEnumName: journal\_entry\_status\_enum | DRAFT, VALIDATED, LOCKED | Lifecycle state of the entry. DRAFT = editable; VALIDATED = frozen (entry\_number immutable, validated\_at/validated\_by set); LOCKED = archived, cannot be modified or reversed. | | validated\_at | string (ISO 8601 timestamp) | ⚪ No | nullable | — | Timestamp when the entry was moved to VALIDATED status. Null while status is DRAFT or LOCKED. | | fiscal\_year | integer | ✅ Yes | columnType: integer; part of the composite unique constraint (workspace\_pk, entry\_number, fiscal\_year) | — | Four-digit fiscal year the entry belongs to (e.g. 2026). Combined with entry\_number to guarantee uniqueness per workspace. | | fiscal\_period | integer | ⚪ No | nullable; CHECK: fiscal\_period IS NULL OR (fiscal\_period >= 1 AND fiscal\_period \<= 13) | 1–13 or null | Accounting period within the fiscal year. Supports up to 13 periods (some jurisdictions use a 13th adjustment period). Null when period-level granularity is not required. | | source\_entity\_type | string | ⚪ No | length ≤ 100; nullable. Soft FK — no database foreign key constraint. | — | Polymorphic soft reference: the entity type that triggered this entry (e.g. 'invoice', 'transaction'). Used together with source\_entity\_id to trace the posting origin without a hard FK. | | source\_entity\_id | string | ⚪ No | length ≤ 50; nullable. Soft FK — no database foreign key constraint. | — | Polymorphic soft reference: the public identifier of the entity that triggered this entry (e.g. the external invoice id from the connector). Paired with source\_entity\_type. | | posting\_idempotency\_key | string | ⚪ No | length ≤ 160; nullable. Partial unique index on (workspace\_pk, posting\_idempotency\_key) WHERE deleted\_at IS NULL AND posting\_idempotency\_key IS NOT NULL — lives in migration, not the entity decorator. | — | Deduplication key used by the automated posting pipelines to prevent re-posting the same source event. A second attempt with the same key will conflict at DB level rather than create a duplicate entry. | | posting\_metadata | object (JSONB) | ⚪ No | nullable; type: jsonb | — | Arbitrary structured metadata attached by the posting pipeline at creation time (e.g. builder version, trigger context). No schema enforcement at the DB level. | | created\_at | string (ISO 8601 timestamp), 🔒 system | ✅ Yes | onCreate: () => new Date() | — | Timestamp set automatically by MikroORM lifecycle hook when the record is first persisted. Not client-settable. | | updated\_at | string (ISO 8601 timestamp), 🔒 system | ✅ Yes | onCreate + onUpdate: () => new Date(). DB column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now()). | — | Timestamp updated automatically on every persist. Tracks last modification time. Not client-settable; NOT NULL at DB level. | | deleted\_at | string (ISO 8601 timestamp) | ⚪ No | nullable; soft-delete sentinel | — | When non-null the entry is soft-deleted. All repository queries must filter deleted\_at IS NULL. The posting\_idempotency\_key partial unique index also gates on this column. | ### Relationships | Name | Type | Required | Description | | ------------------------ | ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | Workspace that owns this journal entry. Mandatory tenant-scope relation. All queries must filter through this relation. FK: workspace\_pk → core\_api.workspaces(pk). | | journal | to-one (journal) | ✅ Yes | The Journal (e.g. SALES, BANK, PURCHASES) this entry belongs to. Determines the accounting category and default ledger accounts. FK: journal\_pk → core\_api.journals(pk). | | validated\_by | to-one (people) | ⚪ No | The People record of the user who validated the entry (set when status transitions to VALIDATED). Nullable. FK: validated\_by\_pk → core\_api.peoples(pk). | | invoice\_transaction | to-one (invoice\_transaction) | ⚪ No | The InvoiceTransaction (invoice ↔ bank transaction match) that produced this entry, when created by the payment-settlement posting pipeline. Nullable. FK: invoice\_transaction\_pk → core\_api.invoice\_transactions(pk). | | lines | to-many (journal\_entry\_line) | — | The JournalEntryLine children that constitute the double-entry: each line carries a debit or credit amount against a ledger account. Accessed via Hasura as the 'lines' array\_relationship. A balanced entry requires at least two lines with equal total debits and credits. | | sourceWorkspaceConnector | to-one (workspace\_connector) | ⚪ No | The WorkspaceConnector instance (MCP sync connector) that created this entry, when it was ingested via the MCP sync mapping pipeline. Null for entries created by the internal posting pipelines or manually. FK: source\_workspace\_connector\_pk → core\_api.workspace\_connectors(pk) ON DELETE SET NULL. Sparse index on this column where non-null. | ### System-computed * journal\_entry\_id is generated by PostgreSQL via gen\_random\_uuid() at INSERT; the TypeScript default randomUUID() is a client-side fallback that is overridden by the DB default. * created\_at is set by MikroORM onCreate lifecycle hook (new Date()); not client-settable. * updated\_at is set by MikroORM onCreate + onUpdate lifecycle hooks (new Date()); updated automatically on every flush. DB column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now()) — cannot be null in production. * deleted\_at follows the platform-wide soft-delete contract: null means active; a non-null timestamp means soft-deleted. All repository queries must predicate deleted\_at IS NULL. The posting\_idempotency\_key partial unique index also gates on deleted\_at IS NULL. * posting\_idempotency\_key dedup: the partial unique index `journal_entries_workspace_posting_idempotency_unique` on (workspace\_pk, posting\_idempotency\_key) WHERE deleted\_at IS NULL AND posting\_idempotency\_key IS NOT NULL prevents double-posting by the automated pipelines. This index lives in Migration20260525102000\_journal\_entry\_posting\_metadata, not on the entity decorator — `schema:fresh` may diverge from production on this column. * status defaults to DRAFT at the DB level (DEFAULT 'DRAFT'). The VALIDATED and LOCKED transitions are enforced at service level (services/accounting/). Once VALIDATED, entry\_number is treated as immutable. * sourceWorkspaceConnector provenance: when non-null, the entry was written by the MCP sync mapping pipeline (target\_model = 'journal\_entry'). A sparse index `idx_journal_entries_source_wc` exists on source\_workspace\_connector\_pk WHERE non-null — added by Migration20260406100000\_expand\_mcp\_sync\_models. * Composite unique constraint: (workspace\_pk, entry\_number, fiscal\_year) — enforced at DB level to prevent duplicate entry numbers within the same fiscal year for a workspace. * Partial index on (workspace\_pk, entry\_date) — `idx_journal_entries_workspace_date` — optimises date-range queries within a workspace. * source\_entity\_type / source\_entity\_id form a polymorphic soft FK pattern (no DB foreign key). They are set by the posting pipelines to trace origin (e.g. 'invoice' + the connector's external invoice id). No ON DELETE cascade. * JournalEntryLine children are the load-bearing accounting rows; the JournalEntry header is the grouping container. Accessing lines via Hasura uses the alias 'lines' (not 'journal\_entry\_lines') per the composites.yml documentation. * journal\_entry\_posting\_attempts is a separate append-only ledger table (Migration20260526020000) that records each posting attempt (source\_kind × source\_id × status). It is not a property of JournalEntry itself but references it by source. ## Example ```json theme={null} { "data": { "type": "journal_entry", "id": "a3f7c21e-84b2-4d19-9f01-2c8e6b5d3a7f", "attributes": { "journal_entry_id": "a3f7c21e-84b2-4d19-9f01-2c8e6b5d3a7f", "entry_number": "VTE-2026-0042", "entry_date": "2026-05-15", "label": "Facture Acme SAS – vente de licences Q2", "status": "VALIDATED", "validated_at": "2026-05-16T09:14:22.000Z", "fiscal_year": 2026, "fiscal_period": 5, "source_entity_type": "invoice", "source_entity_id": "inv_8f3a12cd", "posting_idempotency_key": "invoice:3b6f1a7c-98d2-4e11-a0c3-7f2e8b4d5c1a:v1", "posting_metadata": { "builder_version": "2.1.0", "triggered_by": "invoice_extraction" }, "created_at": "2026-05-15T14:32:11.000Z", "updated_at": "2026-05-16T09:14:22.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "d4e9c2a1-5b7f-4c3e-8d2a-1f6b9e3c7a5d" } }, "journal": { "data": { "type": "journal", "id": "c1b8e7f2-3a4d-4f9e-8c1b-5d7a2e9f4b3c" } }, "validated_by": { "data": { "type": "people", "id": "e5f2a3b1-7c4d-4e8f-9a2b-3c6d1e5f7a8b" } }, "invoice_transaction": { "data": { "type": "invoice_transaction", "id": "b7d3e1f4-2a5c-4b8e-9f3d-1a6c2e7b4d5f" } }, "sourceWorkspaceConnector": { "data": null }, "lines": { "data": [ { "type": "journal_entry_line", "id": "f2a1b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c" }, { "type": "journal_entry_line", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ] } } } } ``` Source: `apps/api/src/database/entities/JournalEntry.ts` · domain: financial-graph · tier: Main # JournalEntryLine Source: https://docs.wellapp.ai/object-reference/journal_entry_lines A Journal Entry Line (`journal_entry_lines`) is a single debit or credit leg within a double-entry journal entry A Journal Entry Line (`journal_entry_lines`) is a single debit or credit leg within a double-entry journal entry. Each line is associated with exactly one `JournalEntry` and one primary `LedgerAccount`, carries a decimal debit or credit amount (never both simultaneously, enforced by a CHECK constraint), and may reference an auxiliary ledger account, a tax rate, and a counterparty company for subledger analysis. Lines are created exclusively by the accounting posting engine (invoice-journal-entry builder and payment-settlement pipeline) and are never written directly by end users. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | JournalEntryLine | | Resource type (JSON:API `type`) | `journal_entry_line` | | Collection / records root | — (not a records root) | | REST base | `/v1/journal-entry-lines` | | Entity class | `JournalEntryLine` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------- | ---------- | | List | `GET /v1/journal-entry-lines` | 🟡 Planned | | Retrieve | `GET /v1/journal-entry-lines/{id}` | 🟡 Planned | | Create | `POST /v1/journal-entry-lines` | 🟡 Planned | | Update | `PATCH /v1/journal-entry-lines/{id}` | 🟡 Planned | | Delete | `DELETE /v1/journal-entry-lines/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | ------------------------ | -------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | journal\_entry\_line\_id | string (UUID) | ✅ Yes | unique | — | Public stable identifier for the line, generated via gen\_random\_uuid() on creation. Used in all API and Hasura references. | | label | string | ⚪ No | max length 500 | — | Free-text description of the line (e.g. 'Revenue recognition – Invoice #INV-001'). Written by the posting engine from the source document. | | debit | decimal(15,2) | ✅ Yes | CHECK (debit >= 0), default '0' | — | Debit amount in the workspace's functional currency. Defaults to 0. Cannot be positive simultaneously with credit (CHECK constraint on parent table: NOT (debit > 0 AND credit > 0)). | | credit | decimal(15,2) | ✅ Yes | CHECK (credit >= 0), default '0' | — | Credit amount in the workspace's functional currency. Defaults to 0. Cannot be positive simultaneously with debit. | | source\_currency | enum (CurrencyCodeEnum) | ⚪ No | — | USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, BGN, HRK, ISK, ALL, BAM, BYN, MDL, MKD, RSD, FOK, GGP ... (full ISO 4217 set defined in CurrencyCodeEnum). Stored as the enum value string (e.g. 'EUR'). | ISO 4217 currency code of the original transaction before conversion to the accounting currency. Populated when the source document is in a foreign currency. | | source\_amount | decimal(15,2) | ⚪ No | — | — | The monetary amount expressed in source\_currency before FX conversion. Null when source and accounting currencies are identical. | | accounting\_currency | enum (CurrencyCodeEnum) | ⚪ No | — | Same set as source\_currency — full CurrencyCodeEnum value strings. | ISO 4217 currency code used for the accounting books (the workspace's reporting currency). Populated during FX conversion. | | accounting\_amount | decimal(15,2) | ⚪ No | — | — | The monetary amount converted to accounting\_currency using the exchange rate at posting time. | | lettering\_code | string | ⚪ No | max length 20 | — | Reconciliation / lettering code used to match open AR/AP lines against their offsetting settlement lines. Assigned by the lettering engine. | | lettering\_date | date | ⚪ No | — | — | Date on which the lettering code was assigned, marking when the outstanding balance was reconciled. | | tax\_rate | decimal(5,2) | ⚪ No | CHECK (tax\_rate >= 0 AND tax\_rate \<= 100) | — | Inline tax rate percentage applied to this line (0–100). Denormalised copy of the tax rate value at the time of posting, independent of any tax\_rate\_ref record. | | posting\_metadata | jsonb | ⚪ No | — | — | Arbitrary key-value payload attached by the posting engine for traceability (e.g. source invoice pk, posting run identifier). No enforced schema; consumers should treat as opaque. | | cost\_center | string | ⚪ No | max length 100 | — | Analytic axis — cost centre code for management reporting and budget allocation. | | project | string | ⚪ No | max length 100 | — | Analytic axis — project identifier for project-based accounting or profitability tracking. | | created\_at | 🔒 system — Date | ✅ Yes | — | — | Timestamp set automatically when the record is first persisted. Never updated thereafter. | | updated\_at | 🔒 system — Date | ⚪ No | — | — | Timestamp set on creation and refreshed on every update by the MikroORM onUpdate hook. | | deleted\_at | 🔒 system — Date \| null | ⚪ No | — | — | Soft-delete timestamp. Null means the record is active. Set by the posting engine or a cascade from the parent JournalEntry; never set directly by end users. All queries must filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (ManyToOne) | ✅ Yes | The tenant boundary. Every journal entry line belongs to exactly one workspace. Indexed together with journal\_entry (composite index) and with ledger\_account (composite index). | | journal\_entry | to-one (ManyToOne) | ✅ Yes | The parent double-entry journal entry this line belongs to. A journal entry must balance: the sum of all its lines' debits must equal the sum of all credits. | | ledger\_account | to-one (ManyToOne) | ✅ Yes | The primary chart-of-accounts ledger account credited or debited by this line (e.g. '401000 – Accounts Payable', '706000 – Revenue'). | | auxiliary\_account | to-one (ManyToOne) | ⚪ No | Optional subledger / auxiliary ledger account for detailed third-party tracking (e.g. a per-customer receivable subledger). References LedgerAccount. | | company | to-one (ManyToOne) | ⚪ No | The counterparty company linked to this line for subledger analysis (e.g. the customer on an AR line or the supplier on an AP line). Populated by the posting engine from the source invoice's issuer or receiver. | | tax\_rate\_ref | to-one (ManyToOne) | ⚪ No | Reference to the TaxRate record whose percentage was applied at posting time. Complements the denormalised tax\_rate decimal field. | ### System-computed * journal\_entry\_line\_id — generated via gen\_random\_uuid() database default on INSERT; also seeded client-side by randomUUID() in the entity constructor. * created\_at — set to new Date() by MikroORM onCreate hook; never updated. * updated\_at — set to new Date() on onCreate and refreshed by MikroORM onUpdate hook on every flush. * deleted\_at — null on creation; set to a timestamp by soft-delete logic in the accounting posting engine or by cascade from the parent JournalEntry; never hard-deleted in normal operation. * debit / credit defaults — both default to '0' at the database level (DEFAULT 0) and in the entity constructor. * CHECK constraint chk\_jel\_debit\_credit\_exclusive — enforced at the database level: NOT (debit > 0 AND credit > 0). A line must be either a debit line or a credit line, never both. * posting\_metadata — populated by the accounting posting engine (journal-entry-persister.service.ts / invoice-journal-entry-draft.builder.ts) with traceability data; not derived from any ORM hook. * accounting\_currency / accounting\_amount — populated during FX conversion in the multi-currency posting path; null for same-currency workspaces. * Composite indexes: (workspace\_pk, journal\_entry\_pk) and (ledger\_account\_pk, workspace\_pk) are created by the Migration20260311100000 migration for query performance. ## Example ```json theme={null} { "data": { "type": "journal_entry_line", "id": "e3f1a2b4-5c6d-7e8f-9012-abcdef345678", "attributes": { "journal_entry_line_id": "e3f1a2b4-5c6d-7e8f-9012-abcdef345678", "label": "Invoice revenue recognition – Well App Inc", "debit": "0.00", "credit": "1250.00", "source_currency": "USD", "source_amount": "1250.00", "accounting_currency": "EUR", "accounting_amount": "1152.37", "lettering_code": "A001", "lettering_date": "2026-05-15", "tax_rate": "20.00", "posting_metadata": { "invoice_pk": 4821, "posting_run_id": "run_20260515_001" }, "cost_center": "SALES-EU", "project": "Q2-2026-EXPANSION", "created_at": "2026-05-15T08:22:10.000Z", "updated_at": "2026-05-15T08:22:10.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "wsp_9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }, "journal_entry": { "data": { "type": "journal_entry", "id": "je_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" } }, "ledger_account": { "data": { "type": "ledger_account", "id": "la_2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e" } }, "auxiliary_account": { "data": null }, "company": { "data": { "type": "company", "id": "cmp_3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f" } }, "tax_rate_ref": { "data": { "type": "tax_rate", "id": "tr_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90" } } } } } ``` Source: `apps/api/src/database/entities/JournalEntryLine.ts` · domain: financial-graph · tier: Supporting # JournalEntryPostingAttempt Source: https://docs.wellapp.ai/object-reference/journal_entry_posting_attempts JournalEntryPostingAttempt is an append-only audit ledger that records every attempt to post a double-entry journal entry from a source document (invoice, invoi JournalEntryPostingAttempt is an append-only audit ledger that records every attempt to post a double-entry journal entry from a source document (invoice, invoice\_transaction, or bank transaction). One row is written per attempt — persisted or halted — so that human-driven retries never overwrite prior halt reasons and the full posting history is preserved. It is workspace-scoped and carries actor metadata (system vs user) together with a nullable reference to the People who triggered a manual retry. The table is the authority for why a journal entry was or was not created for a given source document. | Naming | Value | | ------------------------------- | ------------------------------------ | | Object | JournalEntryPostingAttempt | | Resource type (JSON:API `type`) | `journal_entry_posting_attempt` | | Collection / records root | — (not a records root) | | REST base | `/v1/journal-entry-posting-attempts` | | Entity class | `JournalEntryPostingAttempt` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------------------ | ---------- | | List | `GET /v1/journal-entry-posting-attempts` | 🟡 Planned | | Retrieve | `GET /v1/journal-entry-posting-attempts/{id}` | 🟡 Planned | | Create | `POST /v1/journal-entry-posting-attempts` | 🟡 Planned | | Update | `PATCH /v1/journal-entry-posting-attempts/{id}` | 🟡 Planned | | Delete | `DELETE /v1/journal-entry-posting-attempts/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------------------ | ----------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | journal\_entry\_posting\_attempt\_id | string (UUID) 🔒 system | ✅ Yes | UNIQUE | — | Public UUID identifier for this posting attempt. Generated by gen\_random\_uuid() on insert. This is the value exposed as the JSON:API resource id. | | source\_kind | enum (JournalEntryPostingAttemptSourceKindEnum) | ✅ Yes | Native Postgres enum: journal\_entry\_posting\_attempt\_source\_kind\_enum. NOT NULL. | invoice \| invoice\_transaction \| transaction | Identifies the type of source document that triggered the posting attempt. 'invoice' = invoice-driven JE; 'invoice\_transaction' = legacy per-bridge-row trigger (D9, preserved for backward compat); 'transaction' = D9.1 per-bank-transaction payment-settlement JE. | | source\_id | string | ✅ Yes | VARCHAR(64), NOT NULL | — | Public UUID string of the source row (invoice\_id, invoice\_transaction\_id, or transaction\_id). Stored as VARCHAR(64) rather than a typed FK so new source kinds do not require schema changes. | | source\_pk | integer | ⚪ No | INTEGER, NULLABLE | — | Internal integer PK of the source row for fast joins when the source kind is known. Nullable because legacy attempt rows pre-dating the D9.1 migration may not have this populated. | | status | enum (JournalEntryPostingAttemptStatusEnum) | ✅ Yes | Native Postgres enum: journal\_entry\_posting\_attempt\_status\_enum. NOT NULL. | persisted \| halt | Outcome of this posting attempt. 'persisted' = a journal entry was written; 'halt' = posting was blocked (see reason/details). Halts never roll back the source document. | | reason | string | ⚪ No | VARCHAR(100), NULLABLE | — | Short machine-readable reason code for a halt (e.g. 'missing\_mapping', 'polarity\_conflict'). Free-string rather than enum because halt reasons span multiple subsystems and are expected to grow. NULL on status=persisted. | | details | string | ⚪ No | VARCHAR(500), NULLABLE | — | Human-readable explanation of the halt reason, providing context for the operator reviewing the audit trail. NULL on status=persisted. | | idempotency\_key | string | ⚪ No | VARCHAR(160), NULLABLE | — | Idempotency key passed to the journal entry persister. Set on status=persisted; null on status=halt. Allows the persister to detect duplicate attempts and avoid double-writing JEs. | | line\_count | integer | ⚪ No | INTEGER, NULLABLE | — | Number of journal entry lines written when status=persisted. Null on status=halt. Useful for sanity-checking multi-line JEs (e.g. AR/AP lines per counterparty). | | created | boolean | ⚪ No | BOOLEAN, NULLABLE | — | True when a new journal entry was created; false when an existing one was updated/reused via the idempotency key. Null on status=halt. | | attempted\_at | Date (timestamptz) | ✅ Yes | TIMESTAMPTZ, NOT NULL | — | Wall-clock timestamp at which the posting was attempted. Used as the DESC sort key in the latest-per-source and timeline indexes. Stored as TIMESTAMPTZ. | | attempted\_by\_kind | enum (JournalEntryPostingAttemptActorKindEnum) | ✅ Yes | Native Postgres enum: journal\_entry\_posting\_attempt\_actor\_kind\_enum. NOT NULL. DEFAULT 'system'. | system \| user | Whether the attempt was triggered by an automated pipeline ('system') or by an explicit human action such as a manual retry after fixing a classification ('user'). Defaults to 'system'. | | created\_at | Date 🔒 system | ✅ Yes | TIMESTAMP, NOT NULL, DEFAULT now() | — | Row creation timestamp. Set by the onCreate hook; never user-editable. | | updated\_at | Date 🔒 system | ⚪ No | TIMESTAMP, NULLABLE | — | Last modification timestamp. Set by onUpdate hook. Nullable per entity declaration. | | deleted\_at | Date 🔒 system | ⚪ No | TIMESTAMP, NULLABLE | — | Soft-delete timestamp. NULL means the row is active. Used in the workspace-status composite index to keep halted-attempt dashboard queries efficient. | ### Relationships | Name | Type | Required | Description | | -------------- | ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace this posting attempt belongs to. All queries must filter by workspace\_pk for tenant isolation. References core\_api.workspaces(pk). | | journal\_entry | to-one (ManyToOne, nullable) | ⚪ No | The journal entry that was written on a successful (persisted) attempt. NULL on status=halt. References core\_api.journal\_entries(pk). | | attempted\_by | to-one (ManyToOne, nullable) | ⚪ No | The People record of the user who triggered a manual retry when attempted\_by\_kind='user'. NULL when attempted\_by\_kind='system'. References core\_api.peoples(pk). | ### System-computed * journal\_entry\_posting\_attempt\_id — generated by gen\_random\_uuid() on insert; never user-supplied * created\_at — set by MikroORM onCreate hook (new Date()); immutable after creation * updated\_at — set by MikroORM onUpdate hook on every flush * deleted\_at — soft-delete; set by repository-layer soft-delete helpers; not set by the application layer on normal posting flows * attempted\_by\_kind defaults to JournalEntryPostingAttemptActorKindEnum.SYSTEM ('system') when not explicitly provided * source\_kind native enum extended via Migration20260526030000 to include 'transaction' for D9.1 payment-settlement attempts; 'invoice\_transaction' retained for backward compat with pre-D9.1 rows ## Example ```json theme={null} { "data": { "type": "journal_entry_posting_attempt", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "source_kind": "invoice", "source_id": "inv_7e2f3a1b-4c5d-6e7f-8a9b-0c1d2e3f4a5b", "source_pk": 10482, "status": "halt", "reason": "missing_mapping", "details": "No ledger account mapping found for category 'operating_expense' in chart of accounts version 3.", "idempotency_key": "inv_7e2f3a1b-4c5d-6e7f-8a9b-0c1d2e3f4a5b:attempt:3", "line_count": null, "created": null, "attempted_at": "2026-05-26T14:23:11.000Z", "attempted_by_kind": "system", "created_at": "2026-05-26T14:23:11.000Z", "updated_at": "2026-05-26T14:23:11.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws_00000000-0000-0000-0000-000000000001" } }, "journal_entry": { "data": null }, "attempted_by": { "data": null } } } } ``` Source: `apps/api/src/database/entities/JournalEntryPostingAttempt.ts` · domain: financial-graph · tier: Infrastructure # Journal Source: https://docs.wellapp.ai/object-reference/journals A Journal (record root: `journals`) represents a named accounting book used to categorize and post financial entries within a workspace — for example, a Sales j A Journal (record root: `journals`) represents a named accounting book used to categorize and post financial entries within a workspace — for example, a Sales journal, a Bank journal, or a Miscellaneous journal. It is the top-level organizational unit for double-entry bookkeeping, grouping JournalEntry records beneath it, and optionally carrying default debit/credit LedgerAccount references to pre-fill lines on new entries. Journals are workspace-scoped, soft-deleted, and provisioned either manually or by the financial-pipeline sync (via `sourceWorkspaceConnector`); the `source_entity_id` field is the stable provider-side identifier used for idempotent upserts during sync. | Naming | Value | | ------------------------------- | -------------- | | Object | Journal | | Resource type (JSON:API `type`) | `journal` | | Collection / records root | `journals` | | REST base | `/v1/journals` | | Entity class | `Journal` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ------------- | | List | `GET /v1/journals` | ✅ Implemented | | Retrieve | `GET /v1/journals/{id}` | ✅ Implemented | | Create | `POST /v1/journals` | 🟡 Planned | | Update | `PATCH /v1/journals/{id}` | 🟡 Planned | | Delete | `DELETE /v1/journals/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | journal\_id | string, UUID, 🔒 system | ✅ Yes | UNIQUE; generated via gen\_random\_uuid() on insert | — | Public stable identifier for the journal, exposed in all API responses. Generated server-side; never writable by the client. | | code | string | ✅ Yes | max length 10; UNIQUE(workspace\_pk, code) — plain unique constraint on all rows including soft-deleted ones, enforced at the database level; mirrored by @Unique(\{ properties: \['workspace', 'code'] }) on the entity | — | Short alphanumeric book code assigned by the workspace or imported from the accounting provider (e.g. 'VT' for sales, 'BQ' for bank, 'OD' for miscellaneous). Must be unique per workspace across all rows. | | name | string | ✅ Yes | max length 255 | — | Human-readable display label for the journal (e.g. 'Ventes — Clients France', 'Banque Qonto EUR'). | | journal\_type | enum (journal\_type\_enum) | ✅ Yes | NOT NULL; mapped to PostgreSQL native enum journal\_type\_enum | BANK, SALES, PURCHASES, MISC, OPENING, CLOSING | Accounting classification of the journal. BANK: cash/bank movement book. SALES: receivables and revenue. PURCHASES: payables and expenses. MISC: catch-all for adjustments. OPENING/CLOSING: period-boundary entries. | | source\_entity\_id | string | ⚪ No | nullable; max length 255 (varchar(255) in DDL); PARTIAL UNIQUE INDEX journals\_workspace\_source\_entity\_id\_unique on (workspace\_pk, source\_entity\_id) WHERE source\_entity\_id IS NOT NULL AND deleted\_at IS NULL — ensures idempotent upserts per provider without conflicting on NULL | — | Stable identifier assigned by the originating financial provider (e.g. Pennylane journal ID). Used by the sync pipeline for deduplication/upsert. NULL for manually created journals. | | is\_active | boolean | ✅ Yes | DEFAULT true; indexed (idx\_journals\_is\_active) | true, false | Whether the journal is currently in use. Inactive journals are hidden from accounting entry creation flows but remain queryable for historical reporting. | | created\_at | Date, 🔒 system | ✅ Yes | NOT NULL DEFAULT now(); set once via MikroORM onCreate lifecycle hook; not writable by client | — | ISO 8601 timestamp of record creation. | | updated\_at | Date, 🔒 system | ✅ Yes | NOT NULL DEFAULT now(); set via MikroORM onCreate + onUpdate lifecycle hooks; not writable by client | — | ISO 8601 timestamp of the last modification. Always set at insert time via DEFAULT now(); refreshed on every ORM flush that touches this entity. | | deleted\_at | Date | ⚪ No | nullable; soft-delete sentinel — all active queries must predicate deleted\_at IS NULL | — | ISO 8601 timestamp set when the journal is soft-deleted. NULL means the record is active. Hard deletes are not supported. | ### Relationships | Name | Type | Required | Description | | ---------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (workspace) | ✅ Yes | The tenant workspace that owns this journal. All queries must scope to the authenticated workspace; the combination (workspace\_pk, code) carries a plain UNIQUE constraint enforcing per-workspace code uniqueness. References core\_api.workspaces. | | default\_debit\_account | to-one (ledger\_account) | ⚪ No | Optional default LedgerAccount pre-filled as the debit leg when creating new JournalEntry lines under this journal. Indexed (idx\_journals\_default\_debit\_account). References core\_api.ledger\_accounts. | | default\_credit\_account | to-one (ledger\_account) | ⚪ No | Optional default LedgerAccount pre-filled as the credit leg when creating new JournalEntry lines under this journal. References core\_api.ledger\_accounts. | | source\_workspace\_connector | to-one (workspace\_connector) | ⚪ No | The WorkspaceConnector sync instance that created or last reconciled this journal record. NULL for manually created journals. Used for provenance tracking and to identify connector-managed records that should not be edited manually. References core\_api.workspace\_connectors. | ### System-computed * `journal_id` is generated server-side via PostgreSQL `gen_random_uuid()` (defaultRaw) and mirrored in the ORM entity initializer `randomUUID()`. Never supplied by the client. * `created_at` is set once by the MikroORM `onCreate` lifecycle hook at insert time. The database column is NOT NULL DEFAULT now(). Not writable by the client. * `updated_at` is set by both `onCreate` and `onUpdate` MikroORM lifecycle hooks. The database column is NOT NULL DEFAULT now(), so it is always populated from the moment of insert. Reflects the timestamp of the last ORM flush touching this entity. * `deleted_at` is null on active records. Set to the current timestamp on soft-delete. All repository queries must include `deleted_at: null` as a filter predicate. * The combination `(workspace_pk, code)` carries a plain database-level UNIQUE constraint (defined in the DDL as `UNIQUE(workspace_pk, code)` and mirrored by `@Unique({ properties: ['workspace', 'code'] })` on the entity). This constraint applies to all rows, including soft-deleted ones. * `source_entity_id` carries a PARTIAL UNIQUE INDEX `journals_workspace_source_entity_id_unique` on `(workspace_pk, source_entity_id) WHERE source_entity_id IS NOT NULL AND deleted_at IS NULL`. This enables idempotent upserts by the financial sync pipeline without conflicting on NULL values for manually created journals. * `sourceWorkspaceConnector` (→ `source_workspace_connector_pk` FK column) is set by the ingestion pipeline when a journal is created via connector sync (e.g. Pennylane). NULL indicates manual creation. Presence signals that the record is pipeline-owned and should not be edited without understanding sync implications. A partial index `idx_journals_source_wc` on `(source_workspace_connector_pk) WHERE source_workspace_connector_pk IS NOT NULL` supports provenance queries. * Journals are reachable from a Company via the Hasura computed field `company_journals(company_row, hasura_session)`, which traverses the 3-hop path: `companies → journal_entry_lines (company_pk) → journal_entries (journal_entry_pk) → journals (journal_pk)`. No direct `company_pk` FK exists on the `journals` table itself. * `is_active` defaults to `true` at creation. The pipeline or a workspace admin may flip it to `false` to retire a journal while preserving historical entries. ## Example ```json theme={null} { "data": { "type": "journal", "id": "a3f1c2d4-7e8b-4a9c-b5f6-0d1e2f3a4b5c", "attributes": { "journal_id": "a3f1c2d4-7e8b-4a9c-b5f6-0d1e2f3a4b5c", "code": "VT", "name": "Ventes — Clients France", "journal_type": "SALES", "source_entity_id": "pnl_jnl_00042", "is_active": true, "created_at": "2026-01-15T09:23:11.000Z", "updated_at": "2026-03-04T14:07:55.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "d5e6f7a8-b9c0-4d1e-a2f3-b4c5d6e7f8a9" } }, "default_debit_account": { "data": { "type": "ledger_account", "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e" } }, "default_credit_account": { "data": { "type": "ledger_account", "id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "e3f4a5b6-c7d8-4e9f-a0b1-c2d3e4f5a6b7" } } } } } ``` Source: `apps/api/src/database/entities/Journal.ts` · domain: financial-graph · tier: Main # Ledger Account Source: https://docs.wellapp.ai/object-reference/ledger_accounts A LedgerAccount represents a single line in a workspace's chart of accounts (CoA), classified by accounting type (ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE) an A LedgerAccount represents a single line in a workspace's chart of accounts (CoA), classified by accounting type (ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE) and grouped into account classes 1–9. It is the foundational node of the accounting graph: invoice items, journal entry lines, and workspace posting mappings all FK into it. Ledger accounts may be arranged in a parent–child hierarchy (one self-referential ManyToOne), and they can be marked as auxiliary (linked to a customer, supplier, or employee counterparty dimension). The connector sync pipeline writes ledger accounts from external accounting tools (Xero, Pennylane, etc.) and stamps each row with source\_workspace\_connector\_pk for provenance tracking. | Naming | Value | | ------------------------------- | --------------------- | | Object | Ledger Account | | Resource type (JSON:API `type`) | `ledger_account` | | Collection / records root | `ledger_accounts` | | REST base | `/v1/ledger-accounts` | | Entity class | `LedgerAccount` | ## API operations | Operation | Method & path | Status | | --------- | --------------------------------- | ------------- | | List | `GET /v1/ledger-accounts` | ✅ Implemented | | Retrieve | `GET /v1/ledger-accounts/{id}` | ✅ Implemented | | Create | `POST /v1/ledger-accounts` | 🟡 Planned | | Update | `PATCH /v1/ledger-accounts/{id}` | 🟡 Planned | | Delete | `DELETE /v1/ledger-accounts/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ledger\_account\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() on INSERT | — | Public stable identifier for the ledger account. Use this in all API references; the internal pk is never exposed. | | account\_number | string | ✅ Yes | max length 20; PARTIAL UNIQUE on (workspace\_pk, account\_number) WHERE deleted\_at IS NULL (index idx\_ledger\_accounts\_workspace\_account\_number\_active\_unique); the legacy non-partial UNIQUE constraint was replaced by Migration20260529100000 | — | The chart-of-accounts code for this account within the workspace, e.g. '411000' (PCG), '1200' (IFRS). Uniqueness is enforced only among active (non-deleted) rows to allow re-creation after a soft-delete. | | name | string | ✅ Yes | max length 255 | — | Human-readable display name for the ledger account. | | account\_type | enum (ledger\_account\_type\_enum) | ✅ Yes | non-nullable; native PostgreSQL enum | ASSET \| LIABILITY \| EQUITY \| REVENUE \| EXPENSE | Broad accounting classification per the double-entry model. Drives debit/credit sign convention in journal entries and determines which financial statement section the account appears in. | | account\_class | integer | ✅ Yes | CHECK (account\_class >= 1 AND account\_class \<= 9); composite index (workspace\_pk, account\_class) | 1 – 9 | Numeric account class (plan comptable class digit). Used to group accounts in chart-of-accounts views and as a fast filter for trial-balance queries. Indexed together with workspace\_pk. | | is\_auxiliary | boolean | ✅ Yes | default false | true \| false | Indicates whether this account carries an auxiliary (counterparty) dimension. When true, auxiliary\_type must be set and journal entry lines on this account must reference a counterparty company. | | auxiliary\_type | enum (auxiliary\_type\_enum) | ⚪ No | nullable; native PostgreSQL enum; required in practice when is\_auxiliary = true | CUSTOMER \| SUPPLIER \| EMPLOYEE | The counterparty dimension type for auxiliary accounts. CUSTOMER = accounts receivable style, SUPPLIER = accounts payable style, EMPLOYEE = payroll/expense style. | | is\_active | boolean | ✅ Yes | default true | true \| false | Whether the account is currently open for posting. Inactive accounts remain in the chart for historical reporting but should be excluded from new posting selection lists. | | description | string | ⚪ No | nullable; TEXT (unbounded) | — | Optional free-text description or instructions for use. Populated by the connector sync when the source accounting tool provides an account note. | | created\_at | Date, 🔒 system | ✅ Yes | set once on INSERT via MikroORM onCreate hook; TIMESTAMPTZ NOT NULL DEFAULT now() | — | Timestamp of row creation. Never modified after insert. | | updated\_at | Date, 🔒 system | ✅ Yes | TIMESTAMPTZ NOT NULL DEFAULT now() per migration; set on INSERT and on every UPDATE via MikroORM onUpdate hook | — | Timestamp of last modification. The database column is NOT NULL (baseline migration defines it as TIMESTAMPTZ NOT NULL DEFAULT now()). Updated automatically on every PATCH/flush. | | deleted\_at | Date \| null, 🔒 system | ⚪ No | nullable; soft-delete sentinel; all queries must filter WHERE deleted\_at IS NULL | — | Soft-delete timestamp. When non-null the row is logically deleted. Hard deletes are never performed. The partial unique index on (workspace\_pk, account\_number) ignores rows where deleted\_at IS NOT NULL, so a previously deleted account number can be re-created. | ### Relationships | Name | Type | Required | Description | | ---------------------------- | ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | The workspace that owns this ledger account. Enforces tenant isolation. All queries must scope to workspace\_pk. Indexed via the composite (workspace\_pk, account\_class) and (workspace\_pk, parent\_account\_pk) indexes. | | parent\_account | to-one (ledger\_account) | ⚪ No | Self-referential parent in the chart-of-accounts hierarchy. NULL for top-level accounts (e.g. class root '400000'). Indexed on (workspace\_pk, parent\_account\_pk) — index idx\_ledger\_accounts\_workspace\_parent created by Migration20260306100000 — to support efficient subtree traversal. | | child\_accounts | to-many (ledger\_account) | — | Inverse of parent\_account. Collection of all direct child accounts in the hierarchy. Lazy-loaded via MikroORM Collection. | | source\_workspace\_connector | to-one (workspace\_connector) | ⚪ No | The WorkspaceConnector instance (e.g. a Xero or Pennylane sync) that last wrote or created this ledger account row. NULL for manually created accounts. Added by Migration20260406100000\_expand\_mcp\_sync\_models (that migration covers ledger\_accounts; Migration20260331100000 only added the column to companies and peoples). ON DELETE SET NULL so connector removal does not cascade-delete ledger accounts. Partial index idx\_ledger\_accounts\_source\_wc (WHERE source\_workspace\_connector\_pk IS NOT NULL) added by the same migration. | ### System-computed * ledger\_account\_id is generated by PostgreSQL gen\_random\_uuid() on INSERT and also seeded in-process via randomUUID() from Node crypto as the MikroORM default; it is unique and immutable. * created\_at is set once via MikroORM onCreate lifecycle hook (new Date()); never written again. * updated\_at is set on CREATE and on every UPDATE via MikroORM onUpdate lifecycle hook. The database column is NOT NULL (TIMESTAMPTZ NOT NULL DEFAULT now() per Migration20260306100000); the MikroORM entity declares it optional (updated\_at?: Date) but that only affects TypeScript nullability — the DB enforces NOT NULL. * deleted\_at is null on creation; set to now() by the application (never by the database) to perform a soft-delete. The partial unique index idx\_ledger\_accounts\_workspace\_account\_number\_active\_unique (WHERE deleted\_at IS NULL) ensures uniqueness only among active rows — soft-deleted rows are excluded, allowing re-insertion of the same (workspace\_pk, account\_number) pair after a soft-delete. * The non-partial UNIQUE(workspace\_pk, account\_number) constraint from Migration20260306100000 was dropped by Migration20260529100000\_ledger\_accounts\_dedup\_unique\_idx. That migration also deduped existing active duplicates (keeping the earliest pk) before building the partial index. * source\_workspace\_connector\_pk is set by the connector sync pipeline (MCP / Xero / Pennylane / etc.) to record provenance. It is NULL for manually created accounts and is SET NULL (not cascade-deleted) when the workspace\_connector row is removed. Column and partial index added by Migration20260406100000\_expand\_mcp\_sync\_models. * account\_class is a human-supplied classification digit (1–9) enforced by a CHECK constraint in the database; it is not derived. * is\_auxiliary defaults to false; auxiliary\_type defaults to NULL. The pipeline or user must explicitly set both when creating an auxiliary account. * is\_active defaults to true; setting it to false deactivates the account without soft-deleting it, preserving historical journal entry lines. * The (workspace\_pk, account\_class) composite index (idx\_ledger\_accounts\_workspace\_class) and the (workspace\_pk, parent\_account\_pk) composite index (idx\_ledger\_accounts\_workspace\_parent) are created by Migration20260306100000 and are transparent to the application layer. ## Example ```json theme={null} { "data": { "type": "ledger_account", "id": "e3a4c9f0-12b7-4d88-9031-cc52a7d1e205", "attributes": { "ledger_account_id": "e3a4c9f0-12b7-4d88-9031-cc52a7d1e205", "account_number": "411000", "name": "Clients — France", "account_type": "ASSET", "account_class": 4, "is_auxiliary": true, "auxiliary_type": "CUSTOMER", "is_active": true, "description": "Comptes clients — marché domestique France", "created_at": "2026-03-10T09:14:22.000Z", "updated_at": "2026-05-15T16:03:08.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-0000-4000-8000-111111111111" } }, "parent_account": { "data": { "type": "ledger_account", "id": "b9d00001-aaaa-4bbb-cccc-000000000001" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "f5e60000-dead-beef-cafe-123456789abc" } } } } } ``` Source: `apps/api/src/database/entities/LedgerAccount.ts` · domain: financial-graph · tier: Main # McpOAuthClient Source: https://docs.wellapp.ai/object-reference/mcp_oauth_clients McpOAuthClient stores OAuth 2.0 clients dynamically registered via RFC 7591 Dynamic Client Registration (DCR) McpOAuthClient stores OAuth 2.0 clients dynamically registered via RFC 7591 Dynamic Client Registration (DCR). Each record represents one MCP client application (e.g. `mcp-remote`, Claude Code) that has registered to use Well's OAuth proxy for a specific MCP provider slug. It is written exclusively by the `POST /v1/mcps/:slug/oauth/register` DCR endpoint and subsequently looked up at authorize time. The entity has no workspace association, no soft-delete, and no relationships declared — it is a global, append-only registry of DCR client identities scoped to a provider slug. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | McpOAuthClient | | Resource type (JSON:API `type`) | `mcp_oauth_client` | | Collection / records root | — (not a records root) | | REST base | `/v1/mcp-oauth-clients` | | Entity class | `McpOAuthClient` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------- | ---------- | | List | `GET /v1/mcp-oauth-clients` | 🟡 Planned | | Retrieve | `GET /v1/mcp-oauth-clients/{id}` | 🟡 Planned | | Create | `POST /v1/mcp-oauth-clients` | 🟡 Planned | | Update | `PATCH /v1/mcp-oauth-clients/{id}` | 🟡 Planned | | Delete | `DELETE /v1/mcp-oauth-clients/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------------- | ----------------------- | -------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | client\_id | 🔒 system — UUID | ✅ Yes | UNIQUE (mcp\_oauth\_clients\_client\_id\_unique); default gen\_random\_uuid() | Any valid UUID | Public identifier for the registered OAuth client. Generated by the database on insert. This is the value returned to the MCP client at DCR time and presented on subsequent authorize requests. | | client\_name | string | ✅ Yes | varchar(255); NOT NULL | Any non-empty string ≤ 255 chars | Human-readable name of the registering MCP client as supplied in the RFC 7591 registration request body (e.g. 'mcp-remote', 'Claude Code'). | | redirect\_uris | jsonb (string\[]) | ✅ Yes | jsonb NOT NULL | JSON array of URI strings | Array of allowed redirect URIs registered by the client. Used during the authorization code flow to validate the redirect\_uri parameter. | | grant\_types | jsonb (string\[]) | ✅ Yes | jsonb NOT NULL | JSON array — e.g. \["authorization\_code"] | OAuth 2.0 grant types supported by this client, as declared at registration time (RFC 7591 §2). | | response\_types | jsonb (string\[]) | ✅ Yes | jsonb NOT NULL | JSON array — e.g. \["code"] | OAuth 2.0 response types supported by this client (RFC 7591 §2). Typically \["code"] for authorization code flow. | | token\_endpoint\_auth\_method | string | ✅ Yes | varchar(255); NOT NULL | Standard OAuth 2.0 auth method strings — e.g. 'none', 'client\_secret\_basic', 'client\_secret\_post' | Authentication method the client uses at the token endpoint (RFC 7591 §2). Public clients (e.g. mcp-remote) use 'none'. | | slug | string | ✅ Yes | varchar(255); NOT NULL | Any registered MCP provider slug present in the registry (e.g. 'pennylane', 'wise', 'spiko') | The MCP provider slug this client was registered against. Scopes the DCR entry to a specific external service OAuth proxy. Set from the :slug route parameter at registration time. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL; default now(); set via @Property(\{ onCreate: () => new Date() }) | ISO 8601 timestamp | Timestamp when the DCR registration occurred. Set by the entity's onCreate hook and by the database default. Also exposed as client\_id\_issued\_at (Unix epoch) in the RFC 7591 response. | ### System-computed * client\_id — generated by gen\_random\_uuid() at INSERT time; unique across the table * created\_at — set by the database default (now()) and the MikroORM onCreate hook; never updated after creation * No updated\_at column — the entity is immutable after creation; there is no soft-delete (no deleted\_at column) * No workspace association — global registry shared across all tenants, scoped only by slug * slug — derived from the :slug URL route parameter at DCR time; not user-supplied in the request body ## Example ```json theme={null} { "data": { "type": "mcp_oauth_client", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "client_name": "mcp-remote", "redirect_uris": ["http://localhost:3334/oauth/callback"], "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "none", "slug": "pennylane", "created_at": "2026-03-23T14:55:00.000Z" } } } ``` Source: `apps/api/src/database/entities/McpOAuthClient.ts` · domain: platform · tier: Platform # Media Source: https://docs.wellapp.ai/object-reference/media Media is an atomic, reusable binary-asset record that stores logos, avatars, and banners for companies and people in the Well platform Media is an atomic, reusable binary-asset record that stores logos, avatars, and banners for companies and people in the Well platform. Each row carries a `media_type` enum, an optional human label, and the GCS asset location expressed either as a legacy `url` (deprecated) or a modern GCS `path` (preferred). Media rows are workspace-scoped via a nullable ManyToOne to Workspace. CompanyMedia and PersonMedia are independent pivot entities that each hold a ManyToOne reference to Media — the Media entity itself declares no back-reference collections. | Naming | Value | | ------------------------------- | ------------ | | Object | Media | | Resource type (JSON:API `type`) | `media` | | Collection / records root | `media` | | REST base | `/v1/medias` | | Entity class | `Media` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------ | ------------- | | List | `GET /v1/medias` | ✅ Implemented | | Retrieve | `GET /v1/medias/{id}` | ✅ Implemented | | Create | `POST /v1/medias` | 🟡 Planned | | Update | `PATCH /v1/medias/{id}` | 🟡 Planned | | Delete | `DELETE /v1/medias/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | media\_id | string, UUID, 🔒 system | ✅ Yes | unique; default gen\_random\_uuid() | — | Public identifier for the media asset. Generated by PostgreSQL on insert via gen\_random\_uuid(). This is the value exposed in the JSON:API `id` field. | | media\_type | string (enum) | ✅ Yes | NOT NULL; native PG enum media\_type\_enum in core\_api schema | avatar, logo, banner | Category of the binary asset. `avatar` is used for person profile pictures; `logo` for company brand marks; `banner` for wide header images. Controls how the asset is rendered by cell components in the records table. | | label | string | ⚪ No | varchar(255) by MikroORM default, nullable | — | Human-readable label for the asset, e.g. "Acme Corp primary logo" or "Profile photo – Q1 2026". Optional free-text annotation; not used programmatically. | | url | string | ⚪ No | text, nullable; DEPRECATED — NULL for all assets stored after the GCS migration | — | Legacy absolute URL to the asset. Kept for backward compatibility with records created before the GCS storage migration. For all new assets this column is NULL; the asset is stored at `path` instead. The formatter's `resolveMediaUrl()` falls through to this field only when `path` is absent. | | path | string | ⚪ No | text, nullable | — | GCS object path for the asset, e.g. `workspaces/<workspace_id>/media/<media_id>.png`. The API formatter (`resolveMediaUrl()` in media.formatter.ts) converts this path to a CDN URL via `buildMediaCdnUrl()`. Takes priority over the legacy `url` field when both are present. NULL on pre-migration records. | | created\_at | Date, 🔒 system | ✅ Yes | timestamptz NOT NULL; initialized to `new Date()` as TypeScript default and also set by MikroORM onCreate lifecycle hook | — | Timestamp when the media record was created. Immutable after insert. | | updated\_at | Date, 🔒 system | ⚪ No | timestamptz, nullable; set by MikroORM onCreate hook on insert AND onUpdate hook on every subsequent write; no TypeScript default initializer (unlike created\_at) | — | Timestamp of the last attribute update on this record. Set automatically by MikroORM on both create and update operations. | | deleted\_at | Date | ⚪ No | timestamptz, nullable; NULL = active record | — | Soft-delete timestamp. When non-NULL the record is considered logically deleted. All queries must filter `deleted_at IS NULL`. The composite index `idx_media_workspace_deleted` on (workspace\_pk, deleted\_at) is the hot path for Hasura permission filters. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No | The workspace this media asset belongs to. Nullable — set to NULL on historic records predating the workspace-scoping migration. The FK carries ON DELETE SET NULL so workspace deletion orphans the asset rather than cascading a hard delete. Composite index `idx_media_workspace_deleted` on (workspace, deleted\_at) accelerates the Hasura permission filter `(workspace_pk = $1 AND deleted_at IS NULL)`. | ### System-computed * media\_id: generated by PostgreSQL `gen_random_uuid()` at INSERT time via `@Property({ defaultRaw: 'gen_random_uuid()' })`. Never set by application code. * created\_at: initialized to `new Date()` as a TypeScript class field default AND set by MikroORM `onCreate: () => new Date()` lifecycle hook. Always populated. * updated\_at: set by MikroORM `onCreate` and `onUpdate` lifecycle hooks. No TypeScript default initializer — the field is `updated_at?: Date` (optional). NULL on records that have never been updated after initial creation. * deleted\_at: soft-delete sentinel. NULL = active. Application must never issue hard DELETEs against the media table; set `deleted_at = NOW()` instead. * URL resolution: the public-facing `url` attribute in the JSON:API response is NOT the raw `media.url` column. The formatter calls `resolveMediaUrl(media)` which returns `buildMediaCdnUrl(media.path)` when `path` is present, and falls back to `media.url` otherwise. Consumers must never read `media.url` or `media.path` directly — always use the resolved `url` from the formatted response. * Workspace scoping: added retroactively by Migration20260105120000. Existing unscoped media was reassigned to workspaces via a data migration that de-duplicated assets shared across multiple workspaces by inserting new media rows per workspace. * Deprecated url column: the `url` column was originally NOT NULL (see Migration20250919154301 DDL). It was later made nullable by the GCS migration; all new records store the asset in GCS and leave `url` NULL. * Composite records-table field `composite_media_list`: built from `company_media.media.media_id`, `company_media.media.label`, and `company_media.media.media_type` (for companies) and the analogous `person_media.*` paths (for people). Defined in composites.yml; rendered by the records table as a media-list composite cell. * Primary media computed field: the PostgreSQL function `core_api.company_primary_media(company_row, hasura_session)` selects the most-recently-created non-deleted CompanyMedia row for a company and returns a JSONB projection including `media_id`, `media_type`, `label`, `url`, and `path`. The function was updated by Migration20260527120000 to include `path` alongside the legacy `url` so the data-views layer can derive a CDN URL for logos stored in GCS. * Pivot relationships: CompanyMedia and PersonMedia each declare a @ManyToOne to Media on their own entity classes. The Media entity itself declares NO @OneToMany back-references — navigation from Media to its pivot rows is done via Hasura array relationships (metadata-level), not via MikroORM collections. ## Example ```json theme={null} {"type":"media","id":"b2f7c3d1-09e4-4a7b-8f56-3c2d1e0a9b7f","attributes":{"media_type":"logo","label":"Acme Corp primary logo","url":null,"path":"workspaces/4e9d7c2b-11aa-4b38-9e1f-7a3f5d6c8b20/media/b2f7c3d1-09e4-4a7b-8f56-3c2d1e0a9b7f.png","created_at":"2026-03-14T10:22:00.000Z","updated_at":"2026-04-01T08:45:00.000Z","deleted_at":null},"relationships":{"workspace":{"data":{"type":"workspace","id":"4e9d7c2b-11aa-4b38-9e1f-7a3f5d6c8b20"}}}} ``` Source: `apps/api/src/database/entities/Media.ts` · domain: financial-graph · tier: Supporting # Membership Source: https://docs.wellapp.ai/object-reference/memberships A Membership represents a user's participation in a workspace, binding a Person (identified by their Firebase auth identity) to a Workspace with a specific role A Membership represents a user's participation in a workspace, binding a Person (identified by their Firebase auth identity) to a Workspace with a specific role and lifecycle state. It is the central RBAC artifact in the multi-tenant architecture: every permission check, invitation flow, and workspace-access decision resolves through this record. Each Membership carries a role (`owner`, `admin`, `member`, `guest`), a status (`pending` or `active`), and a provenance flag that marks which workspace is the user's default. A soft-deleted Membership means access has been revoked; the row is retained for audit purposes. | Naming | Value | | ------------------------------- | ----------------- | | Object | Membership | | Resource type (JSON:API `type`) | `membership` | | Collection / records root | `memberships` | | REST base | `/v1/memberships` | | Entity class | `Membership` | ## API operations | Operation | Method & path | Status | | --------- | ----------------------------- | ------------- | | List | `GET /v1/memberships` | ✅ Implemented | | Retrieve | `GET /v1/memberships/{id}` | ✅ Implemented | | Create | `POST /v1/memberships` | ✅ Implemented | | Update | `PATCH /v1/memberships/{id}` | ✅ Implemented | | Delete | `DELETE /v1/memberships/{id}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | membership\_id | string, UUID | ✅ Yes | unique; generated via gen\_random\_uuid() on insert | — | Public stable identifier for this membership. Used in all API responses and external references. The internal `pk` (auto-increment integer) is never exposed. | | firebase\_id | string | ⚪ No | nullable; part of the composite index idx\_memberships\_firebase\_workspace\_deleted (firebase\_id, workspace\_pk, deleted\_at) | — | Firebase Authentication UID of the user. Populated when the membership belongs to a Firebase-authed user. Used as the cross-Person identity key: a single Firebase user may have multiple Person rows (one per invited email) but should have exactly one active `is_default` membership across all of them. | | membership\_role | string (MembershipRole enum) | ⚪ No | nullable; constrained in application code to MEMBERSHIP\_ROLES values; part of the composite index idx\_memberships\_workspace\_role\_deleted (workspace\_pk, membership\_role, deleted\_at) | owner \| admin \| member \| guest | The RBAC role granted to this user within the workspace. Determines which actions the user may perform. Role assignment happens at invitation acceptance, not at invite creation. Parent-workspace admin retention is automatic on workspace hierarchy changes. | | status | string (MembershipStatus enum) | ✅ Yes | default: pending; non-nullable | pending \| active | Lifecycle state of the membership. Newly created memberships (on invitation) start as `pending`; they transition to `active` upon the invitee accepting. Pending memberships confer no workspace access. | | is\_default | boolean | ✅ Yes | default: false; non-nullable. Business invariant: exactly one active, non-deleted membership per firebase\_id must have is\_default = true. Enforced by application logic and backfilled by Migration20260424100000. | true \| false | Flags which workspace is the user's default landing workspace. Used to redirect the user after login when no explicit workspace is specified. Exactly one active membership per firebase\_id should be marked true; zero or multiple defaults indicate a corrupted state (see Migration20260424100000\_backfill\_membership\_default\_flag). | | invite\_token | string, UUID | ⚪ No | nullable; unique (partial — uniqueness enforced across non-null values only) | — | One-time token distributed in the invitation email. Present while the membership is in `pending` status; nulled out (or the row is reused) upon acceptance. Serves as the authentication credential for the invitation acceptance flow. Re-sending an invite to the same email reuses the existing pending membership row rather than creating a new one. | | created\_at | Date, 🔒 system | ✅ Yes | set by @Property onCreate hook; non-nullable | — | Timestamp when the membership record was created (i.e., when the invitation was issued). | | updated\_at | Date, 🔒 system | ⚪ No | set by @Property onCreate and onUpdate hooks; nullable in the DB column definition | — | Timestamp of the last mutation to this row (e.g., status transition from pending to active, role change). Auto-managed by the ORM lifecycle hook. | | deleted\_at | Date | ⚪ No | nullable; part of composite indexes for firebase\_id and workspace+role hot-path lookups. Soft-delete sentinel: when non-null the membership is revoked. | — | Soft-delete timestamp. A non-null value means the membership has been revoked. The row is retained for audit history. All active-membership queries filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ----------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | person | to-one (people) | ✅ Yes | The Person record that holds this membership. A Person is the email-scoped identity (one Person per invited email address). The membership binds this Person to the workspace. Not nullable. | | workspace | to-one (workspace) | ✅ Yes | The Workspace this membership grants access to. The workspace is the multi-tenant boundary; every permission check, data-view query, and notification scopes to this workspace. Not nullable. | | invited\_by | to-one (people) | ⚪ No | The Person who issued this invitation. Nullable — absent for memberships created by the system (e.g., workspace creator's own bootstrapped membership) or for legacy rows predating this column. Provides audit provenance for the invite grant. | ### System-computed * membership\_id is generated via gen\_random\_uuid() as a database default and also seeded via randomUUID() in the entity constructor, ensuring the UUID is available before the first flush. * created\_at is set by the @Property onCreate lifecycle hook and is never subsequently modified. * updated\_at is set by both the @Property onCreate and onUpdate lifecycle hooks; it reflects the most recent mutation to the row. * deleted\_at is the soft-delete sentinel. Setting it to a non-null timestamp revokes access. Queries scoped to active memberships always filter deleted\_at IS NULL. * is\_default invariant: at most one active (status = active, deleted\_at IS NULL) membership per firebase\_id may have is\_default = true. MembershipService.acceptInvitation computes isFirstWorkspace based on the firebase\_id scope (not the Person scope) to determine whether to set is\_default = true on the new membership. Migration20260424100000\_backfill\_membership\_default\_flag repairs any existing rows where this invariant was violated. * invite\_token is a UUID generated at invitation creation and serves as a single-use bearer credential. The invitation flow is idempotent: re-sending an invite to the same email reuses the existing pending membership row rather than inserting a duplicate. * status transitions: pending (created at invite issuance) -> active (set atomically at acceptance). A failed mid-flow acceptance must leave the row in pending status with no membership access granted. * Two composite database indexes are maintained for hot-path queries: idx\_memberships\_firebase\_workspace\_deleted (firebase\_id, workspace\_pk, deleted\_at) for per-user workspace resolution, and idx\_memberships\_workspace\_role\_deleted (workspace\_pk, membership\_role, deleted\_at) for owner/admin lookup within a workspace (added in Migration20260416000000). ## Example ```json theme={null} { "data": { "type": "membership", "id": "b3e2f1a0-4c7d-4e9f-8a1b-2d3c5e6f7890", "attributes": { "membership_id": "b3e2f1a0-4c7d-4e9f-8a1b-2d3c5e6f7890", "firebase_id": "VmN9QkR2TpU8xLdWoAhJzKcFgYbE1s3i", "membership_role": "admin", "status": "active", "is_default": true, "invite_token": null, "created_at": "2025-09-14T10:22:00.000Z", "updated_at": "2025-11-03T08:45:12.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "workspace": { "data": { "type": "workspace", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" } }, "invited_by": { "data": { "type": "people", "id": "11223344-5566-7788-99aa-bbccddeeff00" } } } } } ``` Source: `apps/api/src/database/entities/Membership.ts` · domain: workspace · tier: Platform # Memory Source: https://docs.wellapp.ai/object-reference/memories The `memories` table stores persistent AI memory blobs that the chat agent reads before composing a response The `memories` table stores persistent AI memory blobs that the chat agent reads before composing a response. Each row holds a single free-text `memory` string scoped to either a workspace (`type = 'workspace'`) or to a specific workspace member (`type = 'user'`). The entity is exclusively written by the AI Memory pipeline (`MemoryService`) via a fire-and-forget Cloud Task dispatcher; no user-facing PATCH route exists. Uniqueness constraints guarantee at most one workspace-scoped memory per workspace and at most one user-scoped memory per membership. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Memory | | Resource type (JSON:API `type`) | `memory` | | Collection / records root | — (not a records root) | | REST base | `/v1/memories` | | Entity class | `Memory` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------- | ---------- | | List | `GET /v1/memories` | 🟡 Planned | | Retrieve | `GET /v1/memories/{id}` | 🟡 Planned | | Create | `POST /v1/memories` | 🟡 Planned | | Update | `PATCH /v1/memories/{id}` | 🟡 Planned | | Delete | `DELETE /v1/memories/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | memory\_id | 🔒 system — UUID | ✅ Yes | UNIQUE; generated via gen\_random\_uuid() on INSERT | — | Public identifier for this memory record. Never expose internal pk. | | type | enum (memory\_type\_enum) | ✅ Yes | NOT NULL; DEFAULT 'user'; native PG enum `core_api.memory_type_enum`; CHECK constraint: `type = 'user' AND membership_pk IS NOT NULL` OR `type = 'workspace' AND membership_pk IS NULL` | 'user' \| 'workspace' | Scope of the memory. `user` memories are tied to a specific workspace member (membership required); `workspace` memories are shared across all members of the workspace (membership must be null). | | memory | text | ✅ Yes | NOT NULL; no length cap defined in entity or migration | — | The full, LLM-generated memory blob. Written by MemoryService.upsertMemory() after each chat exchange extraction cycle. The chat agent reads this value before composing responses to personalize answers. | | created\_at | 🔒 system — datetime | ✅ Yes | Set on INSERT via onCreate hook; never updated | — | Timestamp of the first memory extraction write for this scope. | | updated\_at | 🔒 system — datetime | ✅ Yes | Set on INSERT (onCreate) and refreshed on every UPDATE (onUpdate) by MikroORM hook | — | Timestamp of the most recent memory extraction write. The chat agent can use this to judge memory freshness. | ### Relationships | Name | Type | Required | Description | | ---------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace this memory belongs to. Tenant boundary: every memory is scoped to exactly one workspace. FK column: `workspace_pk`. Target entity: Workspace. | | membership | to-one (ManyToOne, nullable) | ⚪ No | The workspace membership (person × workspace) this memory belongs to when `type = 'user'`. Must be NULL when `type = 'workspace'` (enforced by CHECK constraint). FK column: `membership_pk`. Target entity: Membership. | ### System-computed * memory\_id — generated via gen\_random\_uuid() on INSERT * created\_at — set by MikroORM onCreate hook at INSERT time, never mutated * updated\_at — set by MikroORM onCreate hook at INSERT; refreshed by onUpdate hook on every subsequent write * type default — defaults to MemoryType.USER ('user') at the entity level; column DEFAULT 'user' also enforced in Postgres * Partial unique indexes — `memories_workspace_unique`: at most one row per workspace\_pk WHERE type = 'workspace'; `memories_membership_unique`: at most one row per membership\_pk WHERE type = 'user' — enforced by MemoryRepository.upsertForWorkspace / upsertForMembership (find-or-create pattern) * CHECK constraint memories\_type\_membership\_check — ensures type/membership\_pk coherence: user rows must have membership\_pk NOT NULL; workspace rows must have membership\_pk NULL * Whole-record upsert — MemoryService.upsertMemory() writes the entire memory blob atomically on each LLM extraction cycle; there is no incremental append path ## Example ```json theme={null} { "data": { "type": "memory", "id": "a3f8c2d1-7e54-4b6a-9c0d-1f2e3a4b5c6d", "attributes": { "type": "user", "memory": "User prefers invoices sorted by due_date ascending. Frequently asks about overdue invoices and outstanding balances. Works in EUR and occasionally GBP.", "created_at": "2026-03-20T09:14:33.000Z", "updated_at": "2026-06-01T14:52:10.000Z" }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws_9f3e2a1b-4c5d-6e7f-8a9b-0c1d2e3f4a5b" } }, "membership": { "data": { "type": "membership", "id": "ms_b1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/Memory.ts` · domain: intelligence · tier: Activity # Payment Means Source: https://docs.wellapp.ai/object-reference/payment_means A PaymentMeans record represents a payment instrument — a bank account, card, or paper check — linked to a financial transaction as either the debtor or credito A PaymentMeans record represents a payment instrument — a bank account, card, or paper check — linked to a financial transaction as either the debtor or creditor side. It acts as the bridging entity between transactions and the specific financial instrument used, referencing a normalized Account, Card, or Check entity to carry instrument-specific details. Every payment\_means row is workspace-scoped and may be associated with a Company or a People record that owns or operates the instrument. The entity is a primary MCP sync target, and its `sourceWorkspaceConnector` tracks which connector ingested the row. | Naming | Value | | ------------------------------- | ------------------- | | Object | Payment Means | | Resource type (JSON:API `type`) | `payment_means` | | Collection / records root | `payment_means` | | REST base | `/v1/payment-means` | | Entity class | `PaymentMeans` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ------------- | | List | `GET /v1/payment-means` | ✅ Implemented | | Retrieve | `GET /v1/payment-means/{id}` | ✅ Implemented | | Create | `POST /v1/payment-means` | 🟡 Planned | | Update | `PATCH /v1/payment-means/{id}` | 🟡 Planned | | Delete | `DELETE /v1/payment-means/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------------------- | ----------------------- | -------- | -------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | payment\_means\_id | string, UUID, 🔒 system | ✅ Yes | unique; defaultRaw: gen\_random\_uuid() | — | Public stable identifier for the payment instrument. Used in all API responses; the internal pk is never exposed. | | name | string | ⚪ No | length ≤ 255; nullable | — | Human-readable label for the payment instrument (e.g. 'Qonto EUR Operating', 'Visa \*\*\*\* 4321'). Populated by connectors or manually. Previously stored as digital\_wallet\_id before the January 2026 schema refactor. | | payment\_means\_external\_id | string | ⚪ No | length ≤ 255; nullable | — | Connector-assigned external identifier for this payment instrument, enabling deduplication and idempotent upserts during MCP sync. Scoped to the connector — not globally unique. | | created\_at | Date, 🔒 system | ✅ Yes | set on insert via onCreate lifecycle hook; never null | — | Timestamp of when the payment\_means row was created in Well. | | updated\_at | Date, 🔒 system | ⚪ No | set on insert and update via onUpdate lifecycle hook; nullable | — | Timestamp of the most recent update to this row. | | deleted\_at | Date | ⚪ No | nullable; soft-delete sentinel | — | When set, marks the row as soft-deleted. All repository queries must filter deleted\_at IS NULL. A compound index idx\_payment\_means\_workspace\_deleted covers (workspace, deleted\_at) for efficient scoped queries. | ### Relationships | Name | Type | Required | Description | | ------------------------ | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (Workspace) | ⚪ No | Tenant boundary. Every payment\_means must belong to a Workspace. Nullable FK in the DB but treated as mandatory in practice for all workspace-scoped reads. Compound index idx\_payment\_means\_workspace\_deleted covers this FK together with deleted\_at. | | account | to-one (Account) | ⚪ No | The bank account backing this payment instrument. Populated when the instrument is an IBAN/bank-account type. Carries IBAN, BIC, routing number, currency, and balance data on the Account entity. Indexed via idx\_payment\_means\_account. | | card | to-one (Card) | ⚪ No | The card entity backing this payment instrument. Populated when the instrument is a credit, debit, or virtual card. The Card entity carries last\_four\_digits, brand, type (credit/debit/prepaid/corporate/virtual), cardholder\_name, and expiry dates. Indexed via idx\_payment\_means\_card. | | check | to-one (Check) | ⚪ No | The check entity backing this payment instrument. Populated when the instrument is a paper cheque. The Check entity carries CMC7 line, check\_number, and issue\_date. Indexed via idx\_payment\_means\_check. | | company | to-one (Company) | ⚪ No | The company that owns or is associated with this payment instrument. Used when the instrument is held by a legal entity (e.g. a vendor's IBAN, a corporate card). Added in Migration20260109082834. Indexed via idx\_payment\_means\_company. | | people | to-one (People) | ⚪ No | The individual person associated with this payment instrument. Used when the instrument is held by a natural person (e.g. a personal IBAN, an individual cardholder). Added in Migration20260109082834. Indexed via idx\_payment\_means\_people. | | sourceWorkspaceConnector | to-one (WorkspaceConnector) | ⚪ No | The workspace connector that created or last synced this row. Populated for all MCP-connector-ingested payment\_means rows. NULL indicates a manually created or pipeline-inferred row. Used for provenance tracking and backfill auditing (see Migration20260406100000\_expand\_mcp\_sync\_models and Migration20260529093038\_backfill\_orphan\_source\_workspace\_connector). | ### System-computed * payment\_means\_id is generated via gen\_random\_uuid() as a PostgreSQL server-side default. It is unique and immutable after insert. * created\_at is set once at insert time via MikroORM onCreate lifecycle hook. It is never updated. * updated\_at is set at insert and refreshed on every update via onUpdate lifecycle hook. * deleted\_at is null on active rows. Setting it to a non-null timestamp soft-deletes the row. The compound index idx\_payment\_means\_workspace\_deleted (workspace, deleted\_at) enables efficient active-record queries per workspace. * payment\_means\_external\_id is the connector deduplication key. The connector sync pipeline uses this to perform idempotent upserts: if a row with the same payment\_means\_external\_id already exists in the workspace scope, the existing row is updated rather than a duplicate created. * sourceWorkspaceConnector is set automatically by the MCP sync pipeline when a row is ingested from a connector. It remains NULL for rows created through invoice parsing, manual entry, or other non-connector paths. * The name column was introduced in Migration20260102111942 as a rename of the legacy digital\_wallet\_id column from the original flat-column schema. It now serves as a generic free-text label regardless of instrument type. * Exactly one of account, card, or check should be set on a given row — this is a polymorphic instrument pattern. Both company and people may be set simultaneously (e.g. a personal card belonging to a company employee), or only one may be set, or neither. * Transactions reference payment\_means via debtor\_payment\_means\_pk and creditor\_payment\_means\_pk. A single payment\_means row can appear on many transactions over time. ## Example ```json theme={null} { "data": { "type": "payment_means", "id": "b4e2f1a7-83c9-4d6e-9a11-2f3c8e7b0d45", "attributes": { "payment_means_id": "b4e2f1a7-83c9-4d6e-9a11-2f3c8e7b0d45", "name": "Qonto EUR Operating", "payment_means_external_id": "pm_acc_9x2k7w", "created_at": "2026-01-15T09:22:00.000Z", "updated_at": "2026-04-03T14:05:30.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "c91f3d82-1a4b-4e77-b6f2-0a9d5c3e8f12" } }, "account": { "data": { "type": "account", "id": "f3d89b21-7e42-4c5a-b601-d8e3a9c4071f" } }, "company": { "data": { "type": "company", "id": "0ae71c3b-6f4d-4880-a92e-5b7c1d9e2f08" } }, "people": { "data": null }, "card": { "data": null }, "check": { "data": null }, "sourceWorkspaceConnector": { "data": { "type": "workspace_connector", "id": "7a3b8c12-d4e9-4f21-b5a7-1c6d2e9f0374" } } } } } ``` Source: `apps/api/src/database/entities/PaymentMeans.ts` · domain: financial-graph · tier: Main # People Source: https://docs.wellapp.ai/object-reference/people People is a workspace-scoped entity representing an individual contact — an employee, owner, or other relationship a business interacts with People is a workspace-scoped entity representing an individual contact — an employee, owner, or other relationship a business interacts with. It is the central node for personal contact data, linked to atomic contact details (emails, phones, locations, web links) through dedicated pivot entities. A People record is connected to one or more Company entities via the CompanyPerson pivot, participates in workspace Membership, and tracks its data-source provenance through the PeopleWorkspaceConnector relation. It is a primary records-page root exposed in the data-views pipeline. | Naming | Value | | ------------------------------- | ------------ | | Object | People | | Resource type (JSON:API `type`) | `people` | | Collection / records root | `people` | | REST base | `/v1/people` | | Entity class | `People` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------ | ------------- | | List | `GET /v1/people` | ✅ Implemented | | Retrieve | `GET /v1/people/{id}` | ✅ Implemented | | Create | `POST /v1/people` | ✅ Implemented | | Update | `PATCH /v1/people/{id}` | ✅ Implemented | | Delete | `DELETE /v1/people/{id}` | ✅ Implemented | | Enrich | `POST /v1/people/enrich` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | person\_id | string, UUID | ✅ Yes | unique; generated by gen\_random\_uuid() on insert | — | Public stable identifier for this person record. Used in all API responses and external references. Never expose the internal pk. | | full\_name | string | ✅ Yes | varchar(255), not null | — | Display name for the person. Stored directly on the entity (not derived at query time from first\_name + last\_name, though both component fields are also persisted). Used as the primary display label across the records UI and composites. | | first\_name | string | ✅ Yes | varchar(255), not null | — | Given (first) name component. Persisted alongside full\_name to enable name-part filtering and salutation formatting. | | last\_name | string | ✅ Yes | varchar(255), not null | — | Family (last) name component. Persisted alongside full\_name. | | job\_title | string | ⚪ No | varchar(100), nullable | — | Professional role or title of the person at their primary company. Decorated with @Enrichable — the enrichment pipeline may populate or update this field automatically. | | external\_person\_id | string | ⚪ No | nullable; partial unique index on (external\_person\_id, workspace\_pk) WHERE deleted\_at IS NULL — replaces original global unique constraint (Migration20260216120000). Uniqueness is scoped per workspace. | — | Deduplication key from the originating connector (e.g. a CRM contact ID). Used by the reconciliation pipeline for find-or-create matching: if a sync supplies an external\_person\_id already known in the workspace, the existing record is updated rather than a duplicate created. NULL for manually created records. | | created\_at | Date, 🔒 system | ✅ Yes | timestamptz, not null; set via onCreate lifecycle hook | — | Timestamp of record creation. Set automatically by MikroORM on first persist; never updated. | | updated\_at | Date, 🔒 system | ⚪ No | timestamptz, nullable; set via onCreate and onUpdate lifecycle hooks | — | Timestamp of last modification. Set on create and updated on every subsequent write by MikroORM. | | deleted\_at | Date | ⚪ No | timestamptz, nullable; null = active record | — | Soft-delete timestamp. When set, the record is treated as deleted across all queries and is excluded from the partial unique index on external\_person\_id. Never hard-deleted. All repositories must filter deleted\_at: null. | ### Relationships | Name | Type | Required | Description | | ----------------------------- | ---------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (Workspace) | ⚪ No (nullable ManyToOne) | The workspace that owns this person record. Provides multi-tenant scope: all repository queries filter by workspace. Nullable to allow global catalog entries, though in practice all active People records have a workspace. | | source\_workspace\_connector | to-one (WorkspaceConnector) | ⚪ No (nullable ManyToOne) | The WorkspaceConnector instance that created or last synced this record. NULL for manually created people. Used by the pipeline to track provenance and to surface the connector-logo-name composite in the data-views records page. | | media | to-one (Media) | ⚪ No (nullable ManyToOne) | Profile photo / avatar for this person. Linked via a direct ManyToOne to the shared Media entity. The overrides.yml marks media.url as editable with display\_type: image; write goes through add/delete join-entity mutations, not a scalar PATCH. | | emails | to-many (PersonEmail) | ⚪ No | Email addresses linked to this person through the PersonEmail pivot entity. Each pivot record carries is\_primary, is\_verify, and label (work/personal/other). A partial unique index ensures at most one primary email per person (WHERE deleted\_at IS NULL AND is\_primary IS TRUE). | | phones | to-many (PersonPhone) | ⚪ No | Phone numbers linked through the PersonPhone pivot entity. Each pivot record carries is\_primary, is\_verify, and label (mobile/work/personal/other). A partial unique index enforces at most one primary phone per person. Decorated @Enrichable on the People side — enrichment pipeline may populate phone data. | | locations | to-many (PersonLocation) | ⚪ No | Geographic locations linked through the PersonLocation pivot entity. Each pivot record carries is\_primary, is\_legal, and label. A partial unique index enforces at most one primary location per person. | | web\_links | to-many (PersonWebLink) | ⚪ No | Social profile or other web links associated with this person, linked through the PersonWebLink pivot entity. Decorated @Enrichable on the People side — enrichment pipeline may populate social links (e.g. LinkedIn). | | companies | to-many (CompanyPerson) | ⚪ No | Company associations for this person, modelled through the CompanyPerson pivot entity. Each pivot record carries a relationship\_type enum (contact / employee / owner / other, default: other). A person may be associated with multiple companies across workspaces. | | memberships | to-many (Membership) | ⚪ No | Workspace membership records for this person. The Membership entity links People to a Workspace with role and status information for platform access control. | | collect | to-many (Collect) | ⚪ No | Collect (document collection) records associated with this person. Links the People entity into the document collection pipeline. | | workspace\_connectors | to-many (WorkspaceConnector) | ⚪ No | WorkspaceConnector records whose person field points to this People row. Used to associate a connector instance with the authenticated platform user who installed or owns it. | | people\_workspace\_connectors | to-many (PeopleWorkspaceConnector) | ⚪ No | Per-row provenance records linking this People entity to specific WorkspaceConnector instances that have synced it. Each PeopleWorkspaceConnector record carries a direction enum (inbound/outbound) and its own created\_at / updated\_at / deleted\_at. Analogous to CompanyWorkspaceConnector for Companies. | ### System-computed * person\_id is generated by PostgreSQL gen\_random\_uuid() as a column default; unique constraint peoples\_person\_id\_unique enforced at the DB level. * created\_at is set via MikroORM onCreate lifecycle hook (new Date()); never updated after first persist. * updated\_at is set on both onCreate and onUpdate via MikroORM lifecycle hooks; reflects the last modification timestamp. * deleted\_at is null for active records; set to a timestamp on soft-delete. The partial unique index on (external\_person\_id, workspace\_pk) excludes rows where deleted\_at IS NOT NULL, allowing soft-deleted records to be re-created with the same external\_person\_id. * external\_person\_id is the deduplication key for connector-driven sync: if a sync provides an external\_person\_id already present in the workspace (under the partial unique constraint), the reconciliation pipeline updates the existing record rather than creating a new one. * sourceWorkspaceConnector is set by the sync pipeline at creation time to record which WorkspaceConnector produced this record; NULL for manually created people. * The @Enrichable decorator on job\_title, phones, and webLinks marks these fields as candidates for AI enrichment via the enrichment pipeline (Cloud Tasks workers). * Two partial indexes are maintained by migrations for the records-page query path: idx\_peoples\_workspace\_deleted (workspace\_pk WHERE deleted\_at IS NULL) and idx\_peoples\_workspace\_created\_active (workspace\_pk, created\_at DESC WHERE deleted\_at IS NULL) for default sort order. * The composite\_avatar\_fullname composite field (source\_fields: person\_id, media.url, full\_name; display\_type: people\_avatar\_name) is materialized at query time by the data-views pipeline — it is not a persisted column. * The sourceWorkspaceConnector.composite\_connector\_logo\_name composite (source\_fields: workspace\_connector\_id, connector.service\_id, connector.name; display\_type: connector\_logo\_name) is likewise a query-time composite on the people records root. ## Example ```json theme={null} { "data": { "type": "people", "id": "a3f1c2d4-7e89-4b10-bcd2-1f234567890a", "attributes": { "person_id": "a3f1c2d4-7e89-4b10-bcd2-1f234567890a", "full_name": "Sophie Marotremy", "first_name": "Sophie", "last_name": "Marotremy", "job_title": "Head of Finance", "external_person_id": "crm_contact_0049281", "created_at": "2025-11-03T09:14:22.000Z", "updated_at": "2026-03-17T14:55:10.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "f9e2a1b3-0000-4c2d-8888-aabbccddeeff" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "7a123456-dead-beef-cafe-000000000001" } }, "media": { "data": { "type": "media", "id": "bb887766-5544-3322-1100-aabbccddeeff" } }, "emails": { "data": [ { "type": "person_email", "id": "11223344-aaaa-bbbb-cccc-000000000001" } ] }, "phones": { "data": [ { "type": "person_phone", "id": "55667788-aaaa-bbbb-cccc-000000000001" } ] }, "locations": { "data": [ { "type": "person_location", "id": "99aabbcc-aaaa-bbbb-cccc-000000000001" } ] }, "web_links": { "data": [ { "type": "person_web_link", "id": "ddeeff00-aaaa-bbbb-cccc-000000000001" } ] }, "companies": { "data": [ { "type": "company_person", "id": "fa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } ] }, "memberships": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/People.ts` · domain: financial-graph · tier: Main # PeopleWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/people_workspace_connectors PeopleWorkspaceConnector is a direction-discriminated junction table that records per-row provenance between a `People` record and a `WorkspaceConnector` instan PeopleWorkspaceConnector is a direction-discriminated junction table that records per-row provenance between a `People` record and a `WorkspaceConnector` instance. Each row answers the question "which connector sourced or consumed this person, and in which direction?" — `input` for connectors that delivered the record into Well, `output` for connectors that received it from Well. Tenant isolation is inherited transitively through the `People.workspace` and `WorkspaceConnector.workspace` relationships; there is no direct `workspace_pk` column on the junction itself. The table was introduced in Migration20260505200000 as part of a five-entity junction pattern (W19 PR-A1), following the `document_workspace_connectors` precedent. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PeopleWorkspaceConnector | | Resource type (JSON:API `type`) | `people_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/people-workspace-connectors` | | Entity class | `PeopleWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------------------- | ---------- | | List | `GET /v1/people-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/people-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/people-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/people-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/people-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | direction | 🔒 system — DirectionEnum (native PG enum `direction_enum`) | ✅ Yes | NOT NULL; CHECK via PG enum type — stored values are the enum VALUE strings | `input` \| `output` | Classifies the connector's role for this person: `input` means the connector delivered (sourced) the record into Well; `output` means the connector consumed (distributed) the record from Well. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL; default now(); set once on INSERT via MikroORM `onCreate` hook | — | Timestamp of row insertion. Set automatically by the ORM on create; never updated thereafter. | | updated\_at | 🔒 system — timestamptz | ⚪ No | Nullable; set on INSERT and on every UPDATE via MikroORM `onCreate` / `onUpdate` hooks | — | Timestamp of the most recent mutation. Maintained automatically by the ORM. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | Nullable; NULL = active row; non-NULL = soft-deleted | — | Soft-delete timestamp. When set, the row is treated as logically deleted and filtered from normal queries. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | people | to-one (ManyToOne) | ✅ Yes | The `People` record this junction row links. FK column `people_pk` references `core_api.peoples.pk` ON UPDATE CASCADE. Tenant isolation is inherited via `People.workspace`. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The `WorkspaceConnector` instance that sourced or consumed this person. FK column `workspace_connector_pk` references `core_api.workspace_connectors.pk` ON UPDATE CASCADE. | ### System-computed * pk — auto-increment serial primary key, internal only; never exposed on the public API * created\_at — set to `new Date()` on INSERT via MikroORM `onCreate: () => new Date()` hook; PG column default is `now()` * updated\_at — set on INSERT and updated on every subsequent mutation via MikroORM `onCreate` / `onUpdate` hooks * deleted\_at — soft-delete sentinel; injected as NULL on INSERT; set by the connector sync orchestrator when a provenance link is invalidated or the connector is disconnected * Tenant isolation is not a stored column on this table — it is derived transitively through `people.workspace_pk` and `workspace_connector.workspace_pk`; Hasura RLS uses relationship traversal rather than a direct workspace\_pk filter * No UNIQUE constraint on (people\_pk, workspace\_connector\_pk, direction) — duplicate rows are an accepted design gap per the W19 migration rationale; deduplication is deferred to a future iteration * Two composite B-tree indexes: `idx_people_workspace_connectors_record_created` on (people\_pk, created\_at) for record-led traversals; `idx_people_workspace_connectors_wc_created_at` on (workspace\_connector\_pk, created\_at) for sync-status range queries ## Example ```json theme={null} { "data": { "type": "people_workspace_connector", "id": "a3f92c11-8d14-4e2b-b305-7c1e0f3a9d82", "attributes": { "direction": "input", "created_at": "2026-05-10T14:32:00.000Z", "updated_at": "2026-05-10T14:32:00.000Z", "deleted_at": null }, "relationships": { "people": { "data": { "type": "people", "id": "d1e4b7f0-3c2a-4891-a5f6-0b9e8c7d6e5f" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "9b8c7a6d-5e4f-3210-b1a0-c9d8e7f6a5b4" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/PeopleWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # PersonEmail Source: https://docs.wellapp.ai/object-reference/person_emails PersonEmail is the pivot relation that links a People record to an Email address within the financial graph PersonEmail is the pivot relation that links a People record to an Email address within the financial graph. It carries metadata about the association — whether the address is the person's primary contact, whether it has been verified, and a label classifying its context (work, personal, other). A single person may have multiple PersonEmail rows; at most one may carry `is_primary = true` per person (enforced by a partial unique index). The entity is workspace-scoped indirectly through the `People` side and is written exclusively by the connector-sync and enrichment pipelines. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PersonEmail | | Resource type (JSON:API `type`) | `person_email` | | Collection / records root | — (not a records root) | | REST base | `/v1/person-emails` | | Entity class | `PersonEmail` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ---------- | | List | `GET /v1/person-emails` | 🟡 Planned | | Retrieve | `GET /v1/person-emails/{id}` | 🟡 Planned | | Create | `POST /v1/person-emails` | 🟡 Planned | | Update | `PATCH /v1/person-emails/{id}` | 🟡 Planned | | Delete | `DELETE /v1/person-emails/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | is\_primary | boolean | ⚪ No | DEFAULT false; partial unique index uniq\_person\_emails\_primary\_person enforces at-most-one primary per person among non-deleted rows | true \| false | Whether this email address is the person's primary contact address. At most one PersonEmail per person may have is\_primary = true while deleted\_at IS NULL (enforced by partial unique index uniq\_person\_emails\_primary\_person on person\_pk WHERE deleted\_at IS NULL AND is\_primary IS TRUE). | | is\_verify | boolean | ⚪ No | DEFAULT false | true \| false | Whether the email address has been verified for this person. Set by the enrichment or connector-sync pipeline; not validated via a user-facing flow. | | label | 🔒 system — PersonEmailLabelEnum (native PG enum person\_email\_label\_enum) | ✅ Yes | DEFAULT 'other'; native enum person\_email\_label\_enum — stored values are the enum VALUE strings | 'work' \| 'personal' \| 'other' | Classifies the context of this email address for the person. Stored as a native PostgreSQL enum introduced in Migration20251204160141. | | created\_at | 🔒 system — Date (timestamptz) | ✅ Yes | NOT NULL; set on insert via onCreate: () => new Date() | — | Timestamp when the pivot row was created. Set by the MikroORM onCreate hook; never updated. | | deleted\_at | Date \| null (timestamptz) | ⚪ No | NULLABLE; no default (equivalent to null) | — | Soft-delete timestamp. Null when the row is active. All queries must filter deleted\_at IS NULL. The partial index idx\_person\_emails\_person and the primary-person uniqueness index both scope to deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ------ | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | person | to-one (ManyToOne) | ✅ Yes | The People record that owns this email address. Foreign key person\_pk references core\_api.peoples(pk) ON UPDATE CASCADE. An index idx\_person\_emails\_person covers person\_pk WHERE deleted\_at IS NULL for reverse-traversal hot-path queries. | | email | to-one (ManyToOne) | ✅ Yes | The Email atomic record containing the address string. Foreign key email\_pk references core\_api.emails(pk) ON UPDATE CASCADE. An index idx\_person\_emails\_email covers the email\_pk column to support the Hasura emails → person\_emails array relationship traversal. | ### System-computed * created\_at — set on insert via MikroORM onCreate hook; never updated (no updated\_at column on this entity) * deleted\_at — soft-delete; set to current timestamp by the pipeline or service that removes the association; queries must always predicate deleted\_at IS NULL * is\_primary uniqueness — enforced by partial unique index uniq\_person\_emails\_primary\_person (person\_pk) WHERE deleted\_at IS NULL AND is\_primary IS TRUE; the pipeline is responsible for demoting any prior primary before promoting a new one * label default — MikroORM entity-level default PersonEmailLabelEnum.OTHER ('other') applied at construction; mirrored by database DEFAULT 'other' set in Migration20251204160141 * is\_primary / is\_verify defaults — entity-level TypeScript defaults (false) mirrored by database DEFAULT false set in Migration20251204160141 * No UUID public identifier — PersonEmail has no \*\_id UUID column; it is not directly addressable via the public API as a standalone resource; it is always accessed through its parent People or Email relationship ## Example ```json theme={null} { "data": { "type": "person_email", "id": null, "attributes": { "is_primary": true, "is_verify": false, "label": "work", "created_at": "2025-11-14T09:22:07.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "7e4c2f91-3b8a-4d56-9f1e-bc23047a8e12" } }, "email": { "data": { "type": "email", "id": "a1d0c29b-5e3f-4812-8b7a-fc901234abcd" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/PersonEmail.ts` · domain: financial-graph · tier: Supporting # PersonLocation Source: https://docs.wellapp.ai/object-reference/person_locations PersonLocation is a pivot (bridge) entity that links a `People` record to a `Location` record, recording whether that address is the person's primary address an PersonLocation is a pivot (bridge) entity that links a `People` record to a `Location` record, recording whether that address is the person's primary address and whether it serves a legal purpose. It mirrors the `company_locations` pattern applied to individuals. The entity is workspace-scoped indirectly through its owning `People` record, carries soft-delete semantics via `deleted_at`, and enforces a partial-unique constraint ensuring at most one primary address per person among non-deleted rows. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PersonLocation | | Resource type (JSON:API `type`) | `person_location` | | Collection / records root | — (not a records root) | | REST base | `/v1/person-locations` | | Entity class | `PersonLocation` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------- | ---------- | | List | `GET /v1/person-locations` | 🟡 Planned | | Retrieve | `GET /v1/person-locations/{id}` | 🟡 Planned | | Create | `POST /v1/person-locations` | 🟡 Planned | | Update | `PATCH /v1/person-locations/{id}` | 🟡 Planned | | Delete | `DELETE /v1/person-locations/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | is\_primary | boolean | ✅ Yes | Partial unique index: only one row per person\_pk may have is\_primary = TRUE where deleted\_at IS NULL (index uniq\_person\_locations\_primary\_person) | true \| false | Marks this address as the person's primary (canonical) address. At most one non-deleted PersonLocation per person may be primary. | | is\_legal | boolean | ✅ Yes | Not null | true \| false | Indicates whether this address is the person's legal domicile (registered address for official/legal purposes). | | label | string | ✅ Yes | varchar(255), not null | — | Human-readable label categorising the address (e.g. 'Home', 'Work', 'Billing'). Free-form text up to 255 characters. | | created\_at | 🔒 system — Date | ✅ Yes | timestamptz, set on insert via onCreate hook, never updated | — | Timestamp when this pivot row was created. System-set on insert; not writable by the user. | | deleted\_at | Date \| null | ⚪ No | timestamptz, nullable; NULL means active; partial indexes filter WHERE deleted\_at IS NULL | — | Soft-delete timestamp. When set, the row is logically deleted and excluded from all active queries. System-managed; not directly patchable. | ### Relationships | Name | Type | Required | Description | | -------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | person | to-one (ManyToOne) | ✅ Yes | The People record this address is attached to. Foreign key person\_pk → core\_api.people.pk. Indexed via partial index idx\_person\_locations\_person (WHERE deleted\_at IS NULL). | | location | to-one (ManyToOne) | ✅ Yes | The Location record (address details) associated with this pivot. Foreign key location\_pk → core\_api.locations.pk. Indexed via idx\_person\_locations\_location. | ### System-computed * pk — auto-increment serial integer, internal join key only; never exposed on the public API. * created\_at — set to new Date() via @Property() on insert; no subsequent updates (no updated\_at column on this entity). * deleted\_at — soft-delete sentinel; null on creation; set by the application soft-delete service, never by user PATCH. * Partial unique index uniq\_person\_locations\_primary\_person enforces at most one is\_primary = TRUE row per person\_pk among non-deleted rows — maintained by the database, not application code. * The entity has no updated\_at column; mutations that change is\_primary or label do not produce a timestamp trail beyond created\_at. * No UUID public id column — PersonLocation does not carry a \*\_id UUID field; it is identified externally via the combination of person relationship + location relationship. * No workspace\_pk column — tenant isolation is inherited transitively through the person → workspace path; Hasura RLS walks this relationship chain. ## Example ```json theme={null} { "data": { "type": "person_location", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "attributes": { "is_primary": true, "is_legal": false, "label": "Home", "created_at": "2025-09-14T10:23:00.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "location": { "data": { "type": "location", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" } } } } } ``` Source: `apps/api/src/database/entities/PersonLocation.ts` · domain: financial-graph · tier: Supporting # PersonMedia Source: https://docs.wellapp.ai/object-reference/person_media PersonMedia is a soft-deletable pivot (junction) entity that links a People record to a Media record, enabling a person to have one or more associated media ass PersonMedia is a soft-deletable pivot (junction) entity that links a People record to a Media record, enabling a person to have one or more associated media assets (profile photos, avatars, documents). It carries no business attributes beyond its two foreign keys and lifecycle timestamps. The entity is workspace-scoped implicitly through its People relation; it belongs to the Supporting category alongside similar pivot tables such as CompanyMedia and PersonEmail. Every row is authored by the connector sync or enrichment pipeline; no public PATCH route exists for end-users to modify it directly. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PersonMedia | | Resource type (JSON:API `type`) | `person_media` | | Collection / records root | — (not a records root) | | REST base | `/v1/person-media` | | Entity class | `PersonMedia` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------ | ---------- | | List | `GET /v1/person-media` | 🟡 Planned | | Retrieve | `GET /v1/person-media/{id}` | 🟡 Planned | | Create | `POST /v1/person-media` | 🟡 Planned | | Update | `PATCH /v1/person-media/{id}` | 🟡 Planned | | Delete | `DELETE /v1/person-media/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------ | -------- | ------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | datetime 🔒 system | ✅ Yes | NOT NULL; set by onCreate: () => new Date() | ISO 8601 UTC datetime | Timestamp set once at row creation via MikroORM onCreate hook. Never updated afterwards. | | deleted\_at | datetime \| null | ⚪ No | nullable | ISO 8601 UTC datetime or null | Soft-delete timestamp. NULL means the pivot link is active. Set by the soft-delete lifecycle when the link is removed; never cleared once set. | ### Relationships | Name | Type | Required | Description | | ------ | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | person | to-one (ManyToOne) | ✅ Yes | The People record this media asset is attached to. FK person\_pk → core\_api.peoples.pk. Indexed via idx\_person\_media\_person for reverse traversal from People to its media. | | media | to-one (ManyToOne) | ✅ Yes | The Media record (file/image asset) associated with the person. FK media\_pk → core\_api.media.pk. Indexed via idx\_person\_media\_media to support Hasura media → person\_media array relationship traversal. | ### System-computed * pk — auto-increment serial integer, internal join key only; never exposed via the public API. * created\_at — set by MikroORM onCreate hook (onCreate: () => new Date()) at row insertion; not updated on subsequent writes. * deleted\_at — managed by the soft-delete lifecycle; NULL on creation, set to current timestamp when the pivot link is logically removed. Queries must always filter deleted\_at IS NULL to respect soft-delete semantics. * Note on migration parity gap: the Migration20250919154301.ts DDL created the table with an updated\_at timestamptz NULL column, but the entity class declares no @Property for updated\_at. The column exists in the database but is unmapped in ORM layer — it will remain NULL for all rows unless manually backfilled or the entity is amended. ## Example ```json theme={null} { "data": { "type": "person_media", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "created_at": "2025-11-14T09:22:34.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } }, "media": { "data": { "type": "media", "id": "c3d4e5f6-a1b2-4890-bcde-f01234567890" } } } } } ``` Source: `apps/api/src/database/entities/PersonMedia.ts` · domain: financial-graph · tier: Supporting # PersonPhone Source: https://docs.wellapp.ai/object-reference/person_phones PersonPhone is a pivot entity that links a People record to a Phone record within a workspace, carrying metadata about the association PersonPhone is a pivot entity that links a People record to a Phone record within a workspace, carrying metadata about the association. It implements the canonical pivot pattern with `is_primary`, `is_verify`, and `label` fields so a single person can hold multiple phone numbers under different categories. Records are written by the connector sync pipeline and by user-facing People mutations (add/remove phone); there is no standalone PATCH surface for individual `person_phones` rows. Soft-delete semantics via `deleted_at` are enforced, and partial unique indexes guarantee at most one primary phone per person at any time. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PersonPhone | | Resource type (JSON:API `type`) | `person_phone` | | Collection / records root | — (not a records root) | | REST base | `/v1/person-phones` | | Entity class | `PersonPhone` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------- | ---------- | | List | `GET /v1/person-phones` | 🟡 Planned | | Retrieve | `GET /v1/person-phones/{id}` | 🟡 Planned | | Create | `POST /v1/person-phones` | 🟡 Planned | | Update | `PATCH /v1/person-phones/{id}` | 🟡 Planned | | Delete | `DELETE /v1/person-phones/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | is\_primary | boolean | ⚪ No | DEFAULT false; partial unique index `uniq_person_phones_primary_person` enforces at most one row per `person_pk` WHERE `deleted_at IS NULL AND is_primary IS TRUE` | true / false | Marks this phone number as the person's primary contact number. At most one active PersonPhone per person may have `is_primary = true` at a time (enforced by partial unique index). | | is\_verify | boolean | ⚪ No | DEFAULT false | true / false | Indicates whether the phone number has been verified (e.g., via OTP or connector confirmation). | | label | 🔒 system enum — `person_phone_label_enum` (native PostgreSQL enum) | ✅ Yes | DEFAULT 'other'; stored as native enum `core_api.person_phone_label_enum`; values are the enum VALUE strings (lowercase) | mobile \| work \| personal \| other | Categorises the nature or context of the phone number. Stored in a native PostgreSQL enum; the persisted value is the lowercase string (e.g. `"mobile"`, not the TypeScript key `MOBILE`). | | created\_at | 🔒 system datetime | ✅ Yes | Set automatically on INSERT via `onCreate` hook; `timestamptz NOT NULL` | — | Timestamp when the pivot record was created. Set by the MikroORM `onCreate` lifecycle hook; never updated afterward. | | deleted\_at | 🔒 system datetime \| null | ⚪ No | Nullable `timestamptz`; all active-record queries must filter `deleted_at IS NULL` | — | Soft-delete timestamp. When set, the link between the person and phone is logically removed but the row is retained for audit. The partial indexes on `person_pk` and the primary-phone uniqueness constraint both scope themselves to `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | ------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | person | to-one (ManyToOne) | ✅ Yes | The People record this phone number is associated with. Foreign key `person_pk` → `core_api.people.pk`. Indexed via partial index `idx_person_phones_person` (`person_pk` WHERE `deleted_at IS NULL`) for forward traversal, plus partial unique index for primary-phone uniqueness. | | phone | to-one (ManyToOne) | ✅ Yes | The atomic Phone record containing the actual phone number value (e.g. `e164_number`). Foreign key `phone_pk` → `core_api.phones.pk`. Indexed via `idx_person_phones_phone` (`phone_pk`) for reverse traversal (phones → person\_phones array relationship). | ### System-computed * pk — auto-increment serial, internal join key only; never exposed in the public API * created\_at — set on INSERT by MikroORM `onCreate: () => new Date()` hook; no `onUpdate` hook exists on this entity (no `updated_at` column) * deleted\_at — soft-delete field; set to current timestamp when the phone association is removed via People mutation or connector sync; null on active records * label default — ORM default `PersonPhoneLabelEnum.OTHER` ('other') applied on entity construction; also enforced as `DEFAULT 'other'` in PostgreSQL via the native enum column * is\_primary default — ORM default `false`; also `DEFAULT false` in PostgreSQL * is\_verify default — ORM default `false`; also `DEFAULT false` in PostgreSQL * Partial unique index `uniq_person_phones_primary_person` — system-enforced constraint preventing more than one primary phone per person among non-deleted rows; managed at DB level, not ORM level ## Example ```json theme={null} { "data": { "type": "person_phone", "attributes": { "is_primary": true, "is_verify": false, "label": "mobile", "created_at": "2025-11-14T09:23:17.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "d3a1c9e7-4f02-4b88-9c11-2e5f7a83bc40" } }, "phone": { "data": { "type": "phone", "id": "a7b2e451-0c3d-4e6f-8110-9d4f2c1b5e78" } } } } } ``` Source: `apps/api/src/database/entities/PersonPhone.ts` · domain: financial-graph · tier: Supporting # PersonWebLink Source: https://docs.wellapp.ai/object-reference/person_web_links PersonWebLink is a soft-deletable bridge (pivot) relation that attaches one or more web hyperlinks to a person record PersonWebLink is a soft-deletable bridge (pivot) relation that attaches one or more web hyperlinks to a person record. It connects the `People` entity to the `WebLink` atomic entity, enabling a person to carry multiple URLs (e.g. LinkedIn profile, personal website) without denormalising the link data. The pivot follows the standard dual-direction indexing pattern: a composite index on `(person, deleted_at)` for the forward traversal and a single-column index on `web_link` for the reverse traversal. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | PersonWebLink | | Resource type (JSON:API `type`) | `person_web_link` | | Collection / records root | — (not a records root) | | REST base | `/v1/person-web-links` | | Entity class | `PersonWebLink` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------- | ---------- | | List | `GET /v1/person-web-links` | 🟡 Planned | | Retrieve | `GET /v1/person-web-links/{id}` | 🟡 Planned | | Create | `POST /v1/person-web-links` | 🟡 Planned | | Update | `PATCH /v1/person-web-links/{id}` | 🟡 Planned | | Delete | `DELETE /v1/person-web-links/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | Date (timestamptz) — 🔒 system | ✅ Yes | set once on INSERT via onCreate hook; never null | — | Timestamp at which the pivot row was created. Set automatically by the MikroORM onCreate hook; not editable by the user. | | updated\_at | Date (timestamptz) \| undefined — 🔒 system | ⚪ No | set on INSERT and on every UPDATE via onCreate/onUpdate hooks; nullable in DB (timestamptz null) | — | Timestamp of the last modification to this pivot row. Updated automatically by MikroORM; not editable by the user. | | deleted\_at | Date (timestamptz) \| null — 🔒 system | ⚪ No | nullable; soft-delete sentinel; all active-record queries must filter deleted\_at IS NULL | — | Soft-delete timestamp. Null means the link is active. Set by the soft-delete cascade when the parent person is deleted or the link is explicitly removed. Not directly editable by the user. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | person | to-one (ManyToOne) | ✅ Yes | The People record to which this web link belongs. Foreign key: person\_pk → core\_api.peoples.pk. Cascade: ON UPDATE CASCADE. Indexed via the composite partial index idx\_person\_web\_links\_person\_deleted (person\_pk, deleted\_at). | | web\_link | to-one (ManyToOne) | ✅ Yes | The WebLink atomic entity carrying the actual URL. Foreign key: web\_link\_pk → core\_api.web\_links.pk. Cascade: ON UPDATE CASCADE. Indexed via idx\_person\_web\_links\_web\_link. | ### System-computed * created\_at — set on INSERT via MikroORM onCreate hook (new Date()); never written by API consumers * updated\_at — set on INSERT and refreshed on every UPDATE via onCreate/onUpdate hooks * deleted\_at — set by soft-delete cascade; never set directly by the user; queries must always predicate deleted\_at IS NULL * No public UUID (\*\_id) column exists on this entity — it is a pure pivot table identified by its two FK columns; the external API should reference it via the parent person\_id + web\_link\_id pair * No workspace\_pk column — tenant isolation is inherited through the parent people → workspace relationship; queries must join through People to apply workspace scope ## Example ```json theme={null} { "data": { "type": "person_web_link", "id": "a3f7c912-08b4-4e61-bd2a-91c053e7f8d0", "attributes": { "created_at": "2025-10-14T09:22:00.000Z", "updated_at": "2025-10-14T09:22:00.000Z", "deleted_at": null }, "relationships": { "person": { "data": { "type": "people", "id": "e8d22a41-3319-4c7e-b901-7714fce6a1b2" } }, "web_link": { "data": { "type": "web_link", "id": "c14b89f3-5510-4df0-a823-003e8fa0d77c" } } } } } ``` Source: `apps/api/src/database/entities/PersonWebLink.ts` · domain: financial-graph · tier: Supporting # Phone Source: https://docs.wellapp.ai/object-reference/phones Phone is a shared atomic resource representing a single telephone number stored in its canonical decomposed form: ITU-T country-calling-code, national-number st Phone is a shared atomic resource representing a single telephone number stored in its canonical decomposed form: ITU-T country-calling-code, national-number string, and the derived E.164 number. It is linked to People and Company records through pivot entities (PersonPhone, CompanyPhone) that carry relationship-level metadata such as primary/verified status and a label. A Phone always belongs to a workspace via a nullable integer FK (`workspace_pk`) added in Migration20260119180000 and is soft-deletable. It is exposed as a records root named "phones" in the data-views pipeline with composite columns resolving linked companies and people. | Naming | Value | | ------------------------------- | ------------ | | Object | Phone | | Resource type (JSON:API `type`) | `phone` | | Collection / records root | `phones` | | REST base | `/v1/phones` | | Entity class | `Phone` | ## API operations | Operation | Method & path | Status | | --------------- | --------------------------------------- | ------------- | | List | `GET /v1/phones` | ✅ Implemented | | List (nested) | `GET /v1/people/{id}/phones` | 🟡 Planned | | Retrieve | `GET /v1/phones/{id}` | ✅ Implemented | | Create (nested) | `POST /v1/people/{id}/phones` | ✅ Implemented | | Update | `PATCH /v1/phones/{id}` | 🟡 Planned | | Delete (nested) | `DELETE /v1/people/{id}/phones/{subId}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ----------------------- | -------- | ------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | phone\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() on INSERT | — | Public stable identifier for the phone number. Used in all API responses and cross-resource references. The internal pk is never exposed. | | country\_code | integer | ✅ Yes | NOT NULL | Any valid ITU-T calling code (e.g. 1, 33, 44, 49) | The international dialing prefix without the leading '+'. For example France = 33, US = 1, UK = 44. | | national\_number | string | ✅ Yes | varchar(255); NOT NULL | — | The national (subscriber) portion of the phone number, without the country code prefix and without leading zeros stripped. Stored as a string to preserve leading zeros for countries that require them. | | e164\_number | string | ✅ Yes | varchar(255); NOT NULL | — | The fully-qualified E.164 representation of the number, including the leading '+' and country code. This is the canonical machine-readable form used for display, deduplication, and connector sync matching. | | created\_at | datetime, 🔒 system | ✅ Yes | NOT NULL; set once on INSERT via onCreate lifecycle hook | — | Timestamp at which the Phone row was created. Immutable after creation. | | updated\_at | datetime, 🔒 system | ⚪ No | nullable; set on INSERT and updated on every UPDATE via onUpdate lifecycle hook | — | Timestamp of the last modification to the Phone row. Null if the row has never been updated after creation. | | deleted\_at | datetime | ⚪ No | nullable; null = active record | — | Soft-delete timestamp. When set, the phone number is considered deleted and all queries must filter deleted\_at IS NULL. Hard-deletes are not used. | ### Relationships | Name | Type | Required | Description | | --------------- | ------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No (nullable) | The workspace that owns this phone number. @ManyToOne(() => Workspace, \{ nullable: true }). The DB column is `workspace_pk` (int FK → core\_api.workspaces.pk, ON DELETE SET NULL), added by Migration20260119180000. Provides tenant isolation for Hasura RLS filtering. | | person\_phones | to-many (person\_phone) | — | Pivot entities linking this phone to one or more People records. Each PersonPhone carries is\_primary (with a partial-unique constraint: only one primary per person while deleted\_at IS NULL), is\_verify, label (PersonPhoneLabelEnum: mobile \| work \| personal \| other), and created\_at. Reverse-traversal index idx\_person\_phones\_phone is on phone\_pk. | | company\_phones | to-many (company\_phone) | — | Pivot entities linking this phone to one or more Company records. Each CompanyPhone carries is\_primary (partial-unique: one primary per company while deleted\_at IS NULL), is\_verify, label (free-text string), and created\_at. Bridge-table indexes: idx\_company\_phones\_phone (phone\_pk), idx\_company\_phones\_company\_deleted (company\_pk, deleted\_at). | ### System-computed * phone\_id is generated by gen\_random\_uuid() at the database level on INSERT and carries a UNIQUE constraint. It is the public API identifier; the internal pk (serial int) is never exposed. * created\_at is set via MikroORM onCreate lifecycle hook (new Date()) and is immutable thereafter. * updated\_at is set on both INSERT (onCreate) and every UPDATE (onUpdate) via lifecycle hooks. * deleted\_at is null by default. Setting it to a non-null timestamp performs a soft-delete. All reads must include a deleted\_at IS NULL predicate. Hard-deletes are not used for Phone rows. * workspace FK column in the DB is `workspace_pk` (int, nullable FK → core\_api.workspaces.pk, ON DELETE SET NULL), added by Migration20260119180000. Pre-migration rows have workspace\_pk = NULL. The entity declares the relation nullable: true. There is no UUID `workspace_id` column on the phones table. * The PersonPhone pivot enforces a partial unique index (uniq\_person\_phones\_primary\_person) so that at most one PersonPhone per person\_pk has is\_primary = TRUE while deleted\_at IS NULL. * The CompanyPhone pivot enforces a parallel partial unique index (uniq\_company\_phones\_primary\_company) so that at most one CompanyPhone per company\_pk has is\_primary = TRUE while deleted\_at IS NULL. * In the data-views pipeline, the phones root exposes two composite columns: composite\_companies\_list (display\_type: relation\_list, source\_fields: company\_phones.company.company\_id + company\_phones.company.name) and composite\_people\_list (display\_type: relation\_list, source\_fields: person\_phones.people.person\_id + person\_phones.people.full\_name). These are defined in composites.yml under the phones root. * On company and person records roots, composite\_phones\_list composites surface linked phone numbers (source\_fields: .phone.phone\_id + .phone.e164\_number) for display in the records table. ## Example ```json theme={null} { "data": { "type": "phone", "id": "a3f7c291-84e2-4d55-9fc3-b8120e4a7301", "attributes": { "phone_id": "a3f7c291-84e2-4d55-9fc3-b8120e4a7301", "country_code": 33, "national_number": "612345678", "e164_number": "+33612345678", "created_at": "2025-09-14T10:22:00.000Z", "updated_at": "2026-01-07T08:05:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "f1a2b3c4-0000-0000-0000-000000000001" } }, "person_phones": { "data": [ { "type": "person_phone", "id": "pivot-pk-91" } ] }, "company_phones": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/Phone.ts` · domain: financial-graph · tier: Supporting # Provider Source: https://docs.wellapp.ai/object-reference/providers Provider is the Well platform's catalog entry for every external service or application that a workspace can connect to or interact with via the Well extension Provider is the Well platform's catalog entry for every external service or application that a workspace can connect to or interact with via the Well extension. Each Provider record carries identity metadata (name, slug, URL, logo), geographic availability, category classification, and an optional Vision Agent skill (a SKILL.md payload that the AI navigator uses at runtime). Providers are global platform records — not scoped to a specific workspace — and are managed exclusively by the platform via seed scripts and the Vision Agent pipeline. Workspaces link to providers through the `workspace_providers` join entity, and connectors are associated with providers through the `provider_connectors` junction table. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Provider | | Resource type (JSON:API `type`) | `provider` | | Collection / records root | — (not a records root) | | REST base | `/v1/providers` | | Entity class | `Provider` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------- | ---------- | | List | `GET /v1/providers` | 🟡 Planned | | Retrieve | `GET /v1/providers/{id}` | 🟡 Planned | | Create | `POST /v1/providers` | 🟡 Planned | | Update | `PATCH /v1/providers/{id}` | 🟡 Planned | | Delete | `DELETE /v1/providers/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------- | ------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | provider\_id | string (UUID) | ✅ Yes | Unique; default gen\_random\_uuid() | — | Public immutable identifier for the provider. Generated server-side on creation. | | name | string | ✅ Yes | NOT NULL | — | Human-readable display name of the provider (e.g. 'Qonto', 'Stripe'). | | slug | string | ✅ Yes | Unique; indexed | — | URL-safe identifier used as the stable key for provider lookups and MCP routing. Unique across all providers. | | blueprint\_json | json (nullable) | ⚪ No | Nullable | — | Live blueprint payload — the executable flow JSON consumed by the Well browser extension. Populated by ContributionService.publishBlueprint or the one-shot backfill script. Null when no blueprint has been contributed. | | url | string (varchar 255) | ✅ Yes | Max length 255 | — | Canonical homepage or login URL for the provider. Used as the entry point for Vision Agent navigation. | | countries | string\[] (array, nullable) | ⚪ No | Nullable; stored as Postgres array | — | ISO-3166-1 alpha-2 country codes indicating geographic availability of the provider. Null means globally available or availability unknown. | | category | 🔒 system — enum (ProviderCategoryEnum, native type provider\_category\_enum) | ✅ Yes | Default: 'General'; NOT NULL; native Postgres enum | Finance \| Accounting \| Productivity \| CRM & Marketing \| Dev & Infra \| Telecom \| Travel & Logistics \| E-commerce & Retail \| Media & Subscriptions \| General | Thematic grouping shown in the connector onboarding UI. Stored as the enum VALUE string (e.g. 'Finance', not 'FINANCE'). Defaults to 'General'. | | searchable\_terms | string\[] (array, nullable) | ⚪ No | Nullable; stored as Postgres array | — | Additional keywords indexed for fuzzy search in the provider catalog. Supplements the name and slug for discovery. | | popularity\_score | integer (nullable) | ⚪ No | Nullable. Referenced by composite partial index idx\_providers\_active\_skill\_planner for ordered planner reads. | — | Relative popularity rank used to sort providers in the catalog and prioritize the Vision Agent planner's skill selection. Higher is more popular. Managed by the platform scoring pipeline. | | skill | text (nullable) | ⚪ No | Nullable. Added by Migration20260520161351. | — | Full SKILL.md content consumed by the Vision Agent at runtime — YAML frontmatter followed by markdown step body. Null when no skill exists; Vision Agent falls back to pure-vision mode. Written by the Vision Agent skill pipeline (Autobrowse or human contribution). | | skill\_metadata | jsonb (nullable) | ⚪ No | Nullable. Shape: ProviderSkillMetadata (name, description, version, source, confidence\_score, last\_success\_at?, last\_run\_at?, triggers?, successful\_uses?, total\_uses?, pending\_revision?). Added by Migration20260520161351. | — | Parsed SKILL.md frontmatter augmented with runtime execution statistics. Updated atomically by the Vision Agent runner after every run. Contains the optional pending\_revision sub-object when a re-synthesized skill is awaiting human review. | | skill\_status | 🔒 system — enum (ProviderSkillStatusEnum, native type provider\_skill\_status\_enum) | ✅ Yes | Default: 'none'; NOT NULL; native Postgres enum. Covered by composite partial index idx\_providers\_active\_skill\_planner (WHERE deleted\_at IS NULL AND skill\_status \<> 'none'). Added by Migration20260520161351. | none \| draft \| yellow \| green \| drifting \| broken | Lifecycle status of the Vision Agent skill. Transitions: none→draft (Autobrowse generate), draft→yellow (successfulUses ≥ 2 AND EWMA ≥ 0.7), yellow→green (EWMA ≥ 0.85), green→drifting (EWMA \< 0.7), drifting→broken (score \< 0.3), drifting→green (regen). Broken→\* requires human or Autobrowse regen. | | skill\_version | integer | ✅ Yes | Default: 0; NOT NULL. Added by Migration20260520161351. | — | Monotonic version counter incremented on every skill regeneration. Used by the pending\_revision mechanism to compute the target version stamp at synthesis time. | | created\_at | 🔒 system — timestamptz | ✅ Yes | Default: now(); NOT NULL | — | Timestamp of record creation. Set by the database default on insert; never updated. | | updated\_at | 🔒 system — timestamptz | ✅ Yes | Default: now(); onUpdate hook sets to current timestamp | — | Timestamp of last modification. Automatically maintained by MikroORM onUpdate hook on every entity flush. | | deleted\_at | 🔒 system — timestamptz (nullable) | ⚪ No | Nullable; null = active record | — | Soft-delete timestamp. Non-null means the provider has been logically deleted and is excluded from the active planner index (idx\_providers\_active\_skill\_planner WHERE deleted\_at IS NULL). | ### Relationships | Name | Type | Required | Description | | -------------------- | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | logo | to-one (Media) | No | Optional brand logo asset. References core\_api.media. Nullable ManyToOne — providers without a logo have null here. The Media entity holds GCS storage metadata. | | workspace\_providers | to-many (WorkspaceProvider) | No | Collection of workspace-to-provider link records (core\_api.workspace\_providers). Each entry represents a specific workspace that has activated or pinned this provider. Inverse side of WorkspaceProvider.provider. | | connectors | to-many (ProviderConnector) | No | Collection of connector associations via the provider\_connectors junction table. Each ProviderConnector links this provider to a specific Connector (MCP or native). Inverse side of ProviderConnector.provider. Cascade-deleted when the provider is hard-deleted. | ### System-computed * provider\_id — generated server-side via gen\_random\_uuid() database default on INSERT; never accepted from client input * created\_at — set to now() by database default on INSERT; never modified * updated\_at — automatically set to now() by MikroORM onUpdate hook on every entity flush * deleted\_at — set by soft-delete logic in the service layer; null on all active records; excluded from the active skill planner index * skill\_status — state-machine transitions driven exclusively by VisionAgentSkillService.nextStatus(); values advance from none→draft→yellow→green based on EWMA confidence scoring and successfulUses counter; never set by user input * skill\_version — monotonically incremented by the skill pipeline on every regeneration cycle; initial default 0 set by migration * skill\_metadata.successful\_uses / total\_uses — incremented atomically by the Vision Agent executor after each run; JSONB field so updates do not bump the row version for ORM change detection * skill\_metadata.pending\_revision — populated by the incremental re-synthesis loop when an active YELLOW or GREEN skill has a candidate replacement awaiting human review; cleared on promotion or rejection * popularity\_score — maintained by the platform scoring pipeline (Migration20260417100000\_provider\_scoring\_task\_layer); not computed on-the-fly per request * category — seeded and bulk-updated by migrations (Migration20260413200000\_expand\_provider\_categories); entity initializer default is 'General' * blueprint\_json — populated by ContributionService.publishBlueprint or one-shot backfill scripts; not user-editable via public API ## Example ```json theme={null} { "data": { "id": "a3f7c921-8b4e-4d1c-9ef2-d35a70bc1234", "type": "provider", "attributes": { "name": "Qonto", "slug": "qonto", "url": "https://qonto.com", "category": "Finance", "countries": ["FR", "DE", "IT", "ES"], "searchable_terms": ["bank", "neobank", "compte pro", "fintech"], "popularity_score": 980, "blueprint_json": null, "skill": "---\nname: Qonto\ndescription: Log in and sync your Qonto account\nversion: 2\n---\n# Steps\n...", "skill_status": "green", "skill_version": 2, "skill_metadata": { "name": "Qonto", "description": "Log in and sync your Qonto account", "version": 2, "source": "autobrowse", "confidence_score": 0.91, "last_success_at": "2026-05-28T14:22:00Z", "last_run_at": "2026-05-28T14:22:00Z", "successful_uses": 47, "total_uses": 51, "triggers": ["connect bank account"] }, "created_at": "2024-01-15T10:00:00Z", "updated_at": "2026-05-28T14:22:00Z", "deleted_at": null }, "relationships": { "logo": { "data": { "id": "media-uuid-here", "type": "media" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/Provider.ts` · domain: ingestion · tier: Platform # ReconciliationLink Source: https://docs.wellapp.ai/object-reference/reconciliation_links ReconciliationLink records a single AI-produced assertion that two distinct entity records represent the same real-world object (source → target) ReconciliationLink records a single AI-produced assertion that two distinct entity records represent the same real-world object (source → target). Each link carries the reconciliation `action` (e.g. `merge`), a `confidence` score, structured `reasoning`, and an optional `field_contributions` breakdown describing how individual field signals contributed to the match. The link is workspace-scoped, soft-deletable, and optionally tied to a human-review `Task` when the confidence is below auto-accept thresholds. It is written exclusively by the reconciliation pipeline (`reconciliation-persister.ts`) and is read-only from the user API layer. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | ReconciliationLink | | Resource type (JSON:API `type`) | `reconciliation_link` | | Collection / records root | — (not a records root) | | REST base | `/v1/reconciliation-links` | | Entity class | `ReconciliationLink` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------- | ---------- | | List | `GET /v1/reconciliation-links` | 🟡 Planned | | Retrieve | `GET /v1/reconciliation-links/{id}` | 🟡 Planned | | Create | `POST /v1/reconciliation-links` | 🟡 Planned | | Update | `PATCH /v1/reconciliation-links/{id}` | 🟡 Planned | | Delete | `DELETE /v1/reconciliation-links/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | ------------------------------------- | -------- | ------------------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | reconciliation\_link\_id | string (UUID) | ✅ Yes | unique | — | Public identifier for this reconciliation link, generated by `gen_random_uuid()`. Exposed on all API responses; used as the canonical external reference. | | source\_type | string | ✅ Yes | varchar(100) | — | Entity type string for the source record (e.g. `company`, `people`). Together with `source_id` identifies the left-hand side of the match assertion. | | source\_id | string | ✅ Yes | varchar(255) | — | Public UUID of the source entity record. Used with `source_type` to resolve the entity without a FK constraint, supporting heterogeneous entity graphs. | | target\_type | string | ✅ Yes | varchar(100) | — | Entity type string for the target record — the candidate that the reconciliation pipeline identified as matching the source. | | target\_id | string | ✅ Yes | varchar(255) | — | Public UUID of the target entity record. Used with `target_type` to resolve the candidate entity. | | relationship | string | ✅ Yes | varchar(100) | — | Semantic label for the nature of the match (e.g. `duplicate`, `same_company`, `alias`). Determined by the reconciliation agent's strategy. | | action | string | ✅ Yes | varchar(50) | — | Operational action the pipeline intends to take when the link is applied (e.g. `merge`, `link`). Used as an index dimension for the workspace-scoped tenant lookup index. | | confidence | number (numeric) | ✅ Yes | numeric(5,4) | — | Reconciliation confidence score in the range \[0, 1] expressed to 4 decimal places. Determines whether the link is auto-applied or routed to human review. | | reasoning | string (text) | ✅ Yes | text | — | Human-readable explanation generated by the reconciliation agent summarising why the source and target were matched. | | field\_contributions | object\[] (JSONB) \| null | ⚪ No | jsonb, nullable | — | Ordered list of `FieldContribution` objects, each describing one field signal that contributed to the confidence score. Schema: `{ source_field, candidate_field, claimed_match_type, verified_match_type, signal, weight }`. | | status | 🔒 system — enum (link\_status\_enum) | ✅ Yes | native enum `link_status_enum`; default `active` | active \| pending\_review \| rejected | Lifecycle state of the reconciliation link. Defaults to `active` on creation. Transitions to `pending_review` when confidence falls below the auto-apply threshold; set to `rejected` by the human reviewer or system. | | created\_at | 🔒 system — Date | ✅ Yes | timestamptz, not null, default now() | — | Timestamp set by `onCreate` hook when the link is first persisted. Never updated. | | updated\_at | 🔒 system — Date | ⚪ No | timestamptz, set by ORM hooks | — | Timestamp refreshed by `onCreate` and `onUpdate` MikroORM hooks on every write. Reflects the last mutation made by the pipeline. | | deleted\_at | 🔒 system — Date \| null | ⚪ No | timestamptz, nullable | — | Soft-delete timestamp. Null when active. Set by the reconciliation pipeline when a link is superseded or retracted. All workspace-scoped queries filter `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | ------------ | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (ManyToOne) | ✅ Yes | Tenant boundary. Every reconciliation link belongs to exactly one Workspace. Cascade delete: the link is hard-deleted when the workspace is deleted. Indexed via `idx_reconciliation_links_workspace` on `workspace_pk`. | | review\_task | to-one (ManyToOne, nullable) | ⚪ No | Optional reference to a Task created for human review of this link. Set when confidence is below the auto-accept threshold. FK delete rule: set null — the link persists if the review task is deleted. | ### System-computed * reconciliation\_link\_id — generated via gen\_random\_uuid() database default on INSERT; never supplied by the caller * created\_at — set by MikroORM onCreate hook (new Date()); never updated * updated\_at — set by MikroORM onCreate and onUpdate hooks; refreshed on every flush * deleted\_at — managed by the reconciliation pipeline; set to a timestamp on soft-delete; queries must always filter deleted\_at IS NULL * status — defaults to LinkStatusEnum.ACTIVE ('active') at ORM layer; transitioned by the reconciliation persister and review workflow, never by user PATCH * pk — auto-increment serial primary key; internal join key only, never exposed in API responses * field\_contributions — populated by the reconciliation scorer at write time; null when the scoring pass did not produce per-field breakdown data ## Example ```json theme={null} { "data": { "type": "reconciliation_link", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "reconciliation_link_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "source_type": "company", "source_id": "7f3e2a1b-0c4d-5e6f-a7b8-c9d0e1f23456", "target_type": "company", "target_id": "2b4d6f80-a1c3-e5f7-9012-b3c4d5e67890", "relationship": "duplicate", "action": "merge", "confidence": 0.9312, "reasoning": "Both records share the same SIREN number, registered address, and principal email domain. High-confidence deduplication candidate.", "field_contributions": [ { "source_field": "tax_id_value", "candidate_field": "tax_id_value", "claimed_match_type": "exact", "verified_match_type": "exact", "signal": "siren_match", "weight": 0.5 }, { "source_field": "domain", "candidate_field": "domain", "claimed_match_type": "exact", "verified_match_type": "exact", "signal": "domain_match", "weight": 0.3 } ], "status": "active", "created_at": "2026-03-19T12:00:00.000Z", "updated_at": "2026-03-19T12:00:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "ws-uuid-0001" } }, "review_task": { "data": { "type": "task", "id": "task-uuid-0042" } } } } } ``` Source: `apps/api/src/database/entities/ReconciliationLink.ts` · domain: financial-graph · tier: Supporting # SessionEvent Source: https://docs.wellapp.ai/object-reference/session_events SessionEvent is an append-only time-series log of Firebase auth token issuances, recording one row per distinct (membership, token_issued_at) tuple SessionEvent is an append-only time-series log of Firebase auth token issuances, recording one row per distinct (membership, token\_issued\_at) tuple. It is written exclusively by the Firebase authentication strategy at request time and is the canonical source for WAU/MAU, retention, stickiness, and engagement-bucketing analytics. Each row is tied to a Membership (which in turn scopes a user to a workspace); there is no workspace FK on the row itself. The table was renamed from login\_events in migration 20260511113135 and carries no soft-delete column — it is explicitly append-only. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | SessionEvent | | Resource type (JSON:API `type`) | `session_event` | | Collection / records root | — (not a records root) | | REST base | `/v1/session-events` | | Entity class | `SessionEvent` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/session-events` | 🟡 Planned | | Retrieve | `GET /v1/session-events/{id}` | 🟡 Planned | | Create | `POST /v1/session-events` | 🟡 Planned | | Update | `PATCH /v1/session-events/{id}` | 🟡 Planned | | Delete | `DELETE /v1/session-events/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | session\_event\_id | uuid (🔒 system) | ✅ Yes | unique; default gen\_random\_uuid() | — | Public stable identifier for this session event. Generated server-side via gen\_random\_uuid(); never supplied by the caller. | | token\_issued\_at | timestamptz (🔒 system) | ✅ Yes | NOT NULL; composite UNIQUE with membership\_pk (session\_events\_membership\_pk\_token\_issued\_at\_unique); indexed DESC (session\_events\_membership\_pk\_token\_issued\_at\_desc\_idx, session\_events\_token\_issued\_at\_desc\_idx) | — | The decoded.iat of the Firebase ID token, expressed as a timestamp. Identical for every API request served by the same 1-hour token, making this the structural dedup key. Concurrent inserts for the same (membership, token\_issued\_at) silently collapse to one row. | | event\_type | text (🔒 system) | ✅ Yes | NOT NULL; DEFAULT 'login'; CHECK event\_type IN ('login','refresh') — constraint name session\_events\_event\_type\_check | login \| refresh | Discriminates fresh sign-ins from silent token refreshes. 'login' when decoded.auth\_time equals decoded.iat (within clock skew — user just authenticated); 'refresh' when decoded.iat > decoded.auth\_time (Firebase SDK silently refreshed the token). Both are derived from the same decoded token at write time. | | created\_at | timestamptz (🔒 system) | ✅ Yes | NOT NULL; set on insert via onCreate hook | — | Wall-clock timestamp of when the row was inserted. Distinct from token\_issued\_at (which is the token's iat). Legacy rows backfilled token\_issued\_at from this column during the rename migration. | ### Relationships | Name | Type | Required | Description | | ---------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | membership | to-one (ManyToOne) | ✅ Yes | The Membership row that authenticated. Identifies both the user (Person) and the workspace scope. The FK column membership\_pk carries the composite unique constraint with token\_issued\_at. | ### System-computed * session\_event\_id — generated via gen\_random\_uuid() DB default; also initialised in-process via randomUUID() as the ORM default value * created\_at — set by MikroORM onCreate hook to new Date() at insert time; never updated * token\_issued\_at — sourced from decoded Firebase ID token iat field by firebase.strategy.ts; NOT user-supplied * event\_type — derived from decoded.auth\_time vs decoded.iat comparison in firebase.strategy.ts; default 'login' * Dedup on (membership\_pk, token\_issued\_at) — enforced at the DB layer by the unique index; SessionEventRepository.recordIfNew catches SQLSTATE 23505 with constraint name session\_events\_membership\_pk\_token\_issued\_at\_unique for silent dedup without swallowing unrelated unique violations * No deleted\_at — the table is append-only by design; soft-delete does not apply to this entity ## Example ```json theme={null} { "data": { "type": "session_event", "id": "a3c7f2e1-84bb-4d2a-b910-1e5c2f3d4a5b", "attributes": { "session_event_id": "a3c7f2e1-84bb-4d2a-b910-1e5c2f3d4a5b", "token_issued_at": "2026-06-02T09:14:00.000Z", "event_type": "login", "created_at": "2026-06-02T09:14:01.123Z" }, "relationships": { "membership": { "data": { "type": "membership", "id": "d1e2f3a4-5b6c-7d8e-9f0a-b1c2d3e4f5a6" } } } } } ``` Source: `apps/api/src/database/entities/SessionEvent.ts` · domain: platform · tier: Activity # Skill Source: https://docs.wellapp.ai/object-reference/skills A `Skill` is a workspace-scoped markdown document the AI chat LLM consumes at query time A `Skill` is a workspace-scoped markdown document the AI chat LLM consumes at query time. Two flavours coexist in the same table, discriminated by `kind`: `reference` skills are long-form procedure docs browsable in workspace settings, while `action` skills capture a specific AI action (filter, custom column, table setup) and can be shared via a public link whose URL can be dropped into any chat to replay the action. Skills are soft-deletable, carry optional authorship provenance (`created_by` membership) and optional derivation provenance (`source_thread` chat conversation), and control their own visibility (`private` vs `public`). The entity has no cascade-delete from its owning workspace, which is intentional to protect cross-workspace public-link sharing. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Skill | | Resource type (JSON:API `type`) | `skill` | | Collection / records root | — (not a records root) | | REST base | `/v1/skills` | | Entity class | `Skill` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------ | ---------- | | List | `GET /v1/skills` | 🟡 Planned | | Retrieve | `GET /v1/skills/{id}` | 🟡 Planned | | Create | `POST /v1/skills` | 🟡 Planned | | Update | `PATCH /v1/skills/{id}` | 🟡 Planned | | Delete | `DELETE /v1/skills/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | skill\_id | 🔒 system — UUID string | ✅ Yes | UNIQUE; generated by `gen_random_uuid()` default; never client-supplied. | — | Public-facing stable identifier generated by `gen_random_uuid()` at insert time. This is the `id` exposed on the JSON:API envelope. | | kind | enum (skill\_kind\_enum) | ✅ Yes | NOT NULL; native Postgres enum `core_api.skill_kind_enum`. | "reference" \| "action" | Discriminator that separates the two skill flavours. `reference` = long-form Claude-Code-style procedure; `action` = short body capturing a specific AI action shareable via public link. | | name | string (varchar 255) | ✅ Yes | NOT NULL; max length 255. | — | Human-readable display name for the skill, shown in workspace settings and the chat capability surface. | | description | string (text) \| null | ⚪ No | Nullable. | — | Optional short summary of the skill's purpose. Supplements the name for settings browsability. | | body | string (text) | ✅ Yes | NOT NULL. | — | Full markdown content of the skill that the LLM reads. For `reference` skills this is the complete procedure doc; for `action` skills it is the short action-capture body. | | visibility | enum (skill\_visibility\_enum) | ✅ Yes | NOT NULL; native Postgres enum `core_api.skill_visibility_enum`; database default `'private'`. | "private" \| "public" | Controls who can read the skill row. `private` = only members of the owning workspace; `public` = any authenticated Well user (still requires auth — not internet-public). Action skills flip to `public` on Share; reference skills remain `private`. | | created\_at | 🔒 system — datetime | ✅ Yes | NOT NULL; `TIMESTAMPTZ NOT NULL DEFAULT now()`. | — | Timestamp when the row was created. Set by MikroORM `onCreate` hook; never written by callers. | | updated\_at | 🔒 system — datetime | ✅ Yes | NOT NULL; `TIMESTAMPTZ NOT NULL DEFAULT now()`. | — | Timestamp of the most recent update. Maintained by MikroORM `onCreate` and `onUpdate` hooks. | | deleted\_at | 🔒 system — datetime \| null | ⚪ No | Nullable; `TIMESTAMPTZ`. | — | Soft-delete timestamp. NULL means the skill is live. Set by the delete handler; never written directly by callers. All list queries filter `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | -------------- | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (ManyToOne) | Yes — NOT NULL FK | The workspace that owns this skill. Deliberately has no `ON DELETE CASCADE` — a workspace hard-delete must not silently invalidate cross-workspace public-link action skills; callers must clean up skill rows first. FK column: `workspace_pk`. | | source\_thread | to-one (ManyToOne) | No — nullable FK | Originating chat conversation for action skills (provenance). Nullable; `ON DELETE SET NULL` so deleting a conversation does not break the skill. FK column: `source_thread_pk`. Points to `ChatConversation`. | | created\_by | to-one (ManyToOne) | No — nullable FK | Membership of the user who authored the skill. Nullable; `ON DELETE SET NULL` so the skill survives if the creator's membership is removed. FK column: `created_by_pk`. Points to `Membership`. | ### System-computed * skill\_id — generated by gen\_random\_uuid() at insert; never client-supplied. * created\_at — set by MikroORM onCreate hook (new Date()); maps to TIMESTAMPTZ NOT NULL DEFAULT now(). * updated\_at — set by MikroORM onCreate hook and refreshed on every flush via onUpdate hook. * deleted\_at — soft-delete field; set by the delete handler to the current timestamp; never written by callers; live queries always filter deleted\_at IS NULL. * visibility default — the ORM default and DB column default are both 'private'; callers may omit this field on create. * Partial index idx\_skills\_workspace\_kind covers (workspace\_pk, kind, created\_at DESC) WHERE deleted\_at IS NULL — list-by-workspace-and-kind queries satisfy ORDER BY without a separate sort step. * Partial index idx\_skills\_source\_thread covers (source\_thread\_pk) WHERE source\_thread\_pk IS NOT NULL AND deleted\_at IS NULL — reverse-lookup 'skills derived from this conversation' query. ## Example ```json theme={null} { "data": { "id": "d4e7f1a2-3b8c-4e9d-a0f5-1c2b3d4e5f6a", "type": "skill", "attributes": { "skill_id": "d4e7f1a2-3b8c-4e9d-a0f5-1c2b3d4e5f6a", "kind": "reference", "name": "Invoice matching procedure", "description": "Step-by-step procedure for matching supplier invoices to PO lines.", "body": "# Invoice Matching Procedure\n\n## When to use\nUse this skill whenever a supplier invoice...", "visibility": "private", "created_at": "2026-04-10T09:23:14.000Z", "updated_at": "2026-05-18T14:05:07.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "id": "a1b2c3d4-e5f6-7890-ab12-cd34ef567890", "type": "workspace" } }, "source_thread": { "data": null }, "created_by": { "data": { "id": "f7e6d5c4-b3a2-1098-7654-321fedcba098", "type": "membership" } } } } } ``` Source: `apps/api/src/database/entities/Skill.ts` · domain: automation · tier: Activity # TaskTemplate Source: https://docs.wellapp.ai/object-reference/task_templates TaskTemplate is a global, workspace-independent catalogue of onboarding and activation tasks that Well ships as canonical seed data TaskTemplate is a global, workspace-independent catalogue of onboarding and activation tasks that Well ships as canonical seed data. Each template defines the human-readable content (title, markdown body), the CTA action type that drives the frontend button, a display sort order, and an optional initial status that overrides the default `ice_log` when the seed pipeline materialises a per-workspace Task from the template. Templates have no workspace foreign key; they are append-only canonical rows managed exclusively by the `syncTaskTemplates` seed function and data migrations — users never write to this table. A `Task` row is the per-workspace materialisation of a template; many Tasks may reference the same TaskTemplate via `template_pk`. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | TaskTemplate | | Resource type (JSON:API `type`) | `task_template` | | Collection / records root | — (not a records root) | | REST base | `/v1/task-templates` | | Entity class | `TaskTemplate` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------- | ---------- | | List | `GET /v1/task-templates` | 🟡 Planned | | Retrieve | `GET /v1/task-templates/{id}` | 🟡 Planned | | Create | `POST /v1/task-templates` | 🟡 Planned | | Update | `PATCH /v1/task-templates/{id}` | 🟡 Planned | | Delete | `DELETE /v1/task-templates/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------ | ------------------------------------------- | -------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | task\_template\_id | uuid | ✅ Yes | unique | — | Public-facing UUID identifier for the template. Generated by `gen_random_uuid()` at INSERT. Exposed on the API as the resource `id`. | | slug | text | ✅ Yes | unique | — | Human-readable stable identifier used as the upsert key in the seed pipeline (ON CONFLICT slug). Examples: `connect-bank-account`, `connect-email`. Must be unique across all non-deleted templates. | | title | text | ✅ Yes | — | — | Display title shown to the user for the task. Copied into the child Task row at seed time and kept in sync by migration. | | markdown\_content | text | ✅ Yes | — | — | Full instructional body rendered to the user when they open the task. Written in Markdown; may include HTML comment directives for media embedding (e.g. `<!-- media: file.png \| alt: ... -->`). | | action\_type | 🔒 system — enum (task\_action\_type\_enum) | ⚪ No | — | connect\_email, connect\_gmail (tombstone — historical only), connect\_bank, connect\_accounting, connect\_provider, chrome\_extension\_fetch, go\_to\_invoices, go\_to\_transactions, go\_to\_workflows, confirm\_subworkspace, connect\_extension, connect\_tools, build\_canvas, manual\_upload | The CTA action triggered by the task card button. Drives frontend routing at click time. NULL means no automated CTA; the task is informational only. Uses a native Postgres enum; stored values are the enum VALUE strings listed below. | | initial\_status | 🔒 system — enum (task\_status\_enum) | ⚪ No | — | ice\_log, open, in\_progress, done, blocked, cancelled, archived | Optional override for the status assigned to the per-workspace Task row when it is materialised by the seed pipeline. When NULL the seed pipeline defaults to `ice_log`. Used to promote high-priority templates (e.g. confirm-subworkspace) directly to `open`. | | sort\_order | integer | ✅ Yes | default 0; NOT NULL | — | Controls the display order of templates / their materialised tasks (ascending). Default 0; migrations set explicit sequential values (1–N). Has a dedicated index `idx_task_templates_sort`. | | token\_reward | integer | ✅ Yes | CHECK (token\_reward >= 0); default 200 | — | Gamification reward copied into the child Task at seed time. Checked to be non-negative via a DB CHECK constraint (`token_reward >= 0`). | | created\_at | 🔒 system — timestamptz | ✅ Yes | — | — | Row creation timestamp. Set by the MikroORM `onCreate` hook; never updated. | | updated\_at | 🔒 system — timestamptz | ⚪ No | — | — | Timestamp of the last mutation. Set by the `onCreate` hook and refreshed by the `onUpdate` hook on every subsequent write. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | — | — | Soft-delete sentinel. NULL when the template is active. Set by the seed pipeline when a template is retired (e.g. `open_setup_flow` templates retired in Migration20260521). Rows with `deleted_at IS NOT NULL` are excluded from all active queries and task backfills. | ### System-computed * task\_template\_id — generated by gen\_random\_uuid() at INSERT; never written by callers * sort\_order default 0 — migrations set explicit sequential values; entity TypeScript default = 0 * token\_reward default 200 — entity TypeScript default = 200; DB DEFAULT 200 * created\_at — set by MikroORM onCreate hook (new Date()); never updated * updated\_at — set by MikroORM onCreate hook and refreshed by onUpdate hook on every write * deleted\_at — set to a timestamp by the syncTaskTemplates seed function when a template is retired; NULL for active rows * initial\_status default — when NULL the Task seed pipeline defaults the child task status to ice\_log; the column itself has no DB DEFAULT * action\_type — seeded and updated exclusively by syncTaskTemplates and data migrations; the TypeScript OptionalProps declaration means MikroORM treats it as server-side defaulted (nullable native enum) ## Example ```json theme={null} { "data": { "type": "task_template", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "slug": "connect-bank-account", "title": "Connect your bank account", "markdown_content": "# Connect your bank account\n\nSee every transaction in one place...", "action_type": "connect_bank", "initial_status": "open", "sort_order": 1, "token_reward": 200, "created_at": "2026-03-11T10:00:00.000Z", "updated_at": "2026-04-13T10:00:00.000Z", "deleted_at": null } } } ``` Source: `apps/api/src/database/entities/TaskTemplate.ts` · domain: automation · tier: Activity # Task Source: https://docs.wellapp.ai/object-reference/tasks A Task represents a discrete unit of work within a workspace — it can be user-created, agent-created, or system-seeded as part of an onboarding or provider-scor A Task represents a discrete unit of work within a workspace — it can be user-created, agent-created, or system-seeded as part of an onboarding or provider-scoring pipeline. Tasks belong to a single workspace, may be assigned to a Person, and may form a parent-child hierarchy (a task may have one parent task and many subtasks). They carry a rich status lifecycle, typed enums for executor and source, optional plan/step/scoring/subworkspace-candidate metadata in a JSONB field, and a polymorphic references array that links them to invoices, companies, people, documents, transactions, providers, or calendar months. | Naming | Value | | ------------------------------- | ----------- | | Object | Task | | Resource type (JSON:API `type`) | `task` | | Collection / records root | `tasks` | | REST base | `/v1/tasks` | | Entity class | `Task` | ## API operations | Operation | Method & path | Status | | --------- | ----------------------- | ------------- | | List | `GET /v1/tasks` | ✅ Implemented | | Retrieve | `GET /v1/tasks/{id}` | ✅ Implemented | | Create | `POST /v1/tasks` | 🟡 Planned | | Update | `PATCH /v1/tasks/{id}` | 🟡 Planned | | Delete | `DELETE /v1/tasks/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | ----------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | task\_id | string, UUID, system | ✅ Yes | unique; generated by gen\_random\_uuid() on INSERT | — | Public stable identifier for the task. Never changes after creation. | | title | string | ✅ Yes | text; no length limit enforced at DB level | — | Short human-readable label for the task, shown in the UI and chat cards. | | description | string | ⚪ No | text; nullable | — | Optional longer prose explanation of what the task requires and why. | | status | string (enum) | ✅ Yes | default = 'open'; backed by native PG enum task\_status\_enum; partial unique index uniq\_tasks\_active\_provider\_company fires only on status IN ('open','ice\_log','blocked') | ice\_log, open, in\_progress, done, blocked, cancelled, archived | Lifecycle state of the task. ice\_log = surfaced but not yet actionable; archived = closed but kept for audit. | | executor\_type | string (enum) | ✅ Yes | NOT NULL; backed by native PG enum executor\_type\_enum | human, system | Whether the task is intended for a human actor or executed entirely by the system/agent pipeline. | | source | string (enum) | ✅ Yes | default = 'user'; backed by native PG enum task\_source\_enum | user, agent | Whether the task was created by a human user or autonomously by an AI agent. | | priority | string (enum) | ⚪ No | nullable; default = 'medium'; backed by native PG enum task\_priority\_enum | low, medium, high, critical | Urgency level of the task, used to sort and filter the task list in the UI. | | confidence\_score | number (decimal) | ⚪ No | nullable; columnType numeric(3,2); range 0.00-1.00 | — | Agent-assigned confidence that the task is relevant and actionable for this workspace. Higher means more certain. | | token\_reward | integer | ⚪ No | nullable; integer | — | Gamification reward tokens awarded to the user upon completing this task. Sourced from the associated TaskTemplate or set by the pipeline. | | due\_date | datetime | ⚪ No | nullable; no DB-level CHECK | — | Optional deadline after which the task is considered overdue. Displayed in the task card and used for sorting. | | visible\_date | datetime | ⚪ No | nullable; no DB-level CHECK | — | Date before which the task should not be surfaced to the user (deferred visibility). Used by the scoring pipeline to schedule tasks for future monthly-close cycles. | | references | jsonb (array of TaskReference objects) | ✅ Yes | defaults to \[]; GIN index idx\_tasks\_references\_gin (jsonb\_path\_ops) for fast dedup lookups | Array of \{ type: invoice\|company\|person\|document\|transaction\|provider\|month, id: string, label: string } | Polymorphic reference list linking the task to one or more domain entities. Drives context in the chat card and CTA routing. The first provider-typed entry is denormalized into provider\_ref\_id; the first company-typed entry into company\_ref\_id. | | provider\_ref\_id | string | ⚪ No | nullable; varchar(36); denormalized from references\[0 where type='provider'].id; participates in partial unique index uniq\_tasks\_active\_provider\_company on (workspace\_pk, provider\_ref\_id, company\_ref\_id) WHERE deleted\_at IS NULL AND status IN ('open','ice\_log','blocked') AND both ref\_ids NOT NULL AND parent\_task\_pk IS NULL | — | Denormalized first provider reference id extracted from the references array. Populated at task creation by TaskService.createTask. Enables O(1) dedup guard preventing duplicate root scoring tasks for the same workspace x provider x company triplet. | | company\_ref\_id | string | ⚪ No | nullable; varchar(36); denormalized from references\[0 where type='company'].id; co-participant in partial unique index uniq\_tasks\_active\_provider\_company | — | Denormalized first company reference id extracted from the references array. Populated at task creation by TaskService.createTask alongside provider\_ref\_id. | | history | jsonb (array of TaskHistoryEntry objects) | ✅ Yes | defaults to \[] | Array of \{ action: created\|status\_changed\|assigned\|comment, at: ISO8601, by?: string, by\_type: human\|system, from?: string, to?: string, detail?: string } | Append-only audit log of state transitions and comments on this task. Each entry records who did what and when. | | plan\_meta | jsonb (discriminated union on 'kind') | ⚪ No | nullable. Partial unique index uq\_tasks\_subworkspace\_candidate\_dedup on (workspace\_pk, plan\_meta->>'dedup\_key') WHERE deleted\_at IS NULL AND plan\_meta->>'kind' = 'subworkspace-candidate' AND plan\_meta->>'dedup\_key' IS NOT NULL. | kind: plan \| step \| scoring \| mail-connect \| connect-extension \| connect-tools-parent \| subworkspace-candidate | Optional metadata blob whose shape is discriminated by the 'kind' field. plan = agentic plan phases. step = individual step within a plan. scoring = provider-scoring signals and CTA route. mail-connect = connect-email fallback gate. connect-extension = Chrome extension install sub-task. connect-tools-parent = lazy parent for provider-connection subtasks. subworkspace-candidate = WAS-projection, detection signals, and dedup\_key for child workspace discovery. | | score | number (decimal) | ⚪ No | nullable; columnType numeric(8,2) | — | Computed relevance or urgency score assigned by the scoring pipeline (0-1000+ scale). Used to order tasks in the UI surface. | | done\_at | datetime | ⚪ No | nullable; no DB trigger; set by service layer | — | Timestamp when the task transitioned to status=done. Set by the service layer on completion. | | conversation\_thread\_id | string, UUID | ⚪ No | nullable; UuidType; soft reference only (no FK constraint) | — | UUID of the chat conversation thread associated with this task. Allows the AI to link back to the thread context. No FK cascade — deleting the conversation does not affect the task. | | created\_at | datetime, system | ✅ Yes | set on INSERT via MikroORM onCreate lifecycle; not nullable | — | Timestamp when the task row was created. | | updated\_at | datetime, system | ⚪ No | set on INSERT (onCreate) and updated on every UPDATE (onUpdate) via MikroORM lifecycle | — | Timestamp of the last modification to this task row. | | deleted\_at | datetime, system | ⚪ No | nullable; soft-delete sentinel; all active queries filter deleted\_at IS NULL; both partial unique indexes include deleted\_at IS NULL in their WHERE clause so hard-deletes release dedup slots | — | Soft-delete timestamp. When set, the task is logically deleted and excluded from all active queries. | ### Relationships | Name | Type | Required | Description | | ------------ | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ✅ Yes | The workspace this task belongs to. deleteRule = 'cascade' — deleting the workspace hard-deletes all its tasks. Every query must scope to this relationship. Composite index idx\_tasks\_workspace\_status covers (workspace\_pk, status) for efficient status-filtered list queries. | | template | to-one (task\_template) | ⚪ No | Optional reference to the TaskTemplate that spawned or defines this task (title, markdown content, action type, token reward). deleteRule = 'set null' — deleting a template leaves tasks intact but unlinked. Template-less tasks are common for agent-generated or platform-seeded rows. | | parent\_task | to-one (task) | ⚪ No | Self-referential parent for hierarchical task trees. A root task has parent\_task = null. deleteRule = 'set null' — deleting a parent orphans its children rather than cascade-deleting them. The partial unique index uniq\_tasks\_active\_provider\_company only applies to root tasks (parent\_task\_pk IS NULL). | | subtasks | to-many (task) | — | Inverse of parent\_task. Collection of child tasks nested under this task. The composite composite\_subtasks\_list (display\_type: relation\_list, sort\_proxy: subtasks\_aggregate.min.created\_at) in composites.yml surfaces subtask task\_id, title, status, and due\_date as a relation\_list cell in the records table. | | assigned\_to | to-one (people) | ⚪ No | The Person to whom the task is assigned for action. deleteRule = 'set null'. The composite assigned\_to.composite\_avatar\_fullname (display\_type: people\_avatar\_name) in composites.yml surfaces person\_id, avatar URL, and full\_name as a composite cell in the records table. | | created\_by | to-one (people) | ⚪ No | The Person who created the task, when created by a human. Null for agent-created or system-seeded tasks. deleteRule = 'set null'. | ### System-computed * task\_id is generated by gen\_random\_uuid() at INSERT via MikroORM defaultRaw; unique constraint enforced at DB level. * created\_at is set by MikroORM onCreate lifecycle hook to new Date(); never subsequently updated. * updated\_at is set by MikroORM onCreate and refreshed on every UPDATE via onUpdate lifecycle hook. * deleted\_at is the soft-delete sentinel. All active queries must filter deleted\_at IS NULL. Both partial unique indexes (uniq\_tasks\_active\_provider\_company and uq\_tasks\_subworkspace\_candidate\_dedup) include deleted\_at IS NULL so hard-deleting a row releases its dedup slot. * status defaults to 'open' (TaskStatusEnum.OPEN) at entity construction; the native PG enum task\_status\_enum enforces allowed values at the DB level. * source defaults to 'user' (TaskSourceEnum.USER); overridden to 'agent' by the scoring and onboarding pipelines. * priority defaults to 'medium' (TaskPriorityEnum.MEDIUM); nullable so it can be intentionally unset. * references defaults to \[] (empty array). The first provider-typed entry is extracted to provider\_ref\_id and the first company-typed entry to company\_ref\_id by TaskService.createTask. These two denormalized columns must stay in sync with the references array via service-layer discipline (no DB trigger enforces this). * history defaults to \[] (empty array). Entries are appended by the service layer on every state transition (status\_changed, assigned, comment). No DB trigger; the service is the sole writer. * Partial unique index uniq\_tasks\_active\_provider\_company on (workspace\_pk, provider\_ref\_id, company\_ref\_id) WHERE deleted\_at IS NULL AND status IN ('open','ice\_log','blocked') AND provider\_ref\_id IS NOT NULL AND company\_ref\_id IS NOT NULL AND parent\_task\_pk IS NULL. Closes the concurrent-worker dedup race for root scoring tasks. Sub-tasks (parent\_task\_pk IS NOT NULL) are excluded by design. * Partial unique index uq\_tasks\_subworkspace\_candidate\_dedup on (workspace\_pk, (plan\_meta->>'dedup\_key')) WHERE deleted\_at IS NULL AND plan\_meta->>'kind' = 'subworkspace-candidate' AND plan\_meta->>'dedup\_key' IS NOT NULL. Prevents re-emission of duplicate subworkspace-candidate tasks for the same canonical identity. CANCELLED rows are still covered; only hard-deleted rows release the slot. * Composite index idx\_tasks\_workspace\_status on (workspace\_pk, status) for efficient status-filtered workspace list queries. * GIN index idx\_tasks\_references\_gin on references JSONB column (jsonb\_path\_ops) for fast reference-type filter and dedup lookups. * plan\_meta is a discriminated-union JSONB blob. The 'kind' discriminant determines the valid shape. The rendering layer narrows on plan\_meta.kind to select the correct chat card or CTA component. * done\_at is set by the service layer when transitioning status to 'done'; it is not a DB-generated column. * conversation\_thread\_id is a soft UUID reference to a ChatConversation; there is no FK constraint so deleting the conversation does not affect the task. ## Example ```json theme={null} { "data": { "type": "task", "id": "d4f1c7b2-3a9e-4f08-91c3-8e20b5f6a012", "attributes": { "title": "Connect Stripe to import missing invoices", "description": "We detected 14 open transactions from Stripe that have no matching invoice. Connect the Stripe provider to auto-import them.", "status": "open", "executor_type": "human", "source": "agent", "priority": "high", "confidence_score": "0.88", "token_reward": "50", "due_date": "2026-06-15T00:00:00.000Z", "visible_date": "2026-06-01T00:00:00.000Z", "score": "940.00", "done_at": null, "conversation_thread_id": null, "provider_ref_id": "a3f8c1d2-1111-4b0c-9abc-000000000001", "company_ref_id": "b7e2a5c4-2222-4c1d-8def-000000000002", "references": [ { "type": "provider", "id": "a3f8c1d2-1111-4b0c-9abc-000000000001", "label": "Stripe" }, { "type": "company", "id": "b7e2a5c4-2222-4c1d-8def-000000000002", "label": "Stripe Payments Europe Ltd" } ], "history": [ { "action": "created", "at": "2026-06-02T08:30:00.000Z", "by": null, "by_type": "system" }, { "action": "assigned", "at": "2026-06-02T09:00:00.000Z", "by": "usr_human_01", "by_type": "human", "to": "Alice Martin" } ], "plan_meta": { "kind": "scoring", "signals": { "open_transactions": 14, "total_amount_eur": 12800, "dominant_currency": "EUR" }, "route": { "action_type": "connect_provider" } }, "created_at": "2026-06-02T08:30:00.000Z", "updated_at": "2026-06-02T09:00:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "9f3a4b7c-0001-4e2a-b000-aabbccddeeff" } }, "template": { "data": null }, "parent_task": { "data": null }, "subtasks": { "data": [] }, "assigned_to": { "data": { "type": "people", "id": "c8d1f9e0-3333-4d2e-9e00-000000000003" } }, "created_by": { "data": null } } } } ``` Source: `apps/api/src/database/entities/Task.ts` · domain: workspace · tier: Activity # TempAccessToken Source: https://docs.wellapp.ai/object-reference/temp_access_tokens TempAccessToken is a short-lived, single-use JWT backing record used to authenticate guest OAuth connector install flows TempAccessToken is a short-lived, single-use JWT backing record used to authenticate guest OAuth connector install flows. It is created by `TempTokenService.generateTempToken()` when the API issues a signed 15-minute connector-access URL and is consumed (marked used) by `TempTokenService.validateAndConsume()` during the OAuth callback. The table acts as a replay-prevention store: a token is valid only while `expires_at > now()` and `used_at IS NULL`. It has no workspace foreign key, no soft-delete column, and no user-editable fields — it is entirely system-managed. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | TempAccessToken | | Resource type (JSON:API `type`) | `temp_access_token` | | Collection / records root | — (not a records root) | | REST base | `/v1/temp-access-tokens` | | Entity class | `TempAccessToken` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------ | ---------- | | List | `GET /v1/temp-access-tokens` | 🟡 Planned | | Retrieve | `GET /v1/temp-access-tokens/{id}` | 🟡 Planned | | Create | `POST /v1/temp-access-tokens` | 🟡 Planned | | Update | `PATCH /v1/temp-access-tokens/{id}` | 🟡 Planned | | Delete | `DELETE /v1/temp-access-tokens/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------- | --------------------- | -------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jti | uuid (PK) 🔒 system | ✅ Yes | PRIMARY KEY; set by service via crypto.randomUUID() — not a database-generated default | Any valid UUID v4 | JWT ID. Serves as both the database primary key and the `jti` claim embedded in the signed JWT. Used to look up and validate the token during verify and consume operations. | | temp\_access\_token\_id | uuid 🔒 system | ✅ Yes | DEFAULT gen\_random\_uuid(); NOT NULL; added in Migration20260128100000 | Any valid UUID v4 | Public-facing stable identifier for the token record. Returned to callers as the resource `id` in API responses. Generated at row-insert time by the database. | | expires\_at | timestamptz 🔒 system | ✅ Yes | NOT NULL; set to now() + 15 minutes at creation by TempTokenService | Future timestamp at creation; past timestamp marks expiry | Absolute expiry timestamp. The repository's findValidByJti query filters `expires_at > now()`. Tokens past this timestamp are treated as invalid regardless of `used_at`. | | used\_at | timestamptz 🔒 system | ⚪ No | NULLABLE; set once by TempAccessTokenRepository.markAsUsed() on first successful consumption | null (unused) or a single timestamptz value (consumed) | Consumption timestamp. NULL means the token has not yet been used. Once set, findValidByJti will no longer return this row, enforcing single-use semantics. | | created\_at | timestamptz 🔒 system | ✅ Yes | NOT NULL; set by @Property(\{ onCreate: () => new Date() }) in entity class | Timestamp at row creation | Creation timestamp, set by the MikroORM onCreate hook when the entity is first persisted. There is no updated\_at column on this entity. | ### System-computed * temp\_access\_token\_id — generated by the database via DEFAULT gen\_random\_uuid() at INSERT time (Migration20260128100000). * jti — set by the service layer using crypto.randomUUID() before persist; also embedded as the jti claim in the HS256-signed JWT returned to callers. * expires\_at — computed by TempTokenService as Date.now() + 15 minutes (TOKEN\_EXPIRY = 15 \* 60 \* 1000 ms) at generation time. * created\_at — set by MikroORM onCreate hook (no database DEFAULT; application-side timestamp). * used\_at — stamped by TempAccessTokenRepository.markAsUsed() exactly once upon successful validateAndConsume(). Never reset or cleared. * Single-use enforcement — findValidByJti combines three predicates: jti match + expires\_at > now() + used\_at IS NULL. All three must hold for the token to be considered valid. ## Example ```json theme={null} { "data": { "type": "temp_access_token", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "attributes": { "temp_access_token_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "expires_at": "2026-06-02T15:45:00.000Z", "used_at": null, "created_at": "2026-06-02T15:30:00.000Z" } } } ``` Source: `apps/api/src/database/entities/TempAccessToken.ts` · domain: platform · tier: Platform # TransactionDocument Source: https://docs.wellapp.ai/object-reference/transaction_documents TransactionDocument is a soft-deletable N:M join entity that links a `Transaction` to a `Document` (e.g TransactionDocument is a soft-deletable N:M join entity that links a `Transaction` to a `Document` (e.g. a bank-statement attachment or receipt). It lives in the `core_api` schema and mirrors the `CompanyMedia` / `PersonMedia` bridge-table shape. A partial unique index on `(transaction_pk) WHERE deleted_at IS NULL` enforces the current product invariant of one active attachment per transaction; a second transaction hard-delete CASCADE and a `RESTRICT` on the document side protect referential integrity. Workspace scope is inherited transitively through the `Transaction` relation — no direct workspace FK is declared on this table. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | TransactionDocument | | Resource type (JSON:API `type`) | `transaction_document` | | Collection / records root | — (not a records root) | | REST base | `/v1/transaction-documents` | | Entity class | `TransactionDocument` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------------- | ---------- | | List | `GET /v1/transaction-documents` | 🟡 Planned | | Retrieve | `GET /v1/transaction-documents/{id}` | 🟡 Planned | | Create | `POST /v1/transaction-documents` | 🟡 Planned | | Update | `PATCH /v1/transaction-documents/{id}` | 🟡 Planned | | Delete | `DELETE /v1/transaction-documents/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | created\_at | 🔒 system — timestamptz | ✅ Yes | Auto-set via `onCreate: () => new Date()`. Not nullable. | — | Timestamp at which this join row was created (i.e. the document was attached to the transaction). | | deleted\_at | timestamptz \| null | ⚪ No | Nullable. Soft-delete sentinel. Participates in partial unique index `uq_transaction_documents_one_active_per_transaction` — only one row with `deleted_at IS NULL` may exist per `transaction_pk` at any time. | — | When set, the document is considered detached from the transaction. All active-join queries filter `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | ----------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | transaction | to-one (ManyToOne) | ✅ Yes | The transaction this document is attached to. FK `transaction_pk` with `ON DELETE CASCADE` — deleting the parent transaction removes this join row. Composite index `idx_transaction_documents_transaction_deleted` covers `(transaction_pk, deleted_at)` for the common read path. | | document | to-one (ManyToOne) | ✅ Yes | The document being attached. FK `document_pk` with `ON DELETE RESTRICT` — hard-deleting a Document that has live join rows raises a FK violation, forcing callers to go through `DocumentService.softDelete`. Index `idx_transaction_documents_document` supports the reverse-traversal path ('which transactions reference this document'). | ### System-computed * pk — auto-increment serial primary key, internal join only; never exposed via public API. * created\_at — set once on insert via `onCreate: () => new Date()` MikroORM hook. * deleted\_at — set to current timestamp by the service layer on detach (soft-delete); never set by the user directly. * The partial unique index `uq_transaction_documents_one_active_per_transaction` on `(transaction_pk) WHERE deleted_at IS NULL` is enforced by Postgres at the DB layer — the service layer cooperates via a soft-delete-then-insert pattern on re-attach. * Workspace scope is inherited transitively through the Transaction relation; no explicit `workspace_pk` column exists on this table. ## Example ```json theme={null} { "data": { "type": "transaction_document", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "created_at": "2026-04-22T09:14:32.000Z", "deleted_at": null }, "relationships": { "transaction": { "data": { "type": "transaction", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } }, "document": { "data": { "type": "document", "id": "d290f1ee-6c54-4b01-90e6-d701748f0851" } } } } } ``` Source: `apps/api/src/database/entities/TransactionDocument.ts` · domain: financial-graph · tier: Supporting # TransactionWorkspaceConnector Source: https://docs.wellapp.ai/object-reference/transaction_workspace_connectors TransactionWorkspaceConnector is a per-row provenance junction that records which WorkspaceConnector sourced or received a given Transaction — analogous to Docu TransactionWorkspaceConnector is a per-row provenance junction that records which WorkspaceConnector sourced or received a given Transaction — analogous to DocumentWorkspaceConnector. Each row carries a direction discriminator (input = ingestion source, output = egress destination) so that a single transaction can be linked to multiple connectors in each direction without ambiguity. Tenant isolation is inherited transitively through the Transaction's workspace and the WorkspaceConnector's workspace rather than a direct workspace\_pk column. As the highest-volume of the five entity-connector junctions (projected \~436 k rows at 18 months), it ships non-partitioned with two covering indexes: record-led and connector-led for sync-status range queries. | Naming | Value | | ------------------------------- | -------------------------------------- | | Object | TransactionWorkspaceConnector | | Resource type (JSON:API `type`) | `transaction_workspace_connector` | | Collection / records root | — (not a records root) | | REST base | `/v1/transaction-workspace-connectors` | | Entity class | `TransactionWorkspaceConnector` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------------------- | ---------- | | List | `GET /v1/transaction-workspace-connectors` | 🟡 Planned | | Retrieve | `GET /v1/transaction-workspace-connectors/{id}` | 🟡 Planned | | Create | `POST /v1/transaction-workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/transaction-workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/transaction-workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ------------------------------------------------------- | -------- | --------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | direction | enum (direction\_enum — stored as native Postgres enum) | ✅ Yes | NOT NULL; CHECK enforced by the native Postgres enum type | "input" \| "output" | Indicates whether the associated WorkspaceConnector is the ingestion source (input) or an egress destination (output) for the Transaction. Stored as the native Postgres enum `core_api.direction_enum`; the column value equals the enum VALUE string. | | created\_at | 🔒 system — datetime | ✅ Yes | NOT NULL; default now(); timestamptz | — | Row-creation timestamp set automatically on insert via MikroORM onCreate hook. Never writable by the user. | | updated\_at | 🔒 system — datetime | ⚪ No | nullable; timestamptz | — | Last-modified timestamp maintained automatically by MikroORM onCreate + onUpdate hooks. Null until the row is first updated after creation. | | deleted\_at | 🔒 system — datetime | ⚪ No | nullable; timestamptz | — | Soft-delete timestamp. Null means the row is active. Set by the application when the association is logically removed; never hard-deleted. All queries must filter `deleted_at IS NULL`. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | transaction | to-one (ManyToOne) | ✅ Yes | The Transaction this provenance row belongs to. Foreign key `transaction_pk` references `core_api.transactions.pk` ON UPDATE CASCADE. Tenant scope is inherited through this relationship — there is no direct workspace\_pk on the junction. | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The WorkspaceConnector instance (an activated connector in a specific workspace) that ingested or will receive this transaction. Foreign key `workspace_connector_pk` references `core_api.workspace_connectors.pk` ON UPDATE CASCADE. | ### System-computed * pk — serial integer primary key, internal only, never exposed on the public API * created\_at — set to new Date() on insert via MikroORM onCreate hook; never user-writable * updated\_at — set to new Date() on insert and on every update via MikroORM onCreate + onUpdate hooks; null until first update * deleted\_at — null on creation; written by application code for soft-delete; never hard-deleted * No transaction\_workspace\_connector\_id UUID field is present on this entity — it uses only the internal serial pk (junction tables in this codebase omit the public \*\_id UUID unless externally referenced) * Rows are written exclusively by the connector sync pipeline (input direction) or by egress sync writers (output direction, iteration 4+); no user-facing PATCH route exists * Migration20260507000000 backfilled historical input rows from legacy entity.source\_workspace\_connector\_pk values using an idempotent INSERT…SELECT NOT EXISTS guard; idempotency is NOT enforced by a UNIQUE constraint — no UNIQUE on (transaction\_pk, workspace\_connector\_pk, direction) was shipped by design (deduplication is iteration 3+ work) * Two covering B-tree indexes ship with the table: idx\_transaction\_workspace\_connectors\_record\_created on (transaction\_pk, created\_at) for record-led traversals; idx\_transaction\_workspace\_connectors\_wc\_created\_at on (workspace\_connector\_pk, created\_at) for sync-status range queries ## Example ```json theme={null} { "data": { "type": "transaction_workspace_connector", "id": "a3f92c01-4e5b-47dc-9a13-001b8e2d7f44", "attributes": { "direction": "input", "created_at": "2026-05-07T14:23:11.000Z", "updated_at": "2026-05-07T14:23:11.000Z", "deleted_at": null }, "relationships": { "transaction": { "data": { "type": "transaction", "id": "f1d3c8a2-0011-4bcd-8f2e-ab91c42e7001" } }, "workspace_connector": { "data": { "type": "workspace_connector", "id": "9b2a5d3e-ccf1-4a89-b027-0f3d1e7c8a55" } } } } } ``` Source: `apps/api/src/database/entities/TransactionWorkspaceConnector.ts` · domain: ingestion · tier: Infrastructure # Transaction Source: https://docs.wellapp.ai/object-reference/transactions A Transaction represents a single monetary movement recorded against a bank account — a debit or credit of funds initiated by a connector sync (e.g A Transaction represents a single monetary movement recorded against a bank account — a debit or credit of funds initiated by a connector sync (e.g. Plaid, Qonto/PSD2) or created manually. Every transaction belongs to a Workspace and is anchored to an AccountBalance; the two legs of the movement are modelled as a debtor PaymentMeans (source of funds) and a creditor PaymentMeans (destination). Transactions are the primary evidence surface for counterparty-bank discovery, AI categorisation, invoice reconciliation, and the accounting journal-entry pipeline. | Naming | Value | | ------------------------------- | ------------------ | | Object | Transaction | | Resource type (JSON:API `type`) | `transaction` | | Collection / records root | `transactions` | | REST base | `/v1/transactions` | | Entity class | `Transaction` | ## API operations | Operation | Method & path | Status | | --------- | ------------------------------ | ------------- | | List | `GET /v1/transactions` | ✅ Implemented | | Retrieve | `GET /v1/transactions/{id}` | ✅ Implemented | | Create | `POST /v1/transactions` | 🟡 Planned | | Update | `PATCH /v1/transactions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/transactions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | transaction\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen\_random\_uuid() | — | Public immutable identifier for the transaction. Exposed on all API responses; used for deduplication and external references. | | type | string (text, CHECK constraint) | ⚪ No | nullable; TEXT column with CHECK IN (...) — not a native PG enum. MikroORM @Enum without nativeEnumName stores the enum VALUE string. | General payments to vendors or suppliers, Transfers between accounts, Incoming funds or deposits, Cash withdrawals or outgoing funds, Credit/debit card transactions, Automated recurring payments, Refunds or previous paid funds, Services fees and charges, Interest earned or charged, Miscellaneous or unclassified transaction | High-level classification of the transaction's economic nature. The stored DB string is the full description value from TransactionTypeEnum (e.g. 'General payments to vendors or suppliers' for PAYMENT). Written by connector sync at ingest time. | | status | string (text, CHECK constraint) | ⚪ No | nullable; TEXT column with CHECK IN (...) — not a native PG enum. MikroORM @Enum without nativeEnumName stores the enum VALUE string. | Initiated, awaiting processing, Processing in progress, Authorized but not yet settled, Successfully completed and settled, Failed due to technical errors, Rejected by the recipient or system, Cancelled by the initiator or system, Reversed or rolled back, Held for review, Expired without completion | Lifecycle stage of the transaction. The stored DB string is the full description value from TransactionStatusEnum (e.g. 'Successfully completed and settled' for COMPLETED). Terminal success: 'Successfully completed and settled'. Terminal failures: Failed/Rejected/Cancelled/Reversed descriptions. Transient: Initiated/Processing/Authorized descriptions. | | transaction\_external\_id | string | ⚪ No | nullable; length ≤ 255; indexed (idx\_transactions\_external\_id) for connector sync batch dedup | — | Provider-assigned identifier (e.g. Plaid transaction\_id, Qonto entry id). Used by connector sync to resolve existing rows via IN (...) dedup query before creating new ones. | | requested\_execution\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE (no time component) | — | The date the initiator requested the transaction to be executed. May differ from executed\_at when the bank settles on a different day than the request. | | executed\_at | timestamp | ✅ Yes | NOT NULL; indexed (idx\_transactions\_executed\_status, idx\_transactions\_workspace\_executed\_active) | — | Timestamp when the transaction was executed by the banking system. Primary time axis for canvas burn-window reads and temporal ordering. Indexed with workspace\_pk (partial WHERE deleted\_at IS NULL) for anchor-most-recent and range-window query patterns. | | booking\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | Date the transaction was booked in the account ledger at the bank. Used in PSD2/Open Banking flows; may lag executed\_at by one banking day. | | value\_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | Date on which the funds become available (interest calculation date). Used in counterparty-bank discovery recency decay and micro-deposit fingerprint detection (Q12 window: value\_date BETWEEN t1.value\_date AND t1.value\_date + INTERVAL '14 days'). | | instructed\_amount | object (JSONB) — \{ amount: number; currency: CurrencyCodeEnum } | ✅ Yes | NOT NULL; JSONB | — | The amount and currency as instructed by the initiator. Negative values represent debits from the workspace perspective; positive represent credits. currency is an ISO-4217 code. This is the canonical amount column used for ABS() aggregation in counterparty-bank scoring (Q8 threshold, Q12 micro\_amount). | | settlement\_amount | object (JSONB) — \{ amount: number; currency: CurrencyCodeEnum } | ⚪ No | nullable; JSONB | — | The amount actually settled, which may differ from instructed\_amount when FX conversion occurs. Present for cross-currency transactions where the instructed currency differs from the account's base currency. | | foreign\_exchange | object (JSONB) — \{ rate: number; pair: string; source: CurrencyRateSourceEnum; at: Date } | ⚪ No | nullable; JSONB | source: ECB, FED, IMF, XE, OANDA, BANK, EXCHANGE\_RATE\_API, MANUAL, OTHER | FX rate data applied to convert instructed\_amount into settlement\_amount. pair is an ISO currency pair string (e.g. EUR/USD). at is the timestamp of the rate snapshot. source identifies the rate provider. | | category\_purpose | string | ⚪ No | nullable; length ≤ 10 | — | ISO 20022 category purpose code identifying the high-level purpose of the credit transfer (e.g. GDDS = goods and services, SUPP = supplier payment, SALA = salary). At most 10 characters per the ISO standard. | | purpose\_code | string | ⚪ No | nullable; length ≤ 10 | — | ISO 20022 purpose code providing additional detail beyond category\_purpose (e.g. SUPP = supplier payment, SALA = salary, RENT = rental payment). At most 10 characters. | | category\_normalized | string (text) | ⚪ No | nullable; DB CHECK length ∈ \[1, 200] (transactions\_category\_normalized\_length); DB CHECK category\_source = 'classifier' ⟺ category\_confidence IS NOT NULL (transactions\_category\_source\_confidence\_invariant); DB CHECK category\_normalized IS NULL OR category\_source IS NOT NULL (transactions\_category\_normalized\_provenance) | — | Human-readable normalized spending category (e.g. 'Office Supplies', 'Travel & Accommodation'). Written by the W19 AI classifier (source = classifier), by explicit human override via POST /v1/workspaces/:wsId/transactions/:tId/category (source = user), by connector import (source = connector), or by a deterministic FieldRule (source = rule). The service layer enforces source = user on override writes, preventing provenance forgery. | | category\_confidence | string (decimal 4,3) | ⚪ No | nullable; DB CHECK ∈ \[0, 1] (transactions\_category\_confidence\_range); must be non-null iff category\_source = 'classifier'; forced to NULL on user override writes | — | Classifier confidence score in \[0.000, 1.000] for the assigned category\_normalized value. Present only when category\_source = classifier; NULL for user, connector, and rule sources. Stored as decimal(4,3) to avoid float precision drift. | | category\_source | string (enum CategorySourceEnum) | ⚪ No | nullable; native PG enum category\_source\_enum; invariant: classifier ⟺ category\_confidence IS NOT NULL | classifier, user, connector, rule | Provenance of the category\_normalized value. classifier = W19 AI model (carries confidence). user = explicit human override. connector = value imported verbatim from connector mapping (e.g. Plaid personal\_finance\_category). rule = deterministic FieldRule (no LLM, no confidence). | | remittance | object (JSONB) — \{ unstructured?: string; structured\_reference?: string; reference\_type?: RemittanceReferenceTypeEnum } | ⚪ No | nullable; JSONB; the unstructured field is the primary bank-discovery surface for PSD2/Qonto flows | reference\_type: SCOR, QRR, ISR, IREF, EREF, PREF, MREF, CRED, USTD, NON | Payment reference/remittance information. unstructured holds the free-text memo (primary BIC, IBAN, and bank-name extraction surface for PSD2 sources). structured\_reference is a ISO 11649 creditor reference or similar. reference\_type classifies the structured reference scheme. Accessed as remittance->>'unstructured' in SQL. | | fees | array (JSONB) — Array\<\{ type: TransactionFeeTypeEnum; amount: number; currency: CurrencyCodeEnum }> | ⚪ No | nullable; JSONB array | type stored strings: Standard Transfer fee, Wire Transfer or inter-bank transfer fee, Foreign Exchange conversion fee, ATM withdrawal or usage fee, Overdraft or insufficient funds fee, Monthly account maintenance fee, Card insurance, renewal or annual fee, Commission or percentage base fee, Late payment or violation penality, Miscellaneous or unclassified fee | Breakdown of fees associated with this transaction. Each entry carries the fee type (stored as the TransactionFeeTypeEnum VALUE string), absolute amount, and currency. Multiple fee entries are possible (e.g. a wire transfer may carry both a transfer and a currency conversion fee). Note: 'penality' is spelled as in the code/enum. | | scheme | string (enum TransactionSchemeEnum) | ⚪ No | nullable; native PG enum transaction\_scheme\_enum | SEPA, SWIFT, ACH, FASTER\_PAYMENTS, BACS, WIRE, OTHER | Payment rail / clearing scheme used to execute the transaction. SEPA = Eurozone credit transfer or direct debit. SWIFT = international correspondent banking. ACH = US domestic network (relevant for micro-deposit fingerprint Q12). FASTER\_PAYMENTS = UK instant. BACS = UK direct debit. WIRE = generic bank wire. This column uses a native PG enum (nativeEnumName: transaction\_scheme\_enum) so the stored values are the enum keys. | | raw\_data | object (JSONB) — Record\ | ⚪ No | nullable; JSONB; no GIN index — queried via full-document ILIKE/::text cast in counterparty-bank discovery queries Q7/Q10/Q11 | — | Connector-native payload preserved verbatim. Shape varies per connector. For Plaid/Mercury: includes counterparties\[] (name, type, confidence\_level, website, logo\_url, entity\_id), merchant\_name, merchant\_entity\_id, personal\_finance\_category, payment\_meta (ppd\_id, by\_order\_of), payment\_channel, transaction\_code. For PSD2/Qonto: minimal; bank identity surfaces in remittance.unstructured instead. For GoCardless/Tink: raw\_data->'institution'->>'name'. Always branch on originating connector before parsing. | | created\_at | timestamp, 🔒 system | ✅ Yes | NOT NULL; set by onCreate lifecycle hook | — | Timestamp when the transaction row was first persisted in Well. Distinct from executed\_at (bank execution time). Set automatically by MikroORM onCreate; never writable by the API. | | updated\_at | timestamp, 🔒 system | ⚪ No | nullable; set by onCreate and onUpdate lifecycle hooks | — | Timestamp of the most recent update to this row (category assignment, enrichment, soft-delete). Set automatically by MikroORM on every flush. | | deleted\_at | timestamp | ⚪ No | nullable; soft-delete sentinel; all active queries must filter deleted\_at IS NULL; partial indexes use WHERE deleted\_at IS NULL | — | Soft-delete timestamp. NULL means the record is active. Set to current timestamp on deletion; never physically removed. Partial indexes (idx\_transactions\_account\_balance\_active, idx\_transactions\_workspace\_executed\_active, idx\_transactions\_classifier\_confidence) exclude deleted rows to avoid index bloat. | ### Relationships | Name | Type | Required | Description | | ------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (Workspace) | ⚪ No (nullable FK) | The tenant workspace that owns this transaction. All active-record queries filter by workspace\_pk. Indexed via idx\_transactions\_workspace\_deleted (composite workspace\_pk + deleted\_at) for Hasura RLS permission filter. | | debtor\_payment\_means | to-one (PaymentMeans) | ⚪ No (nullable FK, fieldName: debtor\_payment\_means\_pk) | The payment instrument on the source-of-funds (debit) side of the transaction. When company\_pk on this PaymentMeans equals the workspace's own\_company\_pk the transaction is an outbound payment from the workspace. Used in counterparty-bank direction-of-ownership analysis. | | creditor\_payment\_means | to-one (PaymentMeans) | ⚪ No (nullable FK, fieldName: creditor\_payment\_means\_pk) | The payment instrument on the destination-of-funds (credit) side of the transaction. When company\_pk on this PaymentMeans equals own\_company\_pk the transaction is an inbound receipt to the workspace. | | account\_balance | to-one (AccountBalance) | ⚪ No (nullable FK, fieldName: account\_balance\_pk) | The account balance snapshot associated with this transaction. Provides the link to the parent Account (accounts.pk via account\_balances.account\_pk). Used in cash-flow canvas burn-window reads and by the soft-delete partial index idx\_transactions\_account\_balance\_active. | | sourceWorkspaceConnector | to-one (WorkspaceConnector) | ⚪ No (nullable FK, entity property name: sourceWorkspaceConnector — camelCase) | The connector sync instance that created this transaction row. NULL means the transaction was created via a non-connector path (manual entry, invoice import). Non-NULL identifies the ingestion provenance (Plaid, Qonto, GoCardless, etc.) and enables connector-source branching in raw\_data parsing. | | ledger\_account | to-one (LedgerAccount) | ⚪ No (nullable FK) | The accounting ledger account to which this transaction is classified (e.g. chart-of-accounts code 512 – Bank). Set during the accounting journal-entry pipeline. NULL until the transaction is posted. | | transaction\_documents | to-many (TransactionDocument) | — | Documents attached to this transaction (receipts, proofs of payment, bank statements). Managed via TransactionDocument pivot; each document carries a reference back to this transaction. | | transactionWorkspaceConnectors | to-many (TransactionWorkspaceConnector) | — | Multi-connector provenance pivot linking this transaction to one or more WorkspaceConnector instances that have touched it (e.g. a transaction first ingested by Plaid, later enriched by a second connector). Entity property name is camelCase `transactionWorkspaceConnectors` in the MikroORM entity. | ### System-computed * transaction\_id: generated via gen\_random\_uuid() PostgreSQL function at INSERT time; unique constraint enforced at DB level. * created\_at: set by MikroORM onCreate lifecycle hook (new Date()); never writable via API. * updated\_at: set by both onCreate and onUpdate lifecycle hooks; reflects latest flush timestamp. * deleted\_at: soft-delete sentinel; NULL on active records. Set by service layer on deletion; never physically removed. All active queries must carry a deleted\_at IS NULL predicate. * category\_source invariant: when category\_source is set to 'user' the service layer forces category\_confidence to NULL, regardless of payload content, to prevent provenance forgery via Hasura or direct API calls. * category\_confidence invariant: DB CHECK (transactions\_category\_confidence\_range) enforces category\_confidence ∈ \[0, 1]; DB CHECK (transactions\_category\_source\_confidence\_invariant) enforces that category\_confidence IS NOT NULL if and only if category\_source = 'classifier'. * category\_normalized invariant: DB CHECK (transactions\_category\_normalized\_length) enforces length(category\_normalized) ∈ \[1, 200] when non-null; DB CHECK (transactions\_category\_normalized\_provenance) enforces category\_normalized IS NULL OR category\_source IS NOT NULL — a non-null label always requires a non-null source. * transaction\_external\_id dedup: connector sync resolves existing rows via transaction\_external\_id IN (...) batch lookup (idx\_transactions\_external\_id index) before creating new ones. * sourceWorkspaceConnector provenance: non-null value records which WorkspaceConnector created this row; NULL indicates manual/non-connector origin. The raw\_data parsing strategy must branch on the originating connector before reading connector-specific fields. * instructed\_amount is the canonical amount column: used for ABS() aggregation in counterparty-bank scoring queries, micro-deposit fingerprint detection, and large-transaction detection (Q8/Q12). Settlement amount is the FX-converted equivalent and is always in a different or equal currency. * Partial indexes: idx\_transactions\_account\_balance\_active (WHERE deleted\_at IS NULL AND account\_balance\_pk IS NOT NULL); idx\_transactions\_workspace\_executed\_active (WHERE deleted\_at IS NULL); idx\_transactions\_classifier\_confidence (WHERE category\_source = 'classifier' AND deleted\_at IS NULL, on workspace\_pk + category\_confidence — serves the classifier review queue). All exclude deleted rows from hot-path index scans. * type and status enum storage: these columns are TEXT with CHECK IN (...) constraints (not native PG enums). MikroORM @Enum without nativeEnumName stores the enum VALUE string (long description), not the enum key. When filtering in SQL, use the description string, e.g. WHERE type = 'General payments to vendors or suppliers', not WHERE type = 'PAYMENT'. ## Example ```json theme={null} { "data": { "type": "transaction", "id": "d3f4a8b2-1c9e-4d7f-b6a0-2e5c8f901234", "attributes": { "transaction_id": "d3f4a8b2-1c9e-4d7f-b6a0-2e5c8f901234", "type": "General payments to vendors or suppliers", "status": "Successfully completed and settled", "transaction_external_id": "txn_1OqwXY2eZvKYlo2CABCdef12", "requested_execution_date": "2026-05-14", "executed_at": "2026-05-14T09:32:00.000Z", "booking_date": "2026-05-14", "value_date": "2026-05-15", "instructed_amount": { "amount": -1250.00, "currency": "EUR" }, "settlement_amount": { "amount": -1250.00, "currency": "EUR" }, "foreign_exchange": null, "category_purpose": "GDDS", "purpose_code": "SUPP", "category_normalized": "Office Supplies", "category_confidence": "0.941", "category_source": "classifier", "remittance": { "unstructured": "INV-2026-0423 – Acme Office Supplies SAS", "structured_reference": "RF18539007547034", "reference_type": "SCOR" }, "fees": [ { "type": "Standard Transfer fee", "amount": 0.50, "currency": "EUR" } ], "scheme": "SEPA", "raw_data": { "merchant_name": "Acme Office Supplies", "merchant_entity_id": "plaid_entity_abc123", "counterparties": [ { "name": "Acme Office Supplies SAS", "type": "merchant", "confidence_level": "VERY_HIGH" } ], "personal_finance_category": { "primary": "GENERAL_MERCHANDISE", "detailed": "GENERAL_MERCHANDISE_OFFICE_SUPPLIES" } }, "created_at": "2026-05-14T09:32:05.123Z", "updated_at": "2026-05-14T09:35:11.456Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-0000-0000-0000-ffffffffffff" } }, "debtor_payment_means": { "data": { "type": "payment_means", "id": "pm-0001-0000-0000-000000000001" } }, "creditor_payment_means": { "data": { "type": "payment_means", "id": "pm-0002-0000-0000-000000000002" } }, "account_balance": { "data": { "type": "account_balance", "id": "ab-0001-0000-0000-000000000001" } }, "source_workspace_connector": { "data": { "type": "workspace_connector", "id": "wc-0001-0000-0000-000000000001" } }, "ledger_account": { "data": null }, "transaction_documents": { "data": [] }, "transaction_workspace_connectors": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/Transaction.ts` · domain: financial-graph · tier: Main # Web Link Source: https://docs.wellapp.ai/object-reference/web_links A WebLink (table `core_api.web_links`) is an atomic contact-channel record that stores a URL together with its social/web platform classification A WebLink (table `core_api.web_links`) is an atomic contact-channel record that stores a URL together with its social/web platform classification. WebLinks are shared, reusable objects attached to both Companies and People through the `company_web_links` and `person_web_links` bridge tables, following the same pivot-entity pattern as emails and phones. The entity holds a nullable `workspace` FK added in Migration20260119180000 to scope enrichment-created links. It is a Supporting root in the records layer, surfaced in composite `composite_web_links_list` columns on company and person pages. | Naming | Value | | ------------------------------- | --------------- | | Object | Web Link | | Resource type (JSON:API `type`) | `web_link` | | Collection / records root | `web_links` | | REST base | `/v1/web-links` | | Entity class | `WebLink` | ## API operations | Operation | Method & path | Status | | --------------- | ------------------------------------------ | ------------- | | List | `GET /v1/web-links` | ✅ Implemented | | List (nested) | `GET /v1/people/{id}/web-links` | ✅ Implemented | | Retrieve | `GET /v1/web-links/{id}` | ✅ Implemented | | Create (nested) | `POST /v1/people/{id}/web-links` | ✅ Implemented | | Update | `PATCH /v1/web-links/{id}` | 🟡 Planned | | Delete (nested) | `DELETE /v1/people/{id}/web-links/{subId}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ---------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | social\_link\_id | string, UUID, 🔒 system | ✅ Yes | unique; default gen\_random\_uuid() | — | Public stable identifier for the web link, safe to expose in API responses. Generated by the database on insert. | | platform | string (PlatformEnum) | ✅ Yes | native PostgreSQL enum core\_api.platform\_enum; original values twitter/linkedin/github created in Migration20250801141316; instagram/facebook/website/other added via ALTER TYPE ... ADD VALUE IF NOT EXISTS in Migration20260119180000 | twitter, linkedin, github, instagram, facebook, website, other | The social network or web platform the URL belongs to. Drives icon and renderer selection in the composite\_web\_links\_list cell. | | url | string | ✅ Yes | varchar(255); NOT NULL; no uniqueness constraint (same URL may appear on multiple entities) | — | The fully-qualified URL of the web presence (profile page, website, repository, etc.). | | created\_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | timestamptz; set by onCreate lifecycle hook; never null | — | Timestamp when the web link record was first persisted. | | updated\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | timestamptz; nullable; set by onCreate and onUpdate lifecycle hooks | — | Timestamp of the last update. Null if the record has never been modified after creation. | | deleted\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | timestamptz; nullable | — | Soft-delete timestamp. When set, the web link is excluded from all active queries. Hard deletes do not occur on this entity. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (workspace) | ⚪ No | The workspace that owns this web link. Added in Migration20260119180000 to scope enrichment-created links; nullable because web links pre-dating that migration have no workspace assignment. ON DELETE SET NULL. | ### System-computed * social\_link\_id is generated by gen\_random\_uuid() at the database level on INSERT; the application never supplies it. * created\_at is set by the MikroORM onCreate lifecycle hook (new Date()); never written by application code. * updated\_at is set by both onCreate and onUpdate lifecycle hooks; it may be null on records that have never been modified. * deleted\_at is set to a non-null timestamp on soft delete; active queries must always filter deleted\_at: null. No hard-delete path exists on this entity. * workspace\_pk (FK) was added as nullable in Migration20260119180000; records created before that migration have workspace = null. Enrichment pipelines may create web links with a workspace scope; connector-synced links may have workspace = null. * PlatformEnum values instagram, facebook, website, and other were added to the native PostgreSQL enum in Migration20260119180000 via ALTER TYPE ... ADD VALUE IF NOT EXISTS. The original enum (twitter, linkedin, github) was created in Migration20250801141316. * The web\_links table was created in Migration20250919154301, migrated from a legacy social\_links table. The social\_link\_id public UUID field preserves backward-compatible identity across that rename. ## Example ```json theme={null} { "data": { "type": "web_link", "id": "a3f7c812-09be-4d2a-b5e1-dc4a78f30c92", "attributes": { "social_link_id": "a3f7c812-09be-4d2a-b5e1-dc4a78f30c92", "platform": "linkedin", "url": "https://www.linkedin.com/in/marie-leblanc-cfo", "created_at": "2025-11-14T09:22:05.000Z", "updated_at": "2025-11-14T09:22:05.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890" } } } } } ``` Source: `apps/api/src/database/entities/WebLink.ts` · domain: financial-graph · tier: Supporting # Webhook Source: https://docs.wellapp.ai/object-reference/webhooks A `Webhook` represents an outbound HTTP subscription registered by a workspace A `Webhook` represents an outbound HTTP subscription registered by a workspace. When a watched event fires (e.g. `document.processed`, `document.uploaded`), the platform enqueues a Cloud Tasks HTTP POST to `target_url` carrying the event payload and any caller-supplied custom `headers`. One workspace may register multiple webhooks; parent-workspace webhooks also fire for child workspaces. Webhooks are soft-deleted and workspace-scoped. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | Webhook | | Resource type (JSON:API `type`) | `webhook` | | Collection / records root | — (not a records root) | | REST base | `/v1/subscriptions/webhooks` | | Entity class | `Webhook` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------------- | ---------- | | List | `GET /v1/subscriptions/webhooks` | 🟡 Planned | | Retrieve | `GET /v1/subscriptions/webhooks/{id}` | 🟡 Planned | | Create | `POST /v1/subscriptions/webhooks` | 🟡 Planned | | Update | `PATCH /v1/subscriptions/webhooks/{id}` | 🟡 Planned | | Delete | `DELETE /v1/subscriptions/webhooks/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | -------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | webhook\_id | string (UUID) 🔒 system | ✅ Yes | UNIQUE; default gen\_random\_uuid() | — | Public stable identifier for the webhook. Generated at creation; never changes. | | target\_url | string | ✅ Yes | varchar(255); must be a valid URL (Zod .url() at API boundary) | — | The HTTPS endpoint to which the platform delivers event payloads via Cloud Tasks HTTP POST. | | event | string (enum) | ✅ Yes | varchar(255); allowed values enforced by Zod schema | document.processed \| document.uploaded | The workspace event type that triggers delivery. Only one event type per webhook registration. | | headers | object (JSONB) \| null | ⚪ No | jsonb; nullable; key-value map of strings | — | Optional custom HTTP headers merged into the delivery request (e.g. Authorization, X-Source). Sent as-is by the platform. | | active | boolean 🔒 system | ✅ Yes | default true | — | Whether the webhook is currently enabled. Defaults to true on creation. Not exposed in the update schema — cannot be toggled via the API. | | created\_at | datetime 🔒 system | ✅ Yes | timestamptz; set by onCreate hook | — | ISO 8601 timestamp of when the webhook was created. | | updated\_at | datetime 🔒 system | ⚪ No | timestamptz; nullable; set by onCreate and onUpdate hooks | — | ISO 8601 timestamp of the last update. Null until first modification. | | deleted\_at | datetime 🔒 system | ⚪ No | timestamptz; nullable | — | Soft-delete timestamp. When set, the webhook is excluded from all queries and no longer delivers events. Set by the DELETE endpoint. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace that owns this webhook. All queries filter by workspace\_pk. Parent-workspace webhooks also fire for child workspaces during delivery. References Workspace. | ### System-computed * webhook\_id — gen\_random\_uuid() at insertion; also written by WebhookService.createWebhook via crypto.randomUUID() (redundant but consistent) * created\_at — set by onCreate hook (new Date()) * updated\_at — set by onCreate and onUpdate hooks; null until first update * deleted\_at — set to new Date() by WebhookService.deleteWebhook on soft-delete; not a MikroORM hook, explicit assignment * active — defaults to true at ORM level; no toggle exposed via API * Deduplication guard — WebhookService.createWebhook checks for an existing non-deleted webhook with the same (target\_url, event, workspace) triple before inserting; raises WEBHOOK\_ALREADY\_EXISTS on collision * Delivery via Cloud Tasks — sendWebhookRequest dispatches through CloudTasksService.createHttpPostTask with taskId webhook-trigger---; the actual HTTP call is async and not reflected back on the entity * idx\_webhooks\_workspace\_deleted — composite partial index on (workspace\_pk, deleted\_at) added by Migration20260416000000 for hot-path list queries ## Example ```json theme={null} { "data": { "type": "webhook", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "event": "document.processed", "target_url": "https://api.example.com/well-events", "headers": { "Authorization": "Bearer secret-token", "X-Source": "well" }, "active": true, "created_at": "2026-03-15T10:30:00.000Z", "updated_at": "2026-04-01T08:00:00.000Z" } } } ``` Source: `apps/api/src/database/entities/Webhook.ts` · domain: platform · tier: Platform # WorkspaceAccountingSettings Source: https://docs.wellapp.ai/object-reference/workspace_accounting_settings WorkspaceAccountingSettings stores the per-workspace accounting configuration that governs how Well processes invoices, journal entries, and reporting for a giv WorkspaceAccountingSettings stores the per-workspace accounting configuration that governs how Well processes invoices, journal entries, and reporting for a given tenant. Each workspace has at most one settings row (enforced by a UNIQUE constraint on `workspace_pk`), making this effectively a 1-to-1 extension of the Workspace entity. The row is created by the onboarding/setup flow (seed or `WorkspaceService`) and subsequently updated either by the user via `PUT /v1/workspaces/:id/accounting-settings` or automatically by `WorkspaceSelfIdentityService` when invoice-extraction consensus is reached. It anchors the workspace's fiscal identity (base currency, country, accounting framework, business registration details) and configures two LedgerAccount defaults used by the journal-entry posting engine. | Naming | Value | | ------------------------------- | ----------------------------------- | | Object | WorkspaceAccountingSettings | | Resource type (JSON:API `type`) | `workspace_accounting_settings` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-accounting-settings` | | Entity class | `WorkspaceAccountingSettings` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------------------- | ---------- | | List | `GET /v1/workspace-accounting-settings` | 🟡 Planned | | Retrieve | `GET /v1/workspace-accounting-settings/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-accounting-settings` | 🟡 Planned | | Update | `PATCH /v1/workspace-accounting-settings/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-accounting-settings/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_accounting\_settings\_id | string (UUID) | ✅ Yes | unique; default gen\_random\_uuid() | any valid UUID v4 | Public API identifier for this settings row. | | base\_currency | string (CurrencyCodeEnum) \| null | ⚪ No | nativeEnumName: currency\_code\_enum; nullable | ISO 4217 codes, e.g. EUR, USD, GBP (full set in CurrencyCodeEnum from @wellapp/shared) | The workspace's base accounting currency. Drives exchange-rate selection and multi-currency invoice normalization. | | country | string (CountryCodeEnum) \| null | ⚪ No | nativeEnumName: country\_code\_enum; nullable | ISO 3166-1 alpha-2 codes, e.g. FR, DE, US (full set in CountryCodeEnum) | Jurisdiction of the workspace's registered entity. Used for tax defaults and accounting-framework recommendations. | | accounting\_framework | string (AccountingFrameworkEnum) \| null | ⚪ No | nativeEnumName: accounting\_framework\_enum; nullable. Note: PostgreSQL enum also retains the deprecated value 'MAR' (cannot be dropped); any existing 'MAR' rows were migrated to 'IFRS'. | PCG \| IFRS \| US\_GAAP \| SKR | Chart-of-accounts framework in use. Determines which ledger account plan is available and how journal-entry drafts are structured. | | fiscal\_year\_start\_month | integer \| null | ⚪ No | nullable; CHECK (fiscal\_year\_start\_month >= 1 AND fiscal\_year\_start\_month \<= 12) when not null | 1–12 | Month number (1 = January) on which the workspace's fiscal year begins. Null means not yet configured. | | tax\_id\_value | string \| null | ⚪ No | length ≤ 50; nullable | — | Workspace's tax registration number (e.g. VAT number, SIRET/SIREN, EIN). Extracted from invoices by WorkspaceSelfIdentityService via consensus. | | tax\_id\_type | string \| null | ⚪ No | length ≤ 20; nullable | — | Type label for tax\_id\_value (e.g. 'VAT', 'SIRET', 'EIN'). | | registered\_name | string \| null | ⚪ No | length ≤ 255; nullable | — | Full legal registered name of the workspace entity, as it appears on official documents. | | trade\_name | string \| null | ⚪ No | length ≤ 100; nullable | — | Operating / brand name of the workspace entity, if different from registered\_name. | | registered\_value | string \| null | ⚪ No | length ≤ 100; nullable (widened from 50 in Migration20260427100000) | — | Secondary registration identifier (e.g. SIREN, RCS number) distinct from the tax\_id. Stores the plain numeric or alphanumeric value without a type prefix. | | domain | string \| null | ⚪ No | length ≤ 253; nullable | — | Primary web domain of the workspace entity (RFC 1035 max 253 chars). Used for identity matching and enrichment. | | invoice\_status\_tolerance\_pct | string (decimal 6,4) | ✅ Yes | NOT NULL; DEFAULT 0.0100; CHECK (invoice\_status\_tolerance\_pct >= 0 AND invoice\_status\_tolerance\_pct \<= 1) | 0.0000–1.0000 (inclusive) | Percentage-of-grand-total tolerance band for invoice payment\_status recomputation. The larger of (grand\_total × pct) and invoice\_status\_tolerance\_abs is used as the allowable rounding gap, then capped per InvoiceStatusRecomputeService plan §4.3. | | invoice\_status\_tolerance\_abs | string (decimal 12,2) | ✅ Yes | NOT NULL; DEFAULT 0.50; CHECK (invoice\_status\_tolerance\_abs >= 0) | ≥ 0 | Absolute monetary tolerance (in base\_currency) for invoice payment\_status recomputation. Used in conjunction with invoice\_status\_tolerance\_pct. | | created\_at | 🔒 system — Date | ✅ Yes | set once on insert; never updated | — | Timestamp of row creation. | | updated\_at | 🔒 system — Date \| undefined | ⚪ No | set on insert and on every update via onUpdate hook | — | Timestamp of last modification. | | deleted\_at | Date \| null | ⚪ No | nullable; soft-delete sentinel | — | Soft-delete timestamp. Non-null means the settings row has been logically deleted. All active queries must filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ---------------------------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (OneToOne owner side) | ✅ Yes | The workspace this settings row belongs to. This entity owns the foreign key (workspace\_pk). The UNIQUE constraint on workspace\_pk enforces the 1-to-1 cardinality at the DB level. | | account\_receivable\_default | to-one (ManyToOne) | ⚪ No | Default LedgerAccount used for the accounts-receivable line when the posting engine generates journal entries for outbound invoices. Nullable — if absent, the posting engine falls back to plan-defined defaults. | | account\_payable\_default | to-one (ManyToOne) | ⚪ No | Default LedgerAccount used for the accounts-payable line when the posting engine generates journal entries for inbound invoices. Nullable — if absent, the posting engine falls back to plan-defined defaults. | ### System-computed * workspace\_accounting\_settings\_id: generated via gen\_random\_uuid() at insert; client must not supply this value. * created\_at: set by MikroORM onCreate hook at insert time; never overwritten. * updated\_at: set by MikroORM onCreate hook at insert and by onUpdate hook on every subsequent flush. * deleted\_at: written only by soft-delete logic; never set via the upsert API path. * Row creation: WorkspaceService.setupWorkspace() creates the initial row via WorkspaceAccountingSettingsRepository.persist() during workspace onboarding. WorkspaceAccountingSettingsService.upsertByWorkspace() handles all subsequent writes (find-or-create semantics — it creates the row if absent). * Identity field auto-population: WorkspaceSelfIdentityService applies an N=2 consensus rule over WorkspaceIdentityExtraction rows before committing values (registered\_name, tax\_id\_value, tax\_id\_type, registered\_value, domain, trade\_name, base\_currency, country, accounting\_framework) to this table automatically. * accounting\_framework PostgreSQL enum retains the deprecated 'MAR' value (removed from application code; cannot be dropped via ALTER TYPE). Any pre-existing 'MAR' rows were migrated to 'IFRS' by Migration20260409200000. * Tolerance defaults: invoice\_status\_tolerance\_pct defaults to 0.0100 (1%); invoice\_status\_tolerance\_abs defaults to 0.50. Both are application-layer defaults that align with InvoiceStatusRecomputeService plan §4.3 caps. ## Example ```json theme={null} { "data": { "type": "workspace_accounting_settings", "id": "c3a7e291-11b4-4f8a-9f2c-847bde2a0e11", "attributes": { "base_currency": "EUR", "country": "FR", "accounting_framework": "PCG", "fiscal_year_start_month": 1, "tax_id_value": "FR42501234567", "tax_id_type": "VAT", "registered_name": "Acme SAS", "trade_name": "Acme", "registered_value": "501234567", "domain": "acme.com", "invoice_status_tolerance_pct": "0.0100", "invoice_status_tolerance_abs": "0.50", "created_at": "2026-01-15T09:22:00.000Z", "updated_at": "2026-04-03T14:10:55.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspaces", "id": "9f3b1c20-55ea-4a1b-b8f0-fe1234abcd99" } }, "account_receivable_default": { "data": { "type": "ledger_accounts", "id": "aab10000-0000-0000-0000-000000000001" } }, "account_payable_default": { "data": { "type": "ledger_accounts", "id": "aab10000-0000-0000-0000-000000000002" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceAccountingSettings.ts` · domain: financial-graph · tier: Infrastructure # Workspace Connector Sync Log Source: https://docs.wellapp.ai/object-reference/workspace_connector_sync_logs WorkspaceConnectorSyncLog is an append-style audit record that captures the lifecycle of a single connector sync run for a workspace WorkspaceConnectorSyncLog is an append-style audit record that captures the lifecycle of a single connector sync run for a workspace. It is created when a sync begins (SCHEDULED or IN\_PROGRESS), updated by the sync orchestrator as the run progresses, and finalised with SUCCESS or ERROR once the connector returns or fails. Every log row belongs to exactly one WorkspaceConnector and one Workspace, providing a per-connector execution history that the platform uses for observability, retry logic, and connector-status dashboards. | Naming | Value | | ------------------------------- | ----------------------------------- | | Object | Workspace Connector Sync Log | | Resource type (JSON:API `type`) | `workspace_connector_sync_log` | | Collection / records root | `workspace_connector_sync_logs` | | REST base | `/v1/workspace-connector-sync-logs` | | Entity class | `WorkspaceConnectorSyncLog` | ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------------------- | ------------- | | List | `GET /v1/workspace-connector-sync-logs` | ✅ Implemented | | Retrieve | `GET /v1/workspace-connector-sync-logs/{id}` | ✅ Implemented | | Create | `POST /v1/workspace-connector-sync-logs` | 🟡 Planned | | Update | `PATCH /v1/workspace-connector-sync-logs/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-connector-sync-logs/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------------------- | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_connector\_sync\_log\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated by gen\_random\_uuid() at INSERT | — | Public stable identifier for this sync log row. Used in all API responses; the internal pk is never exposed. | | status | string (ConnectorSyncStatusEnum) | ✅ Yes | DEFAULT 'scheduled'; non-nullable; one of the four enum values | scheduled, in\_progress, success, error | Current execution state of the sync run. Starts as SCHEDULED (or IN\_PROGRESS when created by the service), transitions to SUCCESS or ERROR once the connector run completes. The sync orchestrator is the only writer. | | trigger\_type | string (SyncTriggerTypeEnum), nullable | ⚪ No | nullable; one of the three enum values when present | cloud\_task, manual, oauth\_callback | How the sync run was initiated. CLOUD\_TASK = scheduled Cloud Tasks queue; MANUAL = user-initiated via UI or API; OAUTH\_CALLBACK = triggered immediately after a successful OAuth authorization flow. | | cloud\_task\_id | string, VARCHAR(255), nullable | ⚪ No | nullable; max 255 chars (cast from TEXT in Migration20260407100000) | — | The fully-qualified Cloud Tasks task name associated with this sync run when trigger\_type is CLOUD\_TASK. Null for manual and OAuth-callback triggers. Used for deduplication and traceability in the task queue. | | started\_at | datetime, nullable | ⚪ No | nullable; set by the orchestrator when actual processing begins (not at row creation for SCHEDULED status) | — | Timestamp when the sync run transitioned from SCHEDULED to IN\_PROGRESS and connector API calls began. Null if the run was never picked up (e.g. task enqueued but not yet dispatched). | | completed\_at | datetime, nullable | ⚪ No | nullable; set atomically with duration\_ms and the terminal status (SUCCESS or ERROR) | — | Timestamp when the sync run reached a terminal state. Null while the run is SCHEDULED or IN\_PROGRESS. | | duration\_ms | integer, nullable | ⚪ No | nullable; integer; computed as completed\_at - started\_at in milliseconds by the repository markSuccess/markError helpers | — | Wall-clock duration of the sync run in milliseconds. Null when started\_at is null (run never began) or while still in progress. Derived at finalisation time, not stored as a trigger. | | error | string, TEXT, nullable | ⚪ No | nullable; no length limit; populated only when status = error | — | Human-readable error message or stringified exception captured when the sync run terminates with an ERROR status. Null on success. Used by the monitoring layer and displayed in the connector-status dashboard. | | metadata | jsonb, nullable | ⚪ No | nullable; free-form JSONB object; no fixed schema enforced at the DB level | — | Provider-specific sync context written at completion. Common keys include records\_synced, pages\_fetched, connector\_slug, and billing-related counts. Not queried by the platform for logic — observability and debugging only. | | created\_at | datetime, 🔒 system | ✅ Yes | set once on INSERT via MikroORM onCreate lifecycle hook; never updated | — | Row creation timestamp. Corresponds to when the sync was enqueued or initiated, which may precede started\_at by a few seconds for CLOUD\_TASK triggers. | | updated\_at | datetime, 🔒 system | ✅ Yes | set on INSERT and on every UPDATE via MikroORM onCreate/onUpdate lifecycle hooks | — | Last modification timestamp. Advances each time the sync orchestrator patches status, started\_at, completed\_at, duration\_ms, error, or metadata. | | deleted\_at | datetime, nullable | ⚪ No | nullable; soft-delete convention; all queries must filter deleted\_at IS NULL | — | Soft-delete timestamp. Null for active log rows. When set, the log is logically removed but retained for audit purposes. | ### Relationships | Name | Type | Required | Description | | ------------------ | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspaceConnector | to-one (workspace\_connector) | ✅ Yes | The WorkspaceConnector instance this sync run belongs to. Every sync log is scoped to exactly one connector. Cascade-delete: when the parent WorkspaceConnector is deleted, all its sync logs are deleted via ON DELETE CASCADE. | | workspace | to-one (workspace) | ✅ Yes | The Workspace that owns this sync log. Denormalized from the WorkspaceConnector for efficient tenant-scoped queries (direct workspace\_pk filter without joining through workspace\_connectors). Cascade-delete: when the Workspace is deleted, all its sync logs are deleted via ON DELETE CASCADE. | ### System-computed * workspace\_connector\_sync\_log\_id is generated by gen\_random\_uuid() at INSERT (PostgreSQL DEFAULT); the column carries a UNIQUE constraint. * created\_at is set once by MikroORM onCreate: () => new Date() and never mutated. * updated\_at is set by both onCreate and onUpdate hooks, advancing on every PATCH by the sync orchestrator. * deleted\_at follows the platform-wide soft-delete convention; active rows have deleted\_at = NULL; all repository and data-view queries must include this filter. * duration\_ms is computed at finalisation time by the repository helpers (markSuccess / markError) as completed\_at.getTime() - started\_at.getTime(). It is null when started\_at is null. * The workspace FK is denormalized from WorkspaceConnector.workspace at row creation time, enabling workspace-scoped index queries (idx\_workspace\_connector\_sync\_logs\_workspace) without a join. * Three indexes support the common access patterns: composite (workspace\_connector\_pk, created\_at DESC) for per-connector history paging; (workspace\_pk) for workspace-scoped listing; (status) for filtering by sync state. None carry a WHERE predicate — they are plain B-tree indexes, not partial indexes. * The workspace\_connector\_service\_id column that was present in the original migration was dropped in Migration20260407100000 and is absent from the current entity — do not reference it. * cloud\_task\_id was narrowed from TEXT to VARCHAR(255) in Migration20260407100000. ## Example ```json theme={null} { "data": { "type": "workspace_connector_sync_log", "id": "f3a1bc44-902c-4f8d-b7e2-0d9e5a2c1340", "attributes": { "workspace_connector_sync_log_id": "f3a1bc44-902c-4f8d-b7e2-0d9e5a2c1340", "status": "success", "trigger_type": "cloud_task", "cloud_task_id": "projects/well-prod/locations/europe-west1/queues/connector-sync/tasks/task-7f3b91c2", "started_at": "2026-06-02T08:14:02.000Z", "completed_at": "2026-06-02T08:14:47.000Z", "duration_ms": 45000, "error": null, "metadata": { "records_synced": 312, "pages_fetched": 4, "connector_slug": "pennylane" }, "created_at": "2026-06-02T08:14:01.800Z", "updated_at": "2026-06-02T08:14:47.200Z", "deleted_at": null }, "relationships": { "workspace_connector": { "data": { "type": "workspace_connector", "id": "a9d3c82e-1f45-4b2c-8e67-3d0a1b9c4512" } }, "workspace": { "data": { "type": "workspace", "id": "c1e7f034-87b2-41d3-9a56-0f2e8b7d3c90" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceConnectorSyncLog.ts` · domain: ingestion · tier: Platform # WorkspaceConnectorSyncTarget Source: https://docs.wellapp.ai/object-reference/workspace_connector_sync_targets WorkspaceConnectorSyncTarget is the junction table that maps a single WorkspaceConnector to one or more target Workspace rows, enabling multi-workspace sync: on WorkspaceConnectorSyncTarget is the junction table that maps a single WorkspaceConnector to one or more target Workspace rows, enabling multi-workspace sync: one connector can ingest data into N workspaces, not only its own carrier workspace. Each row asserts "this connector must push synced records into this workspace." The table is soft-deleted (tombstones preserve audit history and allow re-add), and a partial unique index prevents a connector from targeting the same workspace twice while active. Backfill rows (one per legacy connector) were inserted by the creation migration so existing connectors continue to behave identically without code changes. | Naming | Value | | ------------------------------- | -------------------------------------- | | Object | WorkspaceConnectorSyncTarget | | Resource type (JSON:API `type`) | `workspace` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-connector-sync-targets` | | Entity class | `WorkspaceConnectorSyncTarget` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------------------- | ---------- | | List | `GET /v1/workspace-connector-sync-targets` | 🟡 Planned | | Retrieve | `GET /v1/workspace-connector-sync-targets/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-connector-sync-targets` | 🟡 Planned | | Update | `PATCH /v1/workspace-connector-sync-targets/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-connector-sync-targets/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | datetime (TIMESTAMPTZ) 🔒 system | ✅ Yes | NOT NULL; set once on INSERT via MikroORM onCreate hook; never updated | — | Timestamp when the sync-target row was created. Stamped by the ORM onCreate hook; not editable by the user. | | updated\_at | datetime (TIMESTAMPTZ) 🔒 system | ⚪ No | Nullable; set on INSERT and on every UPDATE via MikroORM onUpdate hook | — | Timestamp of the last modification to this row. Managed automatically by the ORM. | | deleted\_at | datetime (TIMESTAMPTZ) 🔒 system | ⚪ No | Nullable; NULL means active; non-NULL means soft-deleted. The partial unique index idx\_wcst\_unique\_active only covers rows WHERE deleted\_at IS NULL. | — | Soft-delete sentinel. Set by the service layer when the target is removed (PUT set-semantics diff). NULL = active row. A soft-deleted tombstone does not block a future re-add of the same (connector, workspace) pair. | ### Relationships | Name | Type | Required | Description | | ------------------ | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspaceConnector | to-one (ManyToOne) | ✅ Yes | The WorkspaceConnector that sources the sync. FK column: workspace\_connector\_pk. On DELETE CASCADE at the database level. Indexed via idx\_wcst\_connector\_deleted for hot-path persister lookups. | | targetWorkspace | to-one (ManyToOne) | ✅ Yes | The Workspace that will receive synced records from the connector. FK column: target\_workspace\_pk. On DELETE CASCADE at the database level. Indexed via idx\_wcst\_target\_workspace\_deleted for reverse-lookup (which connectors push into a given workspace). | | createdBy | to-one (ManyToOne) | ⚪ No | The People record (authenticated user) who created this sync-target row. FK column: created\_by\_pk. Nullable by design: backfill rows inserted by the migration carry NULL because there is no human author to credit. On DELETE SET NULL at the database level. Controllers on the API path are required to stamp this from req.user.person\_pk. | ### System-computed * pk — auto-increment SERIAL primary key assigned by PostgreSQL on INSERT; never exposed as a public UUID. The formatter surfaces String(row\.pk) as the JSON:API `id` field because this junction has no dedicated \*\_id UUID column by design — consumers are expected to reference targets by their (workspace\_connector\_id, target\_workspace\_id) tuple. * created\_at — stamped once on INSERT by MikroORM onCreate: () => new Date(). * updated\_at — stamped on INSERT and on every subsequent UPDATE by MikroORM onUpdate: () => new Date(). * deleted\_at — set to the current timestamp by WorkspaceConnectorSyncTargetService when a target is removed via the PUT set-semantics diff; set to NULL (re-add) when a soft-deleted row is reactivated. Never set by the connector sync pipeline directly. * Backfill rows — the creation migration (Migration20260527140000) inserts one row per active WorkspaceConnector pointing at its own carrier workspace with created\_by\_pk = NULL, preserving pre-feature 1:1 behaviour for legacy connectors without any code change. * Partial unique index idx\_wcst\_unique\_active — enforced by PostgreSQL: UNIQUE (workspace\_connector\_pk, target\_workspace\_pk) WHERE deleted\_at IS NULL. Guarantees at most one active target per (connector, workspace) pair; soft-deleted tombstones are excluded so a re-add is always safe. ## Example ```json theme={null} { "data": { "type": "workspaceConnectorSyncTarget", "id": "4182", "attributes": { "created_at": "2026-05-27T14:12:33.000Z", "updated_at": "2026-05-27T14:12:33.000Z" }, "relationships": { "workspace_connector": { "data": { "type": "workspaceConnector", "id": "wc_01hwzgq3pk8f0qb3cns7x4ye2t" } }, "target_workspace": { "data": { "type": "workspace", "id": "ws_01hwzgq3pk8f0qb3cns7x4ye2t" } }, "created_by": { "data": { "type": "people", "id": "ppl_01hwzgq3pk8f0qb3cns7x4ye2t" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/WorkspaceConnectorSyncTarget.ts` · domain: ingestion · tier: Infrastructure # Workspace Connector Source: https://docs.wellapp.ai/object-reference/workspace_connectors A `workspace_connector` is an activation record that binds a generic `Connector` definition to a specific workspace, creating a live, credentialed integration i A `workspace_connector` is an activation record that binds a generic `Connector` definition to a specific workspace, creating a live, credentialed integration instance. It is the unit of auth lifecycle management: it carries the per-workspace OAuth credentials (access token, refresh token, DCR client ID) inside an optional JSONB `config` column, tracks the lifecycle `status` of the integration, and optionally links to the `Person` who owns or administers the connection. Every sync run, connector mapping, and sync diagnostic in the pipeline is scoped to a `workspace_connector`. It serves as the FK target for junction tables (`{entity}_workspace_connectors`, `workspace_connector_sync_targets`, `workspace_connector_sync_logs`) and is the `sourceWorkspaceConnector` provenance pointer on ingested entities. | Naming | Value | | ------------------------------- | -------------------------- | | Object | Workspace Connector | | Resource type (JSON:API `type`) | `workspace_connector` | | Collection / records root | `workspace_connectors` | | REST base | `/v1/workspace-connectors` | | Entity class | `WorkspaceConnector` | ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------- | ------------- | | List | `GET /v1/workspace-connectors` | ✅ Implemented | | Retrieve | `GET /v1/workspace-connectors/{id}` | ✅ Implemented | | Create | `POST /v1/workspace-connectors` | 🟡 Planned | | Update | `PATCH /v1/workspace-connectors/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-connectors/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------ | ----------------------------------- | -------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace\_connector\_id | string, UUID | ✅ Yes | unique, gen on onCreate via randomUUID() | — | Public-facing stable identifier for this connector instance. Used as the external reference in all API responses and OAuth state parameters. Generated at row creation; never changes. | | status | string (StatusEnum) | ✅ Yes | native PG enum `status_enum` | enabled, disabled, to\_configure, processing, error, need\_reconnect, suspended | Current lifecycle state of the connector instance. `enabled` means tokens are valid and sync can run. `need_reconnect` indicates token expiry or revocation requiring user re-auth. `processing` is set during an active sync run. `error` indicates a non-recoverable failure. `suspended` indicates a billing- or admin-triggered pause. `to_configure` is the initial state before OAuth is completed. | | config | jsonb, object | ⚪ No | nullable JSONB; never exposed in plaintext API responses without scrubbing | — | Per-workspace encrypted credential store. For MCP OAuth connectors holds: `client_id` (DCR-registered or pre-configured), `client_secret`, `dcr_access_token`, `access_token`, `refresh_token`, `token_expires_at` (ISO-8601 string), `pkce_code_verifier` (ephemeral, cleared after exchange), `pkce_state` (ephemeral). Also stores provider-specific runtime metadata (e.g. tenant name, scopes) written back by `McpOAuthService` after token exchange. For API-key connectors, stores `api_key`. For WSSE connectors, stores `auth_wsse.username` and `auth_wsse.secret`. | | created\_at | timestamp with time zone, 🔒 system | ✅ Yes | set once on onCreate; never updated | — | Row creation timestamp. Set by MikroORM lifecycle hook on insert. Immutable after creation. | | updated\_at | timestamp with time zone, 🔒 system | ⚪ No | set on onCreate and updated on every onUpdate | — | Last-modified timestamp. Updated by MikroORM lifecycle hook on every persist. Null only if the row has never been updated after insert (edge case). | | deleted\_at | timestamp with time zone | ⚪ No | nullable; soft-delete pattern — null means active | — | Soft-delete timestamp. When set, the connector instance is considered disconnected. All downstream queries filter `deleted_at IS NULL` to exclude soft-deleted connectors. A soft-deleted row preserves audit trail and is the trigger for token revocation on the provider side. | ### Relationships | Name | Type | Required | Description | | ---------------------------------------------------------------------------- | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | connector | to-one (Connector) | ✅ Yes | The canonical connector definition this instance activates. Carries provider type, category/service IDs, authorization flow, transport type, auth\_config (OAuth metadata URL, MCP server URL), and supported target models. A single `Connector` can have many `workspace_connector` activations across different workspaces. | | workspace | to-one (Workspace) | ✅ Yes | The workspace that owns and operates this connector instance. Acts as the tenant boundary: every sync run, entity ingested, and sync target scoped to this workspace\_connector is isolated within this workspace. | | person | to-one (People) | ⚪ No | The person (workspace member) who owns or administered the connection. Optional — connectors created programmatically (e.g. via API key or admin seeding) may have no person owner. Indexed via `idx_workspace_connectors_person`. | | filter (virtual) | to-one (ConnectorFilter) | ⚪ No | Virtual property — NOT a persisted FK column. Populated at query time by `WorkspaceConnectorRepository`. Resolves to either a custom `ConnectorFilter` for this workspace, or the template filter for the connector. Controls what data is ingested (e.g. date range, account scope). Not stored on the `workspace_connectors` table itself. | | syncTargets (WorkspaceConnectorSyncTarget) — inverse reference | to-many (WorkspaceConnectorSyncTarget) | ⚪ No | Inverse reference — NOT declared as `@OneToMany` on WorkspaceConnector. `WorkspaceConnectorSyncTarget` declares `@ManyToOne(() => WorkspaceConnector)`. Junction rows defining which target workspaces this connector ingests data into (added in Migration20260527140000). Each row links this connector to a `target_workspace_pk`. Partial unique index `idx_wcst_unique_active` prevents duplicate active targets. Backfilled with a 1:1 self-pointing row for all pre-existing connectors. | | connectorMappings (ConnectorMapping) — inverse reference | to-many (ConnectorMapping) | ⚪ No | Inverse reference — NOT declared as `@OneToMany` on WorkspaceConnector. `ConnectorMapping` declares `@ManyToOne({ entity: () => WorkspaceConnector, fieldName: 'workspace_connector_pk' })`. The JSONata sync mapping configurations generated by the structured-jury pipeline for each target model (company, invoice, transaction, etc.). One mapping row per (workspace\_connector, target\_model). Holds the compiled JSONata expression, last\_persist\_count, and needs\_regeneration flag. | | syncDiagnostics (ConnectorSyncDiagnostic) — inverse reference | to-many (ConnectorSyncDiagnostic) | ⚪ No | Inverse reference — NOT declared as `@OneToMany` on WorkspaceConnector. `ConnectorSyncDiagnostic` declares `@ManyToOne({ entity: () => WorkspaceConnector, fieldName: 'workspace_connector_pk' })`. Diagnostic and observability rows emitted by the sync pipeline — jury runs, schema drift, mapping failures, regression reverts. Each row references this workspace\_connector as the sync context. | | syncLogs (WorkspaceConnectorSyncLog) — inverse reference | to-many (WorkspaceConnectorSyncLog) | ⚪ No | Inverse reference — NOT declared as `@OneToMany` on WorkspaceConnector. `WorkspaceConnectorSyncLog` declares `@ManyToOne(() => WorkspaceConnector, { deleteRule: 'cascade' })`. Sync execution log rows emitted per sync run. CASCADE-delete: all log rows are deleted when the parent `workspace_connector` row is hard-deleted. | | documentWorkspaceConnectors (DocumentWorkspaceConnector) — inverse reference | to-many (DocumentWorkspaceConnector) | ⚪ No | Inverse reference — NOT declared as `@OneToMany` on WorkspaceConnector. Junction table tracking which documents were ingested by this connector, with direction discrimination (`input` vs `output`). Equivalent to the company/people/account/invoice/transaction junction tables; `DocumentWorkspaceConnector` declares `@ManyToOne(() => WorkspaceConnector)`. | ### System-computed * `workspace_connector_id` is generated via `randomUUID()` on the MikroORM `onCreate` hook — equivalent to `gen_random_uuid()` in Postgres. * `created_at` is set once on `onCreate` and is immutable. `updated_at` is refreshed on every `onUpdate` lifecycle call. * `deleted_at` follows the platform-wide soft-delete convention: null = active, non-null = soft-deleted. Downstream queries and Hasura RLS always filter `deleted_at IS NULL`. * `config` is a free-form JSONB column typed as `Record` in the ORM. At runtime it is cast to `McpOAuthWorkspaceConfig` by `McpOAuthService`. The known sub-fields are: `client_id`, `client_secret`, `dcr_access_token`, `access_token`, `refresh_token`, `token_expires_at`, `pkce_code_verifier` (ephemeral — cleared after PKCE exchange), `pkce_state` (ephemeral — cleared after callback), plus provider-specific keys written back after token exchange (e.g. tenant info). * `filter` is a virtual (non-persisted) property. `WorkspaceConnectorRepository` resolves it at query time by finding either a workspace-scoped `ConnectorFilter` or the connector template filter. It does not correspond to any column on the `workspace_connectors` table. * The internal `pk` is an auto-increment integer used exclusively for FK joins. All public-facing references use `workspace_connector_id` (UUID). * OAuth state tracking for PKCE flows: `McpOAuthService.getAuthorizationUrl()` writes `pkce_code_verifier` and `pkce_state` into `config` before redirect; `McpOAuthService.exchangeCodeForTokens()` clears them and writes `access_token`, `refresh_token`, `token_expires_at` in their place. * DCR (Dynamic Client Registration): `McpOAuthService.ensureClientRegistered()` checks `config.client_id` before writing a new DCR client. Each `workspace_connector` gets its own OAuth client registered with the provider — DCR is per-workspace-connector, not per-connector-template. * Token refresh buffer: `McpOAuthService` refreshes when `token_expires_at` is within 5 minutes of expiry (enforced as an architectural constant). * The junction tables `{entity}_workspace_connectors` (company, people, account, invoice, transaction, document) reference `workspace_connector_pk` via FK — they track which connector ingested each entity row, with direction discrimination (`input` vs `output`). * `workspace_connector_sync_targets` was backfilled via Migration20260527140000 with a 1:1 self-pointing row for every active pre-existing connector to preserve legacy single-workspace ingestion semantics. * Composite `idx_workspace_connectors_workspace_deleted` on `(workspace_pk, deleted_at)` supports the hot-path workspace-scoped connector list query. Additional indexes on `connector_pk`, `person_pk`, and `status` support secondary traversals. * The `WorkspaceConnectorSyncLog` relationship carries `deleteRule: 'cascade'` on the child side — hard-deleting a workspace\_connector row cascades to remove all associated sync log rows. ## Example ```json theme={null} { "type": "workspace_connector", "id": "a3c7e2b1-84f0-4d1e-9c3a-0f2b6d8e1a7c", "attributes": { "workspace_connector_id": "a3c7e2b1-84f0-4d1e-9c3a-0f2b6d8e1a7c", "status": "enabled", "config": { "client_id": "well_dcr_abc123", "dcr_access_token": "ey...", "access_token": "ey...", "refresh_token": "rt_xyz789", "token_expires_at": "2026-06-02T18:00:00.000Z" }, "created_at": "2026-02-14T10:23:00.000Z", "updated_at": "2026-06-01T08:42:11.000Z", "deleted_at": null }, "relationships": { "connector": { "data": { "type": "connector", "id": "b8f1d3a2-5c20-4e6b-8d4a-1e9c7f3b2d01" } }, "workspace": { "data": { "type": "workspace", "id": "9f3e1c7a-2d44-4b8e-a1f5-0c6b3e9d7e42" } }, "person": { "data": { "type": "people", "id": "d4a2c8f0-1b35-4c7d-90e2-5f8a6b3c1d20" } } } } ``` Source: `apps/api/src/database/entities/WorkspaceConnector.ts` · domain: ingestion · tier: Platform # WorkspaceGroupMembership Source: https://docs.wellapp.ai/object-reference/workspace_group_memberships WorkspaceGroupMembership represents a single person's membership within a workspace group, capturing the lifecycle from invitation through acceptance or revocat WorkspaceGroupMembership represents a single person's membership within a workspace group, capturing the lifecycle from invitation through acceptance or revocation. It associates a People record with a WorkspaceGroup, carries the assigned role, invitation token, and status, and is soft-deleted on revocation so the audit trail is preserved. A partial unique index on (person\_pk, workspace\_group\_pk) WHERE deleted\_at IS NULL enforces the idempotent-invite guard at the schema level: at most one live membership per person-group pair, while allowing revoke-then-re-invite semantics. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceGroupMembership | | Resource type (JSON:API `type`) | `people` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-group-memberships` | | Entity class | `WorkspaceGroupMembership` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------------------- | ---------- | | List | `GET /v1/workspace-group-memberships` | 🟡 Planned | | Retrieve | `GET /v1/workspace-group-memberships/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-group-memberships` | 🟡 Planned | | Update | `PATCH /v1/workspace-group-memberships/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-group-memberships/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------------------- | ---------------------------- | -------- | -------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_group\_membership\_id | string (UUID) | ✅ Yes | unique; generated by gen\_random\_uuid() at row creation | any valid UUID v4 | Public stable identifier for the membership. Exposed as the JSON:API resource id. Never changes after creation. | | membership\_role | string (enum) | ✅ Yes | NOT NULL; DB default 'member' | owner \| admin \| member \| guest | The role assigned to the person within the group. Set at invitation time. Changeable via PATCH /v1/workspace-groups/memberships/:membership\_id by an owner or admin. | | status | string (enum) | ✅ Yes | NOT NULL; DB default 'pending' | pending \| active | Lifecycle state of the membership. Starts as 'pending' on invitation; flips to 'active' when the invitee accepts via POST .../accept. | | invite\_token | string (UUID) \| null | ⚪ No | unique; nullable | any valid UUID v4, or null | One-time cryptographic token sent to the invitee. Cleared (set to null) or consumed when the invitation is accepted. The accept endpoint validates this token; the membership\_id alone is not sufficient proof. | | is\_default | boolean | ✅ Yes | NOT NULL; DB default false | true \| false | Marks whether this membership is the person's default group membership for the workspace. Used to determine the group selected by default in UI contexts. DB-defaulted to false. | | created\_at | 🔒 system — datetime | ✅ Yes | NOT NULL | ISO 8601 datetime | Timestamp of row creation. Set once by the onCreate MikroORM hook; never updated. | | updated\_at | 🔒 system — datetime \| null | ⚪ No | nullable | ISO 8601 datetime, or null | Timestamp of the most recent mutation. Set by onCreate and refreshed by onUpdate hooks. | | deleted\_at | 🔒 system — datetime \| null | ⚪ No | nullable | ISO 8601 datetime, or null | Soft-delete timestamp. Set when a membership is revoked. A non-null value excludes the row from active queries and from the partial unique index on (person\_pk, workspace\_group\_pk), allowing revoke-then-re-invite. | ### Relationships | Name | Type | Required | Description | | ---------------- | ---------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | person | to-one (ManyToOne) | Yes — NOT NULL FK | The People record being granted membership in the group. FK: workspace\_group\_memberships.person\_pk → peoples.pk (ON DELETE RESTRICT by default). | | workspace\_group | to-one (ManyToOne) | Yes — NOT NULL FK | The WorkspaceGroup this membership belongs to. FK: workspace\_group\_memberships.workspace\_group\_pk → workspace\_groups.pk (ON DELETE RESTRICT by default). | | invited\_by | to-one (ManyToOne, nullable) | No — nullable FK | The People record of the actor who issued the invitation. Nullable; SET NULL on delete of the inviter. Null for system-created memberships. | ### System-computed * workspace\_group\_membership\_id — generated by gen\_random\_uuid() at INSERT; unique constraint enforced at the DB level * created\_at — set once by MikroORM onCreate hook (new Date()); never overwritten * updated\_at — set by onCreate hook and refreshed on every UPDATE by onUpdate hook * deleted\_at — set to current timestamp by the revocation path (WorkspaceGroupService.revokeMembership); NULL for live rows * Partial unique index workspace\_group\_memberships\_person\_group\_active\_unique on (person\_pk, workspace\_group\_pk) WHERE deleted\_at IS NULL — enforced at the schema level; the service's idempotent-invite guard relies on this index to prevent duplicate pending/active memberships * invite\_token — generated as a UUID by the service's invite flow (WorkspaceGroupService.inviteToGroup); consumed/cleared on accept; the token is the cryptographic proof validated by the accept endpoint rather than the membership\_id alone * status transitions — 'pending' on creation (DB default); 'active' set by the service on a successful acceptInvite call; no direct user-patch path for status ## Example ```json theme={null} { "data": { "type": "workspace-group-membership", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "membership_role": "member", "status": "active", "invite_token": null, "is_default": false, "created_at": "2026-05-29T10:15:00.000Z", "updated_at": "2026-05-30T08:22:00.000Z" }, "relationships": { "person": { "data": { "type": "people", "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } }, "workspace_group": { "data": { "type": "workspace-group", "id": "c3d4e5f6-a7b8-9012-cdef-123456789012" } }, "invited_by": { "data": { "type": "people", "id": "d4e5f6a7-b8c9-0123-defa-234567890123" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceGroupMembership.ts` · domain: workspace · tier: Platform # WorkspaceGroupWorkspace Source: https://docs.wellapp.ai/object-reference/workspace_group_workspaces WorkspaceGroupWorkspace is the join-table entity that links a WorkspaceGroup to an individual Workspace, forming the 'which workspaces belong to this group' edg WorkspaceGroupWorkspace is the join-table entity that links a WorkspaceGroup to an individual Workspace, forming the "which workspaces belong to this group" edge. It is written exclusively by the workspace-group management pipeline (group creation / workspace enrollment flows); users have no resource PATCH endpoint against this table. The row carries a soft-delete column so that removing and re-adding the same (group, workspace) pair is legal — the partial unique index enforces at most one live join per pair while allowing re-creation after soft-deletion. An optional created\_by foreign key records which People actor enrolled the workspace. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceGroupWorkspace | | Resource type (JSON:API `type`) | `workspace_group_workspace` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-group-workspaces` | | Entity class | `WorkspaceGroupWorkspace` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------------- | ---------- | | List | `GET /v1/workspace-group-workspaces` | 🟡 Planned | | Retrieve | `GET /v1/workspace-group-workspaces/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-group-workspaces` | 🟡 Planned | | Update | `PATCH /v1/workspace-group-workspaces/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-group-workspaces/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | created\_at | 🔒 system — timestamptz | ✅ Yes | Set on INSERT via onCreate hook; never NULL | — | Timestamp when the workspace was enrolled into the group. Set automatically on creation. | | updated\_at | 🔒 system — timestamptz | ⚪ No | Set on INSERT and on every UPDATE via onUpdate hook; nullable in DB | — | Timestamp of the last modification to this join row. Updated automatically. | | deleted\_at | 🔒 system — timestamptz | ⚪ No | NULL means live; non-NULL means soft-deleted. The partial unique index on (workspace\_group\_pk, workspace\_pk) applies only WHERE deleted\_at IS NULL. | — | Soft-delete timestamp. When set, the workspace is considered removed from the group. The row is retained so that re-enrollment does not collide with the old row. | ### Relationships | Name | Type | Required | Description | | ---------------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_group | to-one (ManyToOne) | ✅ Yes | The WorkspaceGroup that this row belongs to. FK workspace\_group\_pk references core\_api.workspace\_groups(pk); RESTRICT on delete (no cascade). | | workspace | to-one (ManyToOne) | ✅ Yes | The Workspace being enrolled in the group. FK workspace\_pk references core\_api.workspaces(pk); RESTRICT on delete (no cascade). | | created\_by | to-one (ManyToOne) | ⚪ No | The People actor who enrolled this workspace into the group. FK created\_by\_pk references core\_api.peoples(pk); SET NULL on delete. Nullable — rows created by system flows or migrations may have no actor. | ### System-computed * pk — auto-increment serial primary key, internal only; never exposed in the public API * created\_at — set to new Date() by the MikroORM onCreate hook on INSERT * updated\_at — set to new Date() by the MikroORM onUpdate hook on every UPDATE * deleted\_at — set by the workspace-group management service when a workspace is removed from a group; never set by user direct PATCH * Partial unique index workspace\_group\_workspaces\_group\_workspace\_active\_unique enforces at most one live (workspace\_group\_pk, workspace\_pk) pair WHERE deleted\_at IS NULL — allows remove-then-re-add without 500 collisions * The unconditional unique constraint workspace\_group\_workspaces\_workspace\_group\_pk\_wor\_3c190\_unique was dropped by Migration20260529030000 and replaced with the partial unique index ## Example ```json theme={null} { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "workspace_group_workspace", "attributes": { "created_at": "2026-05-28T09:15:00.000Z", "updated_at": "2026-05-28T09:15:00.000Z", "deleted_at": null }, "relationships": { "workspace_group": { "data": { "type": "workspace_group", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } }, "workspace": { "data": { "type": "workspace", "id": "c9bf9e57-1685-4c89-bafb-ff5af830be8a" } }, "created_by": { "data": { "type": "people", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/WorkspaceGroupWorkspace.ts` · domain: workspace · tier: Platform # WorkspaceGroup Source: https://docs.wellapp.ai/object-reference/workspace_groups A WorkspaceGroup is a named collection that groups multiple workspaces together, enabling cross-workspace access management and shared membership A WorkspaceGroup is a named collection that groups multiple workspaces together, enabling cross-workspace access management and shared membership. Groups are created by a specific person (the creator) and can contain any number of workspace slots via the WorkspaceGroupWorkspace join entity, and any number of members via WorkspaceGroupMembership. The entity carries its own lifecycle (created\_at, updated\_at, soft-deleted via deleted\_at) and is identified externally by a UUID while relying on an internal serial primary key for joins. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceGroup | | Resource type (JSON:API `type`) | `workspace_group` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-groups` | | Entity class | `WorkspaceGroup` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------------- | ---------- | | List | `GET /v1/workspace-groups` | 🟡 Planned | | Retrieve | `GET /v1/workspace-groups/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-groups` | 🟡 Planned | | Update | `PATCH /v1/workspace-groups/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-groups/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | -------------------- | -------------------------------------- | -------- | ---------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_group\_id | string (UUID) | ✅ Yes | unique | — | Public-facing stable identifier for the group. Generated server-side via gen\_random\_uuid(); never supplied by the caller. Unique constraint enforced at the database level. | | name | string (varchar 255) | ✅ Yes | NOT NULL, max length 255 | — | Human-readable label for the workspace group. Set at creation time. NOT NULL in the database; max 255 characters (MikroORM default varchar length). | | created\_at | 🔒 system — Date (timestamptz) | ✅ Yes | NOT NULL, set once on insert | — | Timestamp of record creation. Set automatically by the MikroORM onCreate lifecycle hook; never supplied by the caller. | | updated\_at | 🔒 system — Date (timestamptz) | ⚪ No | nullable | — | Timestamp of the last update. Set by MikroORM onCreate and onUpdate lifecycle hooks; null until the first update after creation. | | deleted\_at | 🔒 system — Date (timestamptz) \| null | ⚪ No | nullable | — | Soft-delete timestamp. Null means the record is active. Set to the deletion timestamp when the group is removed; the row is never physically deleted. All active queries filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ----------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | created\_by | to-one (ManyToOne) | ✅ Yes | The People record of the person who created this workspace group. Foreign key created\_by\_pk references core\_api.peoples(pk). NOT NULL — every group must have an identified creator. | ### System-computed * workspace\_group\_id — generated server-side via gen\_random\_uuid() default and randomUUID() application-side default; unique constraint enforced in DB * created\_at — set automatically by MikroORM onCreate: () => new Date() * updated\_at — set automatically by MikroORM onCreate and onUpdate: () => new Date() * deleted\_at — managed by the soft-delete pattern; set by the service layer on logical deletion, never by the user directly ## Example ```json theme={null} { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "workspace_group", "attributes": { "workspace_group_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "EMEA Finance Team", "created_at": "2026-05-28T08:15:00.000Z", "updated_at": "2026-05-28T08:15:00.000Z", "deleted_at": null }, "relationships": { "created_by": { "data": { "id": "d7e8f9a0-b1c2-3456-def0-123456789abc", "type": "person" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceGroup.ts` · domain: workspace · tier: Platform # WorkspaceIdentityExtraction Source: https://docs.wellapp.ai/object-reference/workspace_identity_extractions WorkspaceIdentityExtraction is an evidence ledger that the self-invoice consensus mechanism uses to identify the workspace owner's own business identity from pr WorkspaceIdentityExtraction is an evidence ledger that the self-invoice consensus mechanism uses to identify the workspace owner's own business identity from processed invoices. Each row captures one (field, value) pair extracted from a single invoice that was determined to originate from or be addressed to the workspace owner, classified by the signal that triggered that determination. The service WorkspaceSelfIdentityService reads these rows to check whether N≥2 independent invoices agree on a given identity field value before committing it to WorkspaceAccountingSettings. The table is workspace-scoped and soft-deleted; both foreign keys cascade on hard delete of the parent workspace or source invoice. | Naming | Value | | ------------------------------- | ------------------------------------ | | Object | WorkspaceIdentityExtraction | | Resource type (JSON:API `type`) | `workspace_identity_extraction` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-identity-extractions` | | Entity class | `WorkspaceIdentityExtraction` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------------------ | ---------- | | List | `GET /v1/workspace-identity-extractions` | 🟡 Planned | | Retrieve | `GET /v1/workspace-identity-extractions/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-identity-extractions` | 🟡 Planned | | Update | `PATCH /v1/workspace-identity-extractions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-identity-extractions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------------------- | -------------------------------------- | -------- | ----------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_identity\_extraction\_id | string (UUID) | ✅ Yes | unique | — | Public-facing stable identifier for the extraction row, generated by gen\_random\_uuid() at insert time. Exposed as the JSON:API id. | | field\_name | string (enum: WorkspaceIdentityField) | ✅ Yes | varchar(40), NOT NULL | "tax\_id\_value" \| "registered\_value" \| "registered\_name" \| "trade\_name" \| "country" | Which workspace accounting field this extraction represents. Mirrors the writable subset of WorkspaceAccountingSettings. | | value | string | ✅ Yes | varchar(255), NOT NULL | — | The extracted value for the field named by field\_name (e.g., the actual VAT number, legal name, or ISO country code). | | tax\_id\_type | string | ⚪ No | varchar(20), nullable | — | Only populated when field\_name is 'tax\_id\_value'. Carries the type of tax identifier extracted (e.g., 'VAT', 'SIREN'). | | signal | string (enum: WorkspaceIdentitySignal) | ✅ Yes | varchar(40), NOT NULL | "mailbox\_sent" \| "receiver\_exact" \| "receiver\_partial" | Classifies why the extraction pipeline concluded that this invoice's issuer or receiver is the workspace owner. Drives confidence weighting in the consensus rule. | | created\_at | 🔒 system — timestamptz | ✅ Yes | NOT NULL, default now() | — | Timestamp set by the @Property onCreate hook when the row is first inserted. Not updated thereafter (no updated\_at on this entity). | | deleted\_at | 🔒 system — timestamptz | ⚪ No | nullable | — | Soft-delete timestamp. When set, the row is excluded from the partial indexes idx\_wie\_workspace\_field and uq\_wie\_workspace\_field\_invoice, and from all active consensus queries. | ### Relationships | Name | Type | Required | Description | | --------------- | ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace whose owner identity this row is gathering evidence for. ON DELETE CASCADE — all extraction rows are removed when the workspace is deleted. | | source\_invoice | to-one (ManyToOne → Invoice) | ✅ Yes | The invoice from which this (field, value) pair was extracted. ON DELETE CASCADE — extraction rows are removed when the source invoice is deleted. Combined with workspace\_pk and field\_name forms the partial-unique constraint uq\_wie\_workspace\_field\_invoice. | ### System-computed * workspace\_identity\_extraction\_id — generated by gen\_random\_uuid() at row creation via defaultRaw; client never supplies this value * created\_at — set by @Property onCreate hook; no updated\_at on this entity (it is append-only evidence; rows are never mutated after insert) * deleted\_at — soft-delete; set by the pipeline when an evidence row is invalidated, not by user action * pk — internal auto-increment serial primary key; never exposed via the API * Partial UNIQUE constraint uq\_wie\_workspace\_field\_invoice (workspace\_pk, field\_name, source\_invoice\_pk) WHERE deleted\_at IS NULL — enforced by the migration, not expressible in ORM decorators; prevents double-insert race during concurrent registerOne calls for the same invoice * Partial index idx\_wie\_workspace\_field (workspace\_pk, field\_name) WHERE deleted\_at IS NULL — hot-path index for the consensus query in WorkspaceSelfIdentityService.checkConsensusAndAct * Rows are created exclusively by WorkspaceSelfIdentityService (extraction pipeline); no user-facing PATCH endpoint exists for this entity * tax\_id\_type is conditionally populated: only the extraction service sets it, and only when field\_name === 'tax\_id\_value' ## Example ```json theme={null} { "data": { "type": "workspace_identity_extraction", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "field_name": "tax_id_value", "value": "FR12345678901", "tax_id_type": "VAT", "signal": "mailbox_sent", "created_at": "2026-04-22T09:15:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901" } }, "source_invoice": { "data": { "type": "invoice", "id": "c3d4e5f6-a7b8-9012-cdef-012345678902" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/WorkspaceIdentityExtraction.ts` · domain: ingestion · tier: Infrastructure # WorkspacePostingMapping Source: https://docs.wellapp.ai/object-reference/workspace_posting_mappings WorkspacePostingMapping encodes a single posting rule that maps a semantic document context (CoA version, semantic role, posting kind, document polarity) to acc WorkspacePostingMapping encodes a single posting rule that maps a semantic document context (CoA version, semantic role, posting kind, document polarity) to accounting targets (LedgerAccount, TaxRate, Journal) for a workspace. The posting engine writes these rows deterministically or via LLM review; the rule-resolver reads them at journal-entry draft time to auto-classify postings. Each row is scoped to a Workspace and optionally to a specific WorkspaceConnector, with precision modifiers for tax behaviour, counterparty kind, currency, country code, and effective date range. A partial unique index (WHERE deleted\_at IS NULL AND mapping\_status = 'active') prevents duplicate active mappings for the same context tuple. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspacePostingMapping | | Resource type (JSON:API `type`) | `workspace_posting_mapping` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-posting-mappings` | | Entity class | `WorkspacePostingMapping` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | -------------------------------------------- | ---------- | | List | `GET /v1/workspace-posting-mappings` | 🟡 Planned | | Retrieve | `GET /v1/workspace-posting-mappings/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-posting-mappings` | 🟡 Planned | | Update | `PATCH /v1/workspace-posting-mappings/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-posting-mappings/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------------------------- | ------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace\_posting\_mapping\_id | string (UUID) | ✅ Yes | UNIQUE | — | Public UUID identifying this posting rule. Generated via gen\_random\_uuid() on insert. | | well\_coa\_version | string | ✅ Yes | max length 32 | — | Version tag of the Well Chart-of-Accounts taxonomy this mapping targets (e.g. 'v2024'). | | well\_semantic\_role | string | ✅ Yes | max length 100 | — | Semantic accounting role within the CoA taxonomy (e.g. 'accounts\_payable', 'revenue\_product'). | | posting\_kind | string | ✅ Yes | max length 50 | — | Kind of source document or transaction driving the posting (e.g. 'invoice', 'bank\_transaction'). | | document\_polarity | string | ✅ Yes | max length 50 | — | Polarity direction of the posting line: whether this rule applies to the debit or credit leg. | | tax\_behavior | string \| null | ⚪ No | max length 50 | — | Optional tax inclusion modifier narrowing the rule ('exclusive', 'inclusive', 'none'). NULL means the rule applies regardless of tax behaviour. | | counterparty\_kind | string \| null | ⚪ No | max length 50 | — | Optional counterparty classification narrowing the rule (e.g. 'supplier', 'customer'). NULL matches any counterparty kind. | | currency | string \| null | ⚪ No | max length 3 | — | Optional ISO 4217 currency code narrowing the rule. NULL matches any currency. | | country\_code | string \| null | ⚪ No | max length 2 | — | Optional ISO 3166-1 alpha-2 country code narrowing the rule. NULL matches any country. | | effective\_from | date \| null | ⚪ No | CHECK: effective\_from \<= effective\_to when both non-NULL | — | Inclusive start date from which this rule is effective. NULL means no lower bound. Used in the partial-unique active-context index via COALESCE. | | effective\_to | date \| null | ⚪ No | CHECK: effective\_from \<= effective\_to when both non-NULL | — | Exclusive end date after which this rule is no longer effective. NULL means open-ended. | | mapping\_status | 🔒 system — enum (WorkspacePostingMappingStatusEnum) | ✅ Yes | default: 'active'; native enum workspace\_posting\_mapping\_status\_enum | active \| needs\_review \| inactive | Lifecycle state of this mapping rule. The partial unique index on active-context enforces one active rule per context tuple. Managed by the posting engine. | | confidence | string (decimal 4,3) \| null | ⚪ No | CHECK: value IS NULL OR (value >= 0 AND value \<= 1); decimal(4,3) | — | Confidence score in \[0.000, 1.000] assigned by the LLM jury or deterministic engine. Stored as decimal(4,3); exposed as string in JSON to preserve precision. | | evidence | object (JSONB) \| null | ⚪ No | — | — | Free-form JSONB payload recording why this mapping was created (matched patterns, rule sources, jury rationale). Shape varies by created\_by value. | | created\_by | 🔒 system — enum (WorkspacePostingMappingCreatedByEnum) | ✅ Yes | default: 'deterministic'; native enum workspace\_posting\_mapping\_created\_by\_enum | deterministic \| manual \| reviewed\_llm | Provenance of this mapping: whether it was produced by a deterministic rule, manual human edit, or an LLM that was subsequently reviewed. | | mapping\_version | 🔒 system — integer | ✅ Yes | default: 1 | — | Monotonically increasing version counter incremented by the posting engine on each rule revision. Default 1. | | created\_at | 🔒 system — datetime | ✅ Yes | — | — | Timestamp when this mapping was created. Set automatically via onCreate hook. | | updated\_at | 🔒 system — datetime \| null | ⚪ No | — | — | Timestamp of the last update to this mapping. Set automatically via onUpdate hook. | | deleted\_at | 🔒 system — datetime \| null | ⚪ No | — | — | Soft-delete timestamp. Non-null means the mapping is logically deleted and excluded from the partial unique active-context index. All queries must filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | ------------------------ | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The Workspace this posting rule belongs to. All queries must scope by workspace\_pk. Indexed as part of the role and status composite indexes. | | sourceWorkspaceConnector | to-one (ManyToOne) | ⚪ No | Optional WorkspaceConnector that provided the data driving this rule. NULL means the rule applies generically across all connectors for this workspace. ON DELETE SET NULL — deleting the connector nullifies but does not delete the mapping. | | ledger\_account | to-one (ManyToOne) | ⚪ No | Target LedgerAccount to post to for this context. NULL is valid when the rule's primary purpose is to select only a TaxRate or Journal without specifying a ledger account. | | tax\_rate | to-one (ManyToOne) | ⚪ No | Target TaxRate to apply when posting. NULL means no tax rate is assigned by this mapping; the posting engine may inherit or default. | | journal | to-one (ManyToOne) | ⚪ No | Target Journal (accounting book) to route journal entries into. NULL defers journal selection to the posting engine's default resolution logic. | ### System-computed * workspace\_posting\_mapping\_id — generated via gen\_random\_uuid() on insert, exposed as the public API id * created\_at — set automatically by the MikroORM onCreate hook; never writable after creation * updated\_at — set automatically by the MikroORM onUpdate hook on every write * deleted\_at — soft-delete field written by the posting engine on rule retirement; not writable by users * mapping\_status — managed exclusively by the posting engine (transitions: active → needs\_review → inactive or active → deleted\_at); default 'active' on creation * mapping\_version — incremented by the posting engine on each rule revision; default 1 * created\_by — set at creation time by the engine to record provenance ('deterministic', 'reviewed\_llm') or set to 'manual' for human-authored rules; not subsequently editable * confidence — computed and stamped by the LLM jury or deterministic scoring logic; null for fully-deterministic rules * evidence — populated by the engine with the reasoning payload from the mapping generation run * sourceWorkspaceConnector provenance — when set, records which connector sync triggered the mapping creation; ON DELETE SET NULL ensures orphan safety on connector removal * Partial unique index workspace\_posting\_mappings\_active\_context\_unique (WHERE deleted\_at IS NULL AND mapping\_status = 'active') — enforced at the DB layer only; cannot be expressed via MikroORM decorators (COALESCE expressions + partial predicate). The entity comment documents this divergence from schema:fresh. ## Example ```json theme={null} { "data": { "type": "workspace_posting_mapping", "id": "a3f1c2e4-8b7d-4f9a-b2c1-0d5e6f7a8b9c", "attributes": { "workspace_posting_mapping_id": "a3f1c2e4-8b7d-4f9a-b2c1-0d5e6f7a8b9c", "well_coa_version": "v2024", "well_semantic_role": "accounts_payable", "posting_kind": "invoice", "document_polarity": "debit", "tax_behavior": "exclusive", "counterparty_kind": "supplier", "currency": "EUR", "country_code": "FR", "effective_from": "2024-01-01", "effective_to": null, "mapping_status": "active", "confidence": "0.950", "evidence": { "rule_source": "deterministic_coa_bootstrap", "matched_patterns": ["vat_fr_20", "supplier_invoice"] }, "created_by": "deterministic", "mapping_version": 1, "created_at": "2025-05-25T10:00:00.000Z", "updated_at": "2025-05-25T10:00:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "b1c2d3e4-0000-0000-0000-000000000001" } }, "source_workspace_connector": { "data": null }, "ledger_account": { "data": { "type": "ledger_account", "id": "c9d8e7f6-0000-0000-0000-000000000002" } }, "tax_rate": { "data": { "type": "tax_rate", "id": "d1e2f3a4-0000-0000-0000-000000000003" } }, "journal": { "data": null } } } } ``` Source: `apps/api/src/database/entities/WorkspacePostingMapping.ts` · domain: financial-graph · tier: Infrastructure # WorkspaceProvider Source: https://docs.wellapp.ai/object-reference/workspace_providers WorkspaceProvider is the join table that records which global Provider entries a specific workspace has explicitly selected and/or was auto-matched to, acting a WorkspaceProvider is the join table that records which global Provider entries a specific workspace has explicitly selected and/or was auto-matched to, acting as the per-tenant provider bookmark layer. Each row links exactly one Workspace to one Provider and carries two boolean flags — `is_selected` (user explicitly added the provider via the API) and `is_matched` (system or explicit selection confirmed the relevance). The entity is owned by the `WorkspaceProviderService`, which upserts rows on explicit selection and soft-deletes them on removal. It is the authority for `GET /v1/workspaces/:id/providers` catalog responses and is read by the Vision Agent to resolve per-workspace provider scope. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceProvider | | Resource type (JSON:API `type`) | `workspace_provider` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-providers` | | Entity class | `WorkspaceProvider` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ------------------------------------- | ---------- | | List | `GET /v1/workspace-providers` | 🟡 Planned | | Retrieve | `GET /v1/workspace-providers/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-providers` | 🟡 Planned | | Update | `PATCH /v1/workspace-providers/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-providers/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ------------ | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | is\_selected | boolean | ⚪ No | DEFAULT false | true, false | Whether the workspace has explicitly selected this provider through the UI or API. Set to true via POST /v1/workspaces/:id/providers; set to false implicitly on soft-delete. | | is\_matched | boolean | ⚪ No | DEFAULT false; set to true automatically whenever is\_selected is set to true (repository enforces: if isSelected → is\_matched = true) | true, false | Whether the provider was confirmed relevant to this workspace, either through explicit user selection (which auto-sets this flag) or via system matching logic such as the workspace-provider scoring pipeline. | | created\_at | 🔒 system — timestamptz | ⚪ No | onCreate hook; DEFAULT now() | — | Timestamp of row creation. Set automatically by MikroORM onCreate hook. | | updated\_at | 🔒 system — timestamptz | ⚪ No | onCreate + onUpdate hooks | — | Timestamp of last row modification. Refreshed automatically by MikroORM onUpdate hook. | | deleted\_at | timestamptz \| null | ⚪ No | nullable; set to current timestamp on soft-delete via WorkspaceProviderRepository.softDelete | null (active) or ISO 8601 timestamp (soft-deleted) | Soft-delete marker. When non-null the provider is considered deselected for the workspace. All active-provider queries filter deleted\_at IS NULL. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace that selected this provider. Foreign key workspace\_pk → core\_api.workspaces.pk. Every WorkspaceProvider row is strictly tenant-scoped to this workspace. | | provider | to-one (ManyToOne) | ✅ Yes | The global provider catalog entry being bookmarked. Foreign key provider\_pk → core\_api.providers.pk. Carries provider name, slug, category, logo, URL, and Vision Agent skill fields. | ### System-computed * pk — auto-increment integer primary key; internal only, never exposed in the API * created\_at — set by MikroORM onCreate: () => new Date() hook * updated\_at — set by both onCreate and onUpdate: () => new Date() hooks * deleted\_at — null at creation; stamped with current timestamp by WorkspaceProviderRepository.softDelete() when the user removes the provider via DELETE /v1/workspaces/:id/providers/:providerId * is\_matched — automatically forced to true by the repository upsert logic whenever is\_selected is set to true (business rule: explicit user selection always counts as matched) * is\_selected default — false at row creation; set to true by WorkspaceProviderService.createWorkspaceProvider() via WorkspaceProviderRepository.upsert(workspace, provider, true) ## Example ```json theme={null} { "data": { "type": "workspace_provider", "id": "a3f1c2d4-88e0-4b5a-9f3c-1d2e3f4a5b6c", "attributes": { "is_selected": true, "is_matched": true, "created_at": "2025-11-14T09:30:00.000Z", "updated_at": "2025-11-14T09:30:00.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "d7e8f9a0-1234-4abc-8def-0123456789ab" } }, "provider": { "data": { "type": "provider", "id": "b2c3d4e5-5678-4bcd-9ef0-1234567890bc" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceProvider.ts` · domain: ingestion · tier: Platform # WorkspaceSubscription Source: https://docs.wellapp.ai/object-reference/workspace_subscriptions WorkspaceSubscription is the billing anchor for a Well workspace WorkspaceSubscription is the billing anchor for a Well workspace. It stores the Stripe customer identity and the active Stripe pricing-plan subscription reference alongside a mirrored subscription status that is kept in sync by the Stripe webhook pipeline. Each workspace owns exactly one subscription row, created during the workspace-creation flow and updated automatically as Stripe events arrive. It is the record the billing middleware consults to gate premium features and the record the Stripe webhook handler writes to when a subscription transitions state. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceSubscription | | Resource type (JSON:API `type`) | `workspace_subscription` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-subscriptions` | | Entity class | `WorkspaceSubscription` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | ----------------------------------------- | ---------- | | List | `GET /v1/workspace-subscriptions` | 🟡 Planned | | Retrieve | `GET /v1/workspace-subscriptions/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-subscriptions` | 🟡 Planned | | Update | `PATCH /v1/workspace-subscriptions/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-subscriptions/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | --------------------------------------- | ---------------------------- | -------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace\_subscription\_id | string (UUID) | ✅ Yes | unique, gen\_random\_uuid() default, never null | any UUID | The public-facing stable identifier for this subscription record. Exposed on the JSON:API envelope as the resource id. | | status | string \| null | ⚪ No | nullable; value is mirrored from Stripe pricing-plan subscription servicing\_status | "active" \| "canceled" \| "paused" | Mirrors the Stripe pricing-plan subscription's servicing\_status. Set by the Stripe webhook handler (customer.subscription.updated / checkout.session.completed) and by the subscription sync service. NULL until the first Stripe event is processed. | | stripe\_customer\_id | string \| null | ⚪ No | nullable | Stripe customer ID string (prefix cus\_) | The Stripe Customer object ID for this workspace's billing account. Written by SubscriptionsService when the workspace is linked to Stripe. | | stripe\_pricing\_plan\_subscription\_id | string \| null | ⚪ No | nullable | Stripe Subscription object ID string (prefix sub\_) | The Stripe Subscription object ID for the active pricing-plan subscription. Written when the workspace subscribes to a plan and updated on plan changes. | | created\_at | 🔒 system — datetime | ✅ Yes | set by onCreate hook, never null | — | Timestamp when the subscription row was first created. | | updated\_at | 🔒 system — datetime \| null | ⚪ No | set by onCreate and onUpdate hooks | — | Timestamp of the most recent mutation to this row. Updated on every Stripe webhook write. | | deleted\_at | datetime \| null | ⚪ No | nullable; soft-delete sentinel | — | When set, the subscription row is treated as deleted. Soft-delete is the only removal mechanism; hard deletes do not occur. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | workspace | to-one (ManyToOne) | Yes — the @ManyToOne decorator is declared without nullable:true on this entity | The workspace this subscription record belongs to. A subscription row is always bound to exactly one workspace; the workspace is the tenant boundary for all billing checks. Target entity: Workspace. | ### System-computed * workspace\_subscription\_id — generated by gen\_random\_uuid() at the database level on INSERT; also seeded client-side via randomUUID() as the ORM default * created\_at — set by MikroORM onCreate lifecycle hook to new Date(); never user-supplied * updated\_at — set by MikroORM onCreate and onUpdate lifecycle hooks; reflects every Stripe-webhook-driven mutation * deleted\_at — not set by any automated pipeline today; available as the soft-delete sentinel column following the platform convention * status — written exclusively by the Stripe webhook handler (stripe-webhooks.service.ts) and by SubscriptionsService.syncSubscriptionFromStripe; mirrors Stripe pricing-plan servicing\_status (active / canceled / paused) * stripe\_customer\_id — written by SubscriptionsService when creating or linking a Stripe Customer for the workspace * stripe\_pricing\_plan\_subscription\_id — written by SubscriptionsService when a pricing-plan subscription is activated or changed ## Example ```json theme={null} { "data": { "type": "workspace_subscription", "id": "3f8c1a2d-9e4b-47f0-b35a-dc12ef890abc", "attributes": { "workspace_subscription_id": "3f8c1a2d-9e4b-47f0-b35a-dc12ef890abc", "status": "active", "stripe_customer_id": "cus_QxR3mN7pLwT9bZ", "stripe_pricing_plan_subscription_id": "sub_1Pf7KkLmN9oQ2rS4tU6vW8", "created_at": "2024-11-14T09:32:11.000Z", "updated_at": "2025-03-01T16:47:05.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } } } ``` Source: `apps/api/src/database/entities/WorkspaceSubscription.ts` · domain: workspace · tier: Platform # WorkspaceView Source: https://docs.wellapp.ai/object-reference/workspace_views WorkspaceView represents a named, workspace-scoped configuration for displaying a record root (e.g WorkspaceView represents a named, workspace-scoped configuration for displaying a record root (e.g. invoices, companies) in the Well records table. It persists the full view state — visible columns, sort order, active filters, grouping, layout type, and layout-specific display settings — so users can switch between saved views without re-configuring each session. Each view is bound to exactly one Workspace and one record root string; a boolean flag marks at most one view per (workspace, root) combination as the default. Views are user-created and user-managed through a dedicated REST surface; they are never written by any connector or pipeline. | Naming | Value | | ------------------------------- | --------------------------------- | | Object | WorkspaceView | | Resource type (JSON:API `type`) | `workspace_view` | | Collection / records root | — (not a records root) | | REST base | `/v1/workspace-views` | | Entity class | `WorkspaceView` | **Internal object.** Not currently exposed on the public REST API. The operations below describe the intended contract. ## API operations | Operation | Method & path | Status | | --------- | --------------------------------- | ---------- | | List | `GET /v1/workspace-views` | 🟡 Planned | | Retrieve | `GET /v1/workspace-views/{id}` | 🟡 Planned | | Create | `POST /v1/workspace-views` | 🟡 Planned | | Update | `PATCH /v1/workspace-views/{id}` | 🟡 Planned | | Delete | `DELETE /v1/workspace-views/{id}` | 🟡 Planned | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | --------------- | ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | view\_id | string (UUID) | ✅ Yes | gen\_random\_uuid() default; UNIQUE | any valid UUID | Public stable identifier for this view. Exposed as the JSON:API id. Stable across renames — links to a saved view remain valid as long as the view exists. | | name | string | ✅ Yes | max length 255 | any non-empty string ≤ 255 chars | Human-readable display name for the view, shown in the view switcher tab and in the views list. | | root | string | ✅ Yes | max length 100; must match a value in the recordsQuerySchema root enum | see DATA\_VIEW\_ROOTS (e.g. 'invoices', 'companies', 'people', 'transactions') | The record root this view targets. Immutable after creation. A view for 'invoices' cannot be reused on 'companies'. | | columns | jsonb (ColumnConfig\[]) | ⚪ No | default '\[]'; each element: field (string\[]), order (number), optional width (px), alias, icon, pinned ('left'\|'right') | array of ColumnConfig objects | Ordered list of visible column configurations. Defines which fields appear, their display order, pixel width, optional alias, and pin side. An empty array means the root's default column set is used. | | sort | jsonb (SortConfig\[]) | ⚪ No | default '\[]'; each element: field (string\[]), direction ('asc'\|'desc') | array of SortConfig objects | Ordered list of sort rules applied to the query. Multiple entries are applied in array order (primary, secondary, etc.). | | filters | jsonb (FilterConfig\[]) | ⚪ No | default '\[]'; each element: field (string\[]), operator (FilterOperator), value, optional conjunction ('and'\|'or') | array of FilterConfig objects; operator one of: eq, neq, gt, gte, lt, lte, contains, not\_contains, starts\_with, ends\_with, is\_null, is\_not\_null, in, not\_in, between | Active filter conditions persisted with this view. Applied when the view is loaded. The conjunction field chains consecutive filters; defaults to 'and'. | | layout\_type | string | ⚪ No | max length 20; default 'table'; CHECK chk\_workspace\_views\_layout\_type enforces allowed values | 'table' \| 'kanban' \| 'calendar' \| 'gallery' \| 'chart' \| 'list' \| 'graph' | The UI layout mode for this view. Determines which renderer and layout\_config shape apply. The CHECK constraint is authoritative; the LAYOUT\_TYPES constant in @wellapp/shared mirrors it. | | layout\_config | jsonb (LayoutConfig) | ⚪ No | default '\{}'; shape depends on layout\_type (TableLayoutConfig \| KanbanLayoutConfig \| CalendarLayoutConfig \| GalleryLayoutConfig \| ChartLayoutConfig \| ListLayoutConfig \| Record\) | layout-type-specific config object or empty object | Layout-specific presentation settings. Contains ONLY layout rendering preferences (row height, card size, chart type, etc.), NOT field selection — that lives in display\_fields. Ignored by the renderer when layout\_type changes until explicitly set. | | display\_fields | jsonb (DisplayFieldConfig\[]) | ⚪ No | default '\[]'; each element: fieldId (dot-path string), visible (bool), order (number), optional displayType, skillId, skillRenderHint, width, role (LayoutFieldRole) | array of DisplayFieldConfig objects; role one of: title, subtitle, group\_by, sub\_group, date, end\_date, measure, thumbnail, badge, color, assignee, status, detail | Field visibility and rendering configuration for non-table layouts (kanban, calendar, gallery, chart, list, graph). Each entry declares what data appears on a card or tile, in what position, and with what display override. | | group\_by | jsonb (string\[] \| null) | ⚪ No | nullable; null means no grouping | array of dot-path field strings, or null | Field paths to group records by in the current view. An empty array and null are both treated as 'no grouping'. Used by the kanban and grouped-table modes. | | is\_default | boolean | ⚪ No | default false; no DB-level unique constraint — enforcement is at the service layer (WorkspaceViewService sets is\_default=false on sibling views when one is promoted) | true \| false | When true, this view is loaded automatically when a user navigates to the record root within the workspace. Only one view per (workspace, root) should be default at any given time; the service ensures this invariant on every PATCH that sets is\_default=true. | | created\_at | 🔒 system (timestamp) | ✅ Yes | set on create via onCreate hook; not nullable | ISO 8601 datetime | Timestamp of view creation. Set once by the MikroORM onCreate lifecycle hook; never updated. | | updated\_at | 🔒 system (timestamp) | ⚪ No | set on create and on every update via onCreate/onUpdate hooks; nullable in schema | ISO 8601 datetime | Timestamp of the last mutation. Refreshed on every PATCH by the MikroORM onUpdate lifecycle hook. Null only if the entity was never flushed after initial creation (should not occur in production). | | deleted\_at | 🔒 system (timestamp \| null) | ⚪ No | nullable; null = active row | ISO 8601 datetime or null | Soft-delete timestamp. Set when the user calls DELETE /workspaces/:id/views/:viewId. Active view queries filter deleted\_at IS NULL. A soft-deleted view is not accessible via the API but its data is preserved for audit and recovery. | ### Relationships | Name | Type | Required | Description | | --------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace | to-one (ManyToOne) | ✅ Yes | The workspace this view belongs to. Every view is workspace-scoped. Declared as @ManyToOne(() => Workspace) on WorkspaceView. Target: Workspace entity (core\_api.workspaces). FK is workspace\_pk (internal). Cross-workspace access is blocked by the workspace middleware on all /workspaces/:id/views endpoints. | ### System-computed * view\_id — generated by gen\_random\_uuid() at INSERT time; never supplied by the caller * created\_at — set by MikroORM onCreate() hook to new Date() at creation; immutable thereafter * updated\_at — set by MikroORM onCreate() hook at creation, then refreshed by onUpdate() hook on every flush after mutation * deleted\_at — written by WorkspaceViewService.deleteView() to perform a soft delete; null on active rows; never set by any pipeline or connector * is\_default sibling-reset — when a PATCH sets is\_default=true, WorkspaceViewService resets is\_default=false on all other views sharing the same (workspace, root) combination; this is a service-layer invariant, not a DB constraint ## Example ```json theme={null} { "data": { "type": "workspace_view", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "attributes": { "name": "Open Invoices — By Due Date", "root": "invoices", "columns": [ { "field": ["invoices", "reference_number"], "order": 0, "width": 160 }, { "field": ["invoices", "issuer", "name"], "order": 1, "width": 220 }, { "field": ["invoices", "due_date"], "order": 2, "width": 130 }, { "field": ["invoices", "grand_total"], "order": 3, "width": 120 } ], "sort": [ { "field": ["invoices", "due_date"], "direction": "asc" } ], "filters": [ { "field": ["invoices", "status"], "operator": "eq", "value": "pending", "conjunction": "and" } ], "layout_type": "table", "layout_config": { "rowHeight": "default" }, "display_fields": [], "group_by": null, "is_default": true, "created_at": "2026-03-15T09:42:00.000Z", "updated_at": "2026-05-10T14:22:33.000Z", "deleted_at": null }, "relationships": { "workspace": { "data": { "type": "workspace", "id": "f0e1d2c3-b4a5-6789-0123-456789abcdef" } } } } } ``` Source: `/Users/maximechampoux/platform/apps/api/src/database/entities/WorkspaceView.ts` · domain: workspace · tier: Infrastructure # Workspace Source: https://docs.wellapp.ai/object-reference/workspaces A Workspace is the top-level multi-tenant boundary in the Well platform A Workspace is the top-level multi-tenant boundary in the Well platform. It represents a single business entity's operational context and is the root scope for all domain resources — companies, invoices, transactions, memberships, connectors, webhooks, and accounting settings. Workspaces may form parent-child hierarchies (e.g. a holding company with subsidiary workspaces) and may belong to workspace groups for cross-workspace reporting. Every other resource in the platform carries a `workspace_id` foreign key, making the Workspace the primary security and tenancy primitive. | Naming | Value | | ------------------------------- | ---------------- | | Object | Workspace | | Resource type (JSON:API `type`) | `workspace` | | Collection / records root | `workspaces` | | REST base | `/v1/workspaces` | | Entity class | `Workspace` | **Read access today:** this object is readable via the universal `POST /v1/records/query` endpoint with `root: "workspaces"`. A dedicated `GET /v1/workspaces` endpoint is **Planned**. ## API operations | Operation | Method & path | Status | | --------- | ---------------------------- | -------------------------------------------------- | | List | `GET /v1/workspaces` | 🟡 Planned via `POST /v1/records/query` | | Retrieve | `GET /v1/workspaces/{id}` | ✅ Implemented | | Create | `POST /v1/workspaces` | ✅ Implemented | | Update | `PATCH /v1/workspaces/{id}` | ✅ Implemented | | Delete | `DELETE /v1/workspaces/{id}` | ✅ Implemented | ## Data model ### Attributes | Field | Type | Required | Constraints | Allowed values | Description | | ----------------------- | ------------------------------------- | -------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspace\_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen\_random\_uuid() on insert | — | Public-facing stable identifier for the workspace. Used as the resource address in all API URLs and multi-tenant scoping headers (X-Hasura-Workspace-Id). Never changes after creation. | | name | string | ✅ Yes | NOT NULL | — | Display name of the workspace, typically the business name (e.g. 'Acme SAS'). Used in the duplicate-check during workspace creation and as a fuzzy fallback for own\_company resolution when own\_company\_pk is NULL. | | description | string (text) | ⚪ No | nullable | — | Optional free-text description of the workspace. Surfaced in workspace settings and admin views. | | trusted | boolean | ⚪ No | nullable; default false | true \| false | Internal trust flag. When true, the workspace is treated as a verified / elevated-trust entity for platform-level operations (e.g. connector auto-provisioning). Not exposed to end users via standard workspace settings. | | avatar\_color | string | ⚪ No | nullable | — | Hex color code used to render the workspace avatar when no logo media is set. Chosen during workspace creation or updated in settings. | | external\_workspace\_id | string | ⚪ No | nullable; unique | — | Deduplication key for external system references (e.g. Stripe customer IDs, partner platform IDs). Unique constraint prevents double-linking. Used by the partner onboarding flow and external ID-based workspace resolution. | | timezone | string | ✅ Yes | columnType text; default 'UTC' | Any IANA timezone string (e.g. 'Europe/Paris', 'America/New\_York', 'UTC') | The workspace's canonical timezone used for date rendering, fiscal-period boundaries, report generation, and scheduler jobs. Defaults to UTC on creation; updated during onboarding or workspace settings. | | auto\_extract\_enabled | boolean | ✅ Yes | default true | true \| false | Controls whether the document auto-extraction pipeline (AI-powered invoice parsing) runs automatically for this workspace. Can be toggled by workspace owners in settings. Defaults to true on all new workspaces. | | enrichment\_config | object (JSONB) | ⚪ No | nullable; JSONB; shape: \{ auto\_enrich: boolean } | \{ auto\_enrich: true \| false } | JSONB configuration blob governing the enrichment pipeline behaviour for this workspace. auto\_enrich: true enables automatic company/person enrichment when new records are created. | | task\_config | object (JSONB) | ⚪ No | nullable; JSONB; shape: \{ max\_open\_tasks: number } | \{ max\_open\_tasks: integer >= 0 } | JSONB configuration blob for workspace task management. max\_open\_tasks caps how many concurrent open tasks the system will create for the workspace before requiring human triage. | | created\_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | set by onCreate lifecycle hook; NOT NULL | — | Timestamp of workspace creation. Set automatically on insert; never updated. | | updated\_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | set by onCreate and onUpdate lifecycle hooks; nullable | — | Timestamp of the most recent update to any workspace property. Managed automatically by the ORM lifecycle hook. | | deleted\_at | string (ISO 8601 datetime) | ⚪ No | nullable; soft-delete sentinel | null (active) \| ISO 8601 timestamp (soft-deleted) | Soft-delete timestamp. When set, the workspace and all its cascade-deleted resources are excluded from standard queries. Hard deletion never occurs; the row persists for audit and data-recovery purposes. | ### Relationships | Name | Type | Required | Description | | ------------------------------- | ---------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | media | to-one (media) | ⚪ No | Optional logo or avatar image for the workspace. References a Media entity stored in GCS. When null, the UI falls back to rendering avatar\_color with initials. | | parent\_workspace | to-one (workspace) | ⚪ No | Self-referential FK for workspace hierarchies. When set, this workspace is a child of the referenced workspace. A partial index idx\_workspaces\_parent covers rows where this is non-null. Parent-workspace admins automatically retain elevated access in child workspaces per the RBAC invariants. | | own\_company | to-one (company) | ⚪ No | The Company record representing the workspace's own legal entity — the business the workspace belongs to. Invoices issued to or from this company are treated as internal. Used by the provider scoring guard to avoid proposing connector tasks for the workspace's own company. Nullable; when null the guard is a no-op and Q1/Q6 counterparty discovery falls back to a name fuzzy-match. | | child\_workspaces | to-many (workspace) | — | Inverse collection of all Workspace records whose parent\_workspace points to this workspace. Represents the direct children in the workspace hierarchy. Used by the workspace scope resolver to assemble the descendants array for cross-workspace data views. | | memberships | to-many (membership) | — | All Membership records belonging to this workspace, spanning pending and active states. Drives RBAC role resolution: owner, admin, member, guest. Indexed by (workspace, membership\_role, deleted\_at) for hot-path admin lookup. | | webhooks | to-many (webhook) | — | All Webhook endpoint registrations scoped to this workspace. Webhooks fire on workspace-scoped domain events (invoice created, transaction synced, etc.). | | workspace\_accounting\_settings | to-one (workspace\_accounting\_settings) | ⚪ No | One-to-one relationship (inverse side) to the WorkspaceAccountingSettings entity, which carries base\_currency, accounting\_framework, fiscal\_year\_start\_month, tax\_id, registered name, tolerance bands for invoice payment status recompute, and default AR/AP ledger accounts. The owner side lives on WorkspaceAccountingSettings. | | workspace\_providers | to-many (workspace\_provider) | — | Junction records linking this workspace to the global Provider catalog entries. Tracks per-workspace provider selection (is\_selected) and match state (is\_matched) for the provider scoring / onboarding flow. | ### System-computed * workspace\_id is generated via gen\_random\_uuid() as a Postgres DEFAULT on insert; the application layer also sets randomUUID() as the JS default, so no manual assignment is needed at any layer. * created\_at is set by an ORM onCreate lifecycle hook to new Date() on first persist. Never updated. * updated\_at is set by both onCreate and onUpdate lifecycle hooks — it reflects the timestamp of the most recent ORM flush touching any column. * deleted\_at is null on active workspaces. Soft-deletion sets this field; every downstream query must filter deleted\_at IS NULL. Hard deletes never occur. * external\_workspace\_id carries a unique constraint and serves as a stable deduplication key for partner/external integrations. When provided during workspace creation, it prevents duplicate workspace provisioning for the same external customer. * parent\_workspace\_pk is covered by a partial index idx\_workspaces\_parent filtered to WHERE parent\_workspace\_pk IS NOT NULL. This index is non-trivial only when the hierarchy feature is in use and keeps the hot-path workspace lookup unaffected for flat tenants. * own\_company is nullable by design. When null, the provider scoring guard is a no-op and counterparty-bank discovery (Q1, Q6) falls back to fuzzy name matching against companies scoped to the workspace — never cross-workspace. * timezone defaults to 'UTC' at the database level and is updated during workspace onboarding; it governs all date/time rendering, scheduler triggers, and fiscal-period boundary calculations in the platform. * auto\_extract\_enabled defaults to true; the document extraction pipeline checks this flag before dispatching AI extraction tasks for newly uploaded documents. * enrichment\_config and task\_config are JSONB blobs with typed TypeScript interfaces (WorkspaceEnrichmentConfig, WorkspaceTaskConfig). They are nullable at the column level; consuming services must null-check before reading nested fields. * WorkspaceGroupWorkspace join rows reference workspaces by pk with a partial unique index (workspace\_group\_pk, workspace\_pk) WHERE deleted\_at IS NULL — soft-deleted join rows allow legal remove-then-re-add without unique-constraint collisions. ## Example ```json theme={null} { "data": { "type": "workspace", "id": "b3f2a1e0-4d7c-41aa-9f1b-0c8e3d2b5a6f", "attributes": { "workspace_id": "b3f2a1e0-4d7c-41aa-9f1b-0c8e3d2b5a6f", "name": "Acme SAS", "description": "Operating workspace for Acme SAS - European entity", "trusted": false, "avatar_color": "#3B82F6", "external_workspace_id": null, "timezone": "Europe/Paris", "auto_extract_enabled": true, "enrichment_config": { "auto_enrich": true }, "task_config": { "max_open_tasks": 50 }, "created_at": "2025-09-14T08:22:00.000Z", "updated_at": "2026-04-03T14:11:45.000Z", "deleted_at": null }, "relationships": { "media": { "data": { "type": "media", "id": "e7a91c30-1234-4bcd-8ef0-aabbcc112233" } }, "parent_workspace": { "data": null }, "own_company": { "data": { "type": "company", "id": "a1b2c3d4-5678-4abc-9def-000011112222" } }, "child_workspaces": { "data": [] }, "memberships": { "data": [ { "type": "membership", "id": "f0e1d2c3-b4a5-4678-9012-aabbccddeeff" } ] }, "webhooks": { "data": [] }, "workspace_accounting_settings": { "data": { "type": "workspace_accounting_settings", "id": "cc001122-dead-beef-cafe-112233445566" } }, "workspace_providers": { "data": [] } } } } ``` Source: `apps/api/src/database/entities/Workspace.ts` · domain: workspace · tier: Platform