---
title: "Email Module"
url: "https://developer.incode.com/sdk-reference/web-sdk-2-module-email/"
section: "sdk-reference"
group: "Incode Web SDK 2 Reference / Web SDK 2 Individual Modules"
version: "v1.1"
status: "live"
---
# Email Module

:::note
This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation [here](/sdk-reference/web-sdk-reference).  Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade. <br /><br />Full rollout to all clients still TBD.
:::

The Email module verifies a user's email address via OTP (One-Time Password).

> Follows the [form-based pattern](/sdk-reference/web-sdk-2-module-patterns/#1-form-based-modules) (with the OTP sub-loop). See that page for the shared manager lifecycle and skeleton; the rest of this page covers Email-specific config, states, and methods.

## Tag

`<incode-email>` is a standard Web Component. Importing the UI subpath registers the custom element; importing the CSS applies the module's styles.

```ts
import '@incodetech/web/email';
import '@incodetech/web/email/styles.css';
```

## Properties

Set these as JavaScript properties on the element (not as HTML attributes):

| Property   | Type                      | Required | Description                        |
| ---------- | ------------------------- | -------- | ---------------------------------- |
| `config`   | `EmailConfig`             | ✅       | Configuration options              |
| `onFinish` | `() => void`              | ❌       | Called when verification completes |
| `onError`  | `(error: string) => void` | ❌       | Called when an error occurs        |

## Usage

### Vanilla HTML / TypeScript

```html
<incode-email></incode-email>

<script type="module">
  import { setup } from '@incodetech/core';
  import '@incodetech/web/email';
  import '@incodetech/web/email/styles.css';

  await setup({
    apiURL: 'https://demo-api.incodesmile.com',
    token: 'your-session-token',
  });

  const email = document.querySelector('incode-email');
  email.config = {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  };
  email.onFinish = () => console.log('Email verified!');
  email.onError = (err) => console.error('Email error:', err);
</script>
```

### React

> **React 18 or earlier:** add the one-time JSX augmentation from [Framework Integration → TypeScript: JSX support for `incode-*` tags](/sdk-reference/web-sdk-2-framework-integration/#typescript-jsx-support-for-incode--tags). React 19+ doesn't need it, and can also use the simpler form from [Framework Integration → React 19+ shortcut](/sdk-reference/web-sdk-2-framework-integration/#react-19-shortcut).

```tsx
import { useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import type { EmailConfig } from '@incodetech/core/email';
import '@incodetech/web/email';
import '@incodetech/web/email/styles.css';

type EmailElement = HTMLElement & {
  config: EmailConfig;
  onFinish: () => void;
  onError: (error: string) => void;
};

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
});

export function EmailVerification() {
  const ref = useRef<EmailElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.config = {
      otpVerification: true,
      otpExpirationInMinutes: 5,
      prefill: false,
    };
    el.onFinish = () => console.log('Email verified!');
    el.onError = (err) => console.error('Email error:', err);
  }, []);

  return <incode-email ref={ref} />;
}
```

For Angular (`CUSTOM_ELEMENTS_SCHEMA`) and Vue (`compilerOptions.isCustomElement`) setup, see [Framework Integration](/sdk-reference/web-sdk-2-framework-integration/).

---

## Headless Mode

For complete UI control, use the `createEmailManager` from `@incodetech/core/email`.

### Quick Start

```tsx
import { setup } from '@incodetech/core';
import { createEmailManager } from '@incodetech/core/email';

// Initialize SDK first
await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
});

const manager = createEmailManager({
  config: {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  },
});

manager.subscribe((state) => {
  console.log('Status:', state.status);

  if (state.status === 'finished') {
    console.log('Email verified!');
    manager.stop();
  }
});

// Start the flow
manager.load();

// When state is 'inputting', set the email and submit
manager.setEmail('user@example.com', true);
manager.submit();

// When state is 'awaitingOtp', submit the OTP code
manager.submitOtp('ABC123');
```

### State Machine Flow

```mermaid
flowchart LR
    idle -->|load| inputting
    inputting -->|submit| awaitingOtp
    awaitingOtp -->|submitOtp| finished
    awaitingOtp -.->|back| inputting
    awaitingOtp -.->|resendOtp| awaitingOtp
```

### States Reference

| Status              | Description                                   | Key Properties                                              |
| ------------------- | --------------------------------------------- | ----------------------------------------------------------- |
| `idle`              | Initial state, waiting for `load()`           | –                                                           |
| `loadingPrefill`    | Fetching pre-filled email from backend        | –                                                           |
| `inputting`         | Ready for email input                         | `prefilledEmail?`, `emailError?`, `validationErrors?`, `isValid`, `otpVerification?` |
| `submitting`        | Submitting email to backend                   | –                                                           |
| `sendingInitialOtp` | Sending OTP code via email for the first time | –                                                           |
| `resendingOtp`      | Resending OTP code via email                  | `otpCode`, `otpLength`                                      |
| `awaitingOtp`       | Waiting for user to enter OTP                 | `resendTimer`, `canResend`, `attemptsRemaining`, `otpCode`, `otpLength`, `otpValidationErrors?`, `isOtpValid` |
| `verifyingOtp`      | Verifying OTP code with backend               | `resendTimer`, `canResend`, `otpCode`, `otpLength`           |
| `otpError`          | OTP verification failed                       | `otpError`, `attemptsRemaining`, `resendTimer`, `canResend`, `otpCode`, `otpLength`, `otpValidationErrors?`, `isOtpValid` |
| `finished`          | Verification complete                         | –                                                           |
| `error`             | Fatal error occurred                          | `error`                                                     |

### State Properties

**When `status === 'inputting'`:**

| Property           | Type                     | Description                                                                          |
| ------------------ | ------------------------ | ------------------------------------------------------------------------------------- |
| `prefilledEmail`   | `string?`                | Pre-populated email address (if `prefill: true`)                                     |
| `emailError`       | `string?`                | Submission error message if the backend rejected the email address                    |
| `validationErrors` | `EmailValidationErrors?` | Client-side validation errors (e.g. invalid/empty email), surfaced on blur and submit |
| `isValid`          | `boolean`                | True iff `validationErrors` is empty. Optimistic — starts `true` on initial render.    |

**When `status === 'awaitingOtp'`, `'verifyingOtp'`, `'otpError'`, or `'resendingOtp'`:**

| Property              | Type                   | Description                                                                             |
| --------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
| `otpCode`             | `string`               | The OTP code entered so far (controlled-input value)                                    |
| `otpLength`           | `number`               | Expected OTP length (owned by Core — don't hardcode it)                                 |
| `resendTimer`         | `number`               | Seconds remaining before resend allowed (`awaitingOtp`, `verifyingOtp`, `otpError`)      |
| `canResend`           | `boolean`              | Whether resend button should be enabled (`awaitingOtp`, `verifyingOtp`, `otpError`)      |
| `attemptsRemaining`   | `number`               | OTP verification attempts left (`awaitingOtp`, `otpError`)                               |
| `otpValidationErrors` | `OtpValidationErrors?` | Client-side OTP errors (e.g. incomplete code), surfaced on blur and submit (`awaitingOtp`, `otpError`) |
| `isOtpValid`          | `boolean`              | True iff `otpValidationErrors` is empty (`awaitingOtp`, `otpError`)                       |
| `otpError`            | `string`               | Error message describing failure (`otpError` only)                                       |

### API Methods

| Method                     | Description                             | When to Use                           |
| -------------------------- | --------------------------------------- | ------------------------------------- |
| `load()`                   | Initializes the flow                    | Always call first                     |
| `setEmail(email, isValid)` | Sets email address and validation state | When `inputting`, before `submit()`   |
| `validateEmail()`          | Runs blur-style validation, populating or clearing `validationErrors.email` | When `inputting` |
| `submit()`                 | Submits the email address               | After setting valid email             |
| `setOtpCode(code)`         | Sets OTP without submitting             | When `awaitingOtp` (controlled input) |
| `validateOtp()`            | Runs blur-style OTP validation, populating or clearing `otpValidationErrors.otp` | When `awaitingOtp` or `otpError` |
| `submitOtp(code)`          | Sets and submits OTP code               | When `awaitingOtp` or `otpError`      |
| `resendOtp()`              | Requests new OTP code                   | When `canResend` is `true`            |
| `back()`                   | Returns to email input                  | When `awaitingOtp`                    |
| `reset()`                  | Resets to initial state                 | After `finished` or `error`           |
| `stop()`                   | Cleanup resources                       | When unmounting                       |
| `getState()`               | Returns current state synchronously     | Anytime                               |
| `subscribe(callback)`      | Subscribe to state changes              | Returns unsubscribe function          |

### React Example

```tsx
import { useState, useEffect } from 'react';
import { createEmailManager, type EmailState } from '@incodetech/core/email';

function CustomEmailVerification() {
  const [manager] = useState(() =>
    createEmailManager({
      config: {
        otpVerification: true,
        otpExpirationInMinutes: 5,
        prefill: false,
      },
    }),
  );
  const [state, setState] = useState<EmailState>({ status: 'idle' });
  const [email, setEmail] = useState('');
  const [otp, setOtp] = useState('');

  useEffect(() => {
    const unsubscribe = manager.subscribe(setState);
    manager.load();
    return () => {
      unsubscribe();
      manager.stop();
    };
  }, [manager]);

  const handleSubmitEmail = () => {
    // Simple email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const isValid = emailRegex.test(email);
    manager.setEmail(email, isValid);
    manager.submit();
  };

  switch (state.status) {
    case 'inputting':
      return (
        <div>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Enter email address"
          />
          {state.emailError && <p className="error">{state.emailError}</p>}
          <button onClick={handleSubmitEmail}>Send OTP</button>
        </div>
      );

    case 'submitting':
    case 'sendingInitialOtp':
    case 'resendingOtp':
      return <div>Sending OTP...</div>;

    case 'awaitingOtp':
      return (
        <div>
          <p>Enter the 6-character code sent to your email</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value.toUpperCase())}
            placeholder="ABC123"
            maxLength={6}
          />
          <button onClick={() => manager.submitOtp(otp)}>Verify</button>

          {state.canResend ? (
            <button onClick={() => manager.resendOtp()}>Resend Code</button>
          ) : (
            <p>Resend available in {state.resendTimer}s</p>
          )}

          <button onClick={() => manager.back()}>Change Email</button>
        </div>
      );

    case 'otpError':
      return (
        <div>
          <p className="error">{state.otpError}</p>
          <p>Attempts remaining: {state.attemptsRemaining}</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value.toUpperCase())}
            maxLength={6}
          />
          <button onClick={() => manager.submitOtp(otp)}>Try Again</button>
        </div>
      );

    case 'verifyingOtp':
      return <div>Verifying...</div>;

    case 'finished':
      return <div>Email verified successfully!</div>;

    case 'error':
      return <div className="error">Error: {state.error}</div>;

    default:
      return <div>Loading...</div>;
  }
}
```

---

## Configuration Options

`EmailConfig` shape:

| Option                   | Type      | Required | Description                                                                                            |
| ------------------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `otpVerification`        | `boolean` | ✅       | Require OTP (email code) verification. If `false`, the email is verified immediately after submission. |
| `otpExpirationInMinutes` | `number`  | ✅       | How long the OTP code remains valid. After expiration the user must request a new code.                |
| `prefill`                | `boolean` | ❌       | Pre-populate with the user's previously stored email. Useful for returning users. Default `false`.     |
| `maxOtpAttempts`         | `number`  | ❌       | Maximum OTP verification attempts before lockout. Default `3`.                                         |

## Troubleshooting

### OTP Not Received

- Check the spam/junk folder
- Verify the email address is correct
- Some corporate email filters may block OTP emails
- Add the sender to your contacts/allowlist

### Invalid OTP Error

- OTPs expire after the configured time (default 5 minutes)
- Each OTP can only be used once
- Request a new OTP if the current one expires
- Check `attemptsRemaining` before lockout

### Email Address Rejected

- Ensure the email format is valid
- Some disposable email domains may be blocked
- Corporate email addresses are generally accepted

---

## See Also

- [Headless Mode](/sdk-reference/web-sdk-2-headless-mode/): Complete headless API reference
- [Individual Modules](/sdk-reference/web-sdk-2-individual-modules/): Overview of all modules