# OTP API

> Generate and verify one-time passwords for secure operations in MakerKit. Use the OTP API for account deletion, ownership transfers, and other high-risk actions.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/api/otp-api*

---

The OTP API generates and verifies one-time passwords for secure operations like account deletion, ownership transfers, and email verification. It uses Supabase for secure token storage with automatic expiration and verification tracking.

{% sequence title="How to use the OTP API" description="Learn how to use the OTP API in Makerkit" %}

[OTP API - What is it for?](#otp-api---what-is-it-for)

[Installation](#installation)

[Basic Usage](#basic-usage)

[Sending OTPs from a Server Function](#sending-otps-from-a-server-function)

[Verification UI Component](#verification-ui-component)

[API Reference](#api-reference)

[Database Schema](#database-schema)

[Best Practices](#best-practices)

[Example Use Cases](#example-use-cases)

{% /sequence %}

It is used for various destructive actions in the SaaS Kit, such as deleting
accounts, deleting teams, and deleting users. However, you can use it for a
variety of other purposes as well, such as:

- Your custom destructive actions
- oAuth account connections
- etc.

## OTP API - What is it for?

The OTP package offers:

- **Secure Token Generation**: Create time-limited tokens with configurable expiration
- **Email Delivery**: Send OTP codes via email with customizable templates
- **Verification UI**: Ready-to-use verification form component
- **Token Management**: Revoke, verify, and check token status

## Installation

If you're using Makerkit, this package is already included. For manual installation:

```bash
pnpm add @kit/otp
```

## Basic Usage

### Creating and Sending an OTP

To create and send an OTP, you can use the `createToken` method:

```typescript
import { createOtpApi } from '@kit/otp/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

// Create the API instance
const client = getSupabaseServerClient();
const api = createOtpApi(client);

// Generate and send an OTP email
await api.createToken({
  userId: user.id,
  purpose: 'email-verification',
  expiresInSeconds: 3600, // 1 hour
  metadata: { redirectTo: '/verify-email' }
});

// Send the email with the OTP
await api.sendOtpEmail({
  email: userEmail,
  otp: token.token
});
```

### Verifying an OTP

To verify an OTP, you can use the `verifyToken` method:

```typescript
// Verify the token
const result = await api.verifyToken({
  token: submittedToken,
  purpose: 'email-verification'
});

if (result.valid) {
  // Token is valid, proceed with the operation
  const { userId, metadata } = result;
  // Handle successful verification
} else {
  // Token is invalid or expired
  // Handle verification failure
}
```

## Sending OTPs from a Server Function

The built-in `VerifyOtpForm` component sends OTP emails for you (via an internal auth-gated server function), so most flows never need to send OTPs manually. When you do need a custom send flow, wrap the OTP API in your own server function that runs behind `authFunctionMiddleware`:

```typescript
import { createServerFn } from '@tanstack/react-start';
import * as z from 'zod';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { createOtpApi } from '@kit/otp/api';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';

const SendResetOtpSchema = z.object({
  email: z.string().email(),
});

export const sendResetOtpFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(SendResetOtpSchema)
  .handler(async ({ context: { user } }) => {
    const api = createOtpApi(getSupabaseServerAdminClient());

    const token = await api.createToken({
      userId: user.id,
      purpose: 'password-reset',
      expiresInSeconds: 1800, // 30 minutes
    });

    // OTP emails are always sent to the address on the user's account
    await api.sendOtpEmail({ email: user.email!, otp: token.token });

    return { success: true };
  });
```

Call it from the client with `useServerFn(sendResetOtpFunction)` inside a TanStack Query mutation.

**Note:** OTP emails are always sent to the email address associated with the authenticated user, never to an arbitrary address supplied by the client.

## Verification UI Component

The package includes a ready-to-use OTP verification form:

```tsx
import { VerifyOtpForm } from '@kit/otp/components';

function MyVerificationPage() {
  return (
    <VerifyOtpForm
      purpose="password-reset"
      email={userEmail}
      onSuccess={(otp) => {
        // Handle successful verification
        // Use the OTP for verification on the server
      }}
      CancelButton={
        <Button variant="outline" onClick={handleCancel}>
          Cancel
        </Button>
      }
    />
  );
}
```

## API Reference

### `createOtpApi(client)`

Creates an instance of the OTP API.

**Parameters**:
- `client`: A Supabase client instance
- **Returns**: OTP API instance with the following methods:

### `api.createToken(params)`

Creates a new one-time token.

**Parameters**:
- `params.userId` (optional): User ID to associate with the token
- `params.purpose`: Purpose of the token (e.g., 'password-reset')
- `params.expiresInSeconds` (optional): Token expiration time in seconds (default: 3600)
- `params.metadata` (optional): Additional data to store with the token
- `params.description` (optional): Description of the token
- `params.tags` (optional): Array of string tags
- `params.scopes` (optional): Array of permission scopes
- `params.revokePrevious` (optional): Whether to revoke previous tokens with the same purpose (default: true)

**Returns**:
```typescript
{
  id: string;           // Database ID of the token
  token: string;        // The actual token to send to the user
  expiresAt: string;    // Expiration timestamp
  revokedPreviousCount: number; // Number of previously revoked tokens
}
  ```

### `api.verifyToken(params)`

Verifies a one-time token.

**Parameters**:
- `params.token`: The token to verify
- `params.purpose`: Purpose of the token (must match the purpose used when creating)
- `params.userId` (optional): User ID for additional verification
- `params.requiredScopes` (optional): Array of required permission scopes
- `params.maxVerificationAttempts` (optional): Maximum allowed verification attempts

**Returns**:
```typescript
{
  valid: boolean;       // Whether the token is valid
  userId?: string;      // User ID associated with the token (if valid)
  metadata?: object;    // Metadata associated with the token (if valid)
  message?: string;     // Error message (if invalid)
  scopes?: string[];    // Permission scopes (if valid)
  purpose?: string;     // Token purpose (if valid)
}
  ```

### `api.revokeToken(params)`

Revokes a token to prevent its future use.

**Parameters**:
- `params.id`: ID of the token to revoke
- `params.reason` (optional): Reason for revocation

**Returns**:
```typescript
{
  success: boolean;     // Whether the token was successfully revoked
}
  ```

### `api.getTokenStatus(params)`

Gets the status of a token.

**Parameters**:
- `params.id`: ID of the token

**Returns**:
```typescript
{
  exists: boolean;      // Whether the token exists
  purpose?: string;     // Token purpose
  userId?: string;      // User ID associated with the token
  createdAt?: string;   // Creation timestamp
  expiresAt?: string;   // Expiration timestamp
  usedAt?: string;      // When the token was used (if used)
  revoked?: boolean;    // Whether the token is revoked
  revokedReason?: string; // Reason for revocation (if revoked)
  verificationAttempts?: number; // Number of verification attempts
  lastVerificationAt?: string;  // Last verification attempt timestamp
  lastVerificationIp?: string;  // IP address of last verification attempt
  isValid?: boolean;    // Whether the token is still valid
}
  ```

### `api.sendOtpEmail(params)`

Sends an email containing the OTP code.

**Parameters**:
- `params.email`: Email address to send to
- `params.otp`: OTP code to include in the email

**Returns**: Promise that resolves when the email is sent

## Database Schema

The package uses a `nonces` table in your Supabase database with the following structure:

- `id`: UUID primary key
- `client_token`: Hashed token sent to client
- `nonce`: Securely stored token hash
- `user_id`: Optional reference to auth.users
- `purpose`: Purpose identifier (e.g., 'password-reset')
- Status fields: `expires_at`, `created_at`, `used_at`, etc.
- Audit fields: `verification_attempts`, `last_verification_at`, etc.
- Extensibility fields: `metadata`, `scopes`

## Best Practices

1. **Use Specific Purposes**: Always use descriptive, specific purpose identifiers for your tokens.
2. **Short Expiration Times**: Set token expiration times to the minimum necessary for your use case.
3. **Handle Verification Failures**: Provide clear error messages when verification fails.
4. **Secure Your Tokens**: Never log or expose tokens in client-side code or URLs.

## Example Use Cases

- Email verification
- Two-factor authentication
- Account deletion confirmation
- Important action verification

Each use case should use a distinct purpose identifier. The purpose will
always need to match the one used when creating the token.

When you need to assign a specific data to a token, you can modify the
purpose with a unique identifier, such as `email-verification-12345`.

## Related documentation

- [Authentication API](/docs/tanstack-supabase/api/authentication-api) - User authentication and session handling
- [Team Account API](/docs/tanstack-supabase/api/team-account-api) - Team management for ownership transfers
- [Email Configuration](/docs/tanstack-supabase/emails/email-configuration) - Configure email delivery for OTP codes
- [Writing Data to the Database](/docs/tanstack-supabase/development/writing-data-to-database) - Use OTP verification in server functions
