> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Email

> 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`      |

<Note>
  **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**.
</Note>

## 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": []
      }
    }
  }
}
```

<sub>Source: `apps/api/src/database/entities/Email.ts` · domain: financial-graph · tier: Supporting</sub>
