# 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
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
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
- External business contact
- Client or customer representative
- Vendor or supplier contact
- General networking connection
employee
Company employee
- Full-time or part-time staff
- Contractor or freelancer
- Temporary worker
- Intern or trainee
owner
Business owner or principal
- Company founder or co-founder
- Business owner or partner
- Shareholder or investor
- Principal or managing director
other
Other relationship types
- Board member or advisor
- Consultant or specialist
- Legal or financial representative
- Any other business relationship
## 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
- Manage users and permissions
- Configure workspace settings
- Access all workspace content
- Manage integrations and billing
member
Standard user access
- Create and edit own content
- Collaborate on shared projects
- View workspace content
- Limited administrative actions
owner
Ultimate ownership permissions
- All admin rights
- Delete workspace
- Transfer ownership
- Billing and subscription management
guest
Limited read-only access
- View shared content only
- Read-only permissions
- External collaborator access
- Time-limited access (optional)
## 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