Overview

Integration

User guide

API reference

Webhooks

AI Prompts

Pre-built prompts for integrating Postmark with AI-powered development tools

We've created a collection of prompts to help you integrate Postmark into your applications using AI-powered development tools like Cursor, GitHub Copilot, Windsurf, or Claude.

These prompts give AI assistants the context they need to help you implement common email workflows—from basic API integration to complex authentication flows—while following Postmark's best practices for reliability and deliverability.

How to use these prompts #

Copy the prompt Each guide below includes a complete prompt you can copy directly into your AI chat interface.

Paste into your AI tool Use these prompts with any AI assistant:

  • Cursor or GitHub Copilot: Include the prompt in your chat or use #<prompt> to reference
  • Claude or ChatGPT: Paste directly into the conversation
  • Windsurf or Zed: Add as project context using /file or similar commands

Get working code The AI will generate complete, functional code based on the prompt, including error handling, best practices, and proper Postmark API usage.

What you'll get #

Each prompt is designed to generate production-ready code that includes:

  • Complete integration setup with environment variable configuration
  • Proper error handling and retry logic
  • Security best practices (API token protection, input validation)
  • Appropriate use of message streams
  • Metadata for tracking and analytics

Note: AI can make mistakes! Always verify the output before using AI generated code.

Why use AI prompts with Postmark? #

Get started faster: Skip the documentation deep-dive and start sending emails in minutes.

Follow best practices: Prompts include Postmark's recommendations for deliverability, security, and reliability.

Avoid common mistakes: Generated code handles edge cases like bounce management, webhook validation, and rate limiting.

Learn as you build: AI explanations help you understand how the integration works, not just copy-paste code.


Integrate Postmark with Node.js/Express #

Send transactional email from your Node.js application using Postmark's official library.

What you'll build #

A Node.js/Express app that sends transactional emails through Postmark's API, handles delivery webhooks, and processes bounce notifications.

Prerequisites #

  • Node.js 14 or later
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token (found in your server's API Tokens tab)

Prompt #

Copy and paste this prompt into your preferred AI chat:

Build a Node.js/Express application that integrates with Postmark for transactional email. Include:

1. Installation and setup:
   - Install the postmark npm package
   - Configure the Postmark client with my Server API token
   - Set up environment variables securely

2. Send a welcome email:
   - Create an endpoint that sends a welcome email when a user signs up
   - Use Postmark's sendEmail method
   - Include the user's name and email address
   - Handle success and error responses

3. Handle delivery webhooks:
   - Create a POST endpoint to receive webhook data from Postmark
   - Parse bounce notifications
   - Parse spam complaint notifications
   - Log delivery events
   - Verify webhook authenticity (if applicable)

4. Error handling:
   - Handle network failures gracefully
   - Log Postmark API errors
   - Implement retry logic for failed sends

5. Best practices:
   - Use async/await for API calls
   - Validate email addresses before sending
   - Don't expose API tokens in client-side code
   - Set appropriate timeout values

Use TypeScript if possible. Include comments explaining key concepts.

Example response #

Your AI assistant should generate code similar to this structure:

1. Installation and configuration

// Install: npm install postmark dotenv express

import express from 'express';
import { ServerClient } from 'postmark';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
app.use(express.json());

// Initialize Postmark client
const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);

2. Send welcome email

app.post('/api/users/welcome', async (req, res) => {
  const { email, name } = req.body;

  try {
    const response = await client.sendEmail({
      From: 'welcome@yourdomain.com',
      To: email,
      Subject: 'Welcome to our app',
      TextBody: `Hi ${name}, thanks for signing up!`,
      HtmlBody: `<p>Hi ${name},</p><p>Thanks for signing up!</p>`,
      MessageStream: 'outbound'
    });

    res.json({ success: true, messageId: response.MessageID });
  } catch (error) {
    console.error('Email send failed:', error);
    res.status(500).json({ error: 'Failed to send email' });
  }
});

3. Handle webhooks

app.post('/webhooks/postmark', async (req, res) => {
  const webhook = req.body;

  // Log the webhook type
  console.log(`Received ${webhook.RecordType} webhook`);

  switch (webhook.RecordType) {
    case 'Bounce':
      // Handle bounce - update user record, pause sending
      console.log(`Bounce for ${webhook.Email}: ${webhook.Description}`);
      break;
    
    case 'SpamComplaint':
      // Handle spam complaint - unsubscribe user
      console.log(`Spam complaint from ${webhook.Email}`);
      break;
    
    case 'Delivery':
      // Email successfully delivered
      console.log(`Delivered to ${webhook.Recipient}`);
      break;
  }

  // Always respond with 200 to acknowledge receipt
  res.status(200).send('Webhook received');
});
```
  1. Set up your environment: Create a `.env` file with your Postmark Server API token: ```   POSTMARK_SERVER_TOKEN=your-server-token-here
  2. Test locally: Send a test email using your signup endpoint to verify the integration works.
  3. Configure webhooks: In your Postmark server settings, add your webhook URL (e.g., https://yourdomain.com/webhooks/postmark) and select which events to receive.
  4. Verify your sender: Make sure you've verified the From address in Postmark before sending production emails.

Next steps #

  • Use email templates instead of inline HTML for easier updates
  • Set up separate message streams for transactional vs. broadcast email
  • Track opens and clicks to understand engagement
  • Handle inbound email to build two-way communication

Related resources #


Add Postmark to a Rails application #

Configure ActionMailer to send transactional email through Postmark's SMTP or API integration.

What you'll build #

A Rails application that sends transactional emails using Postmark, with proper ActionMailer configuration, background job processing, and delivery handling.

Prerequisites #

  • Rails 6.0 or later
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token (found in your server's API Tokens tab)

Prompt #

Copy and paste this prompt into your preferred AI chat:

Build a Rails application that integrates with Postmark for transactional email. Include:

1. Installation and configuration:
   - Add the postmark-rails gem to the Gemfile
   - Configure ActionMailer to use Postmark in development and production
   - Set up environment variables for the Server API token
   - Configure delivery method (API vs SMTP)

2. Create a UserMailer:
   - Generate a mailer for user-related emails
   - Create a welcome email method
   - Create a password reset email method
   - Use Rails view templates for email content
   - Set from address, subject, and recipient

3. Send emails from controllers:
   - Send welcome email after user registration
   - Send password reset email when requested
   - Deliver emails in background using ActiveJob

4. Handle delivery failures:
   - Configure ActionMailer to raise delivery errors in development
   - Log failed deliveries
   - Set up error notification

5. Best practices:
   - Use Postmark message streams
   - Configure default from address
   - Set up email previews for testing
   - Use layout templates for consistent styling

Include both mailer code and view templates. Add comments explaining configuration options.

Example response #

Your AI assistant should generate code similar to this structure:

1. Installation and configuration

# Gemfile
gem 'postmark-rails'

# config/environments/production.rb
config.action_mailer.delivery_method = :postmark
config.action_mailer.postmark_settings = {
  api_token: ENV['POSTMARK_API_TOKEN']
}

# config/application.rb
config.action_mailer.default_url_options = { host: 'yourdomain.com' }
config.action_mailer.default_options = {
  from: 'noreply@yourdomain.com'
}

2. Create mailer

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def welcome_email(user)
    @user = user
    @login_url = login_url
    
    mail(
      to: email_address_with_name(@user.email, @user.name),
      subject: 'Welcome to our app',
      message_stream: 'outbound'
    )
  end

  def password_reset(user, reset_token)
    @user = user
    @reset_url = password_reset_url(token: reset_token)
    @expires_at = 1.hour.from_now
    
    mail(
      to: @user.email,
      subject: 'Reset your password',
      message_stream: 'outbound'
    )
  end
end

3. Email templates

<!-- app/views/user_mailer/welcome_email.html.erb -->
<!DOCTYPE html>
<html>
  <body>
    <h1>Welcome, <%= @user.name %>!</h1>
    <p>Thanks for signing up. We're excited to have you on board.</p>
    <p><a href="<%= @login_url %>">Log in to get started</a></p>
  </body>
</html>

<!-- app/views/user_mailer/welcome_email.text.erb -->
Welcome, <%= @user.name %>!

Thanks for signing up. We're excited to have you on board.

Log in to get started: <%= @login_url %>

4. Send from controller

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    
    if @user.save
      # Send welcome email in background
      UserMailer.welcome_email(@user).deliver_later
      
      redirect_to root_path, notice: 'Account created! Check your email.'
    else
      render :new
    end
  end
end

# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    
    if user
      reset_token = user.generate_password_reset_token
      UserMailer.password_reset(user, reset_token).deliver_later
    end
    
    # Always show success to prevent email enumeration
    redirect_to root_path, notice: 'If that email exists, we sent reset instructions.'
  end
end

What to do with this code #

  1. Install the gem: Run bundle install after adding postmark-rails to your Gemfile.
  2. Set your API token: Add your Postmark Server API token to your environment variables or credentials:
export POSTMARK_API_TOKEN=your-server-token-here

Test with previews: Create mailer previews to see your emails without sending:

# test/mailers/previews/user_mailer_preview.rb
   class UserMailerPreview < ActionMailer::Preview
     def welcome_email
       UserMailer.welcome_email(User.first)
     end
   end

Visit https://localhost:3000/rails/mailers to preview.

Verify your sender: Make sure you've verified your from address in Postmark before sending production emails.

Next steps #

  • Use Postmark templates for easier email management
  • Set up webhooks to handle bounces and spam complaints
  • Configure message streams to separate transactional and broadcast emails
  • Track opens and clicks to measure engagement

Related resources #


Implement Postmark in Laravel #

Configure Laravel Mail to send transactional email through Postmark's API.

What you'll build #

A Laravel application that sends transactional emails using Postmark, with proper mail driver configuration, Mailable classes, and queue integration.

Prerequisites #

  • Laravel 9.0 or later
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token (found in your server's API Tokens tab)

Prompt #

Copy and paste this prompt into your preferred AI chat:

Build a Laravel application that integrates with Postmark for transactional email. Include:

1. Installation and configuration:
   - Install the Postmark package for Laravel
   - Configure the Postmark mail driver in config files
   - Set up environment variables for the Server API token
   - Configure default from address and name

2. Create Mailable classes:
   - Generate a WelcomeEmail mailable
   - Generate a PasswordResetEmail mailable
   - Use markdown templates for email content
   - Pass data to email views
   - Set message stream for transactional emails

3. Send emails from controllers:
   - Send welcome email after user registration
   - Send password reset email when requested
   - Queue emails for background processing
   - Handle email sending in event listeners

4. Use Laravel Notifications:
   - Create a notification that sends via Postmark
   - Configure the mail channel
   - Send notification from a model

5. Best practices:
   - Use queues for email delivery
   - Configure retry logic for failed jobs
   - Set up email testing with Mailtrap or log driver in development
   - Use Postmark tags for email categorization

Include Mailable classes, markdown templates, and configuration files. Add comments explaining key concepts.

Example response #

Your AI assistant should generate code similar to this structure:

1. Installation and configuration

# Install Postmark driver
composer require wildbit/laravel-postmark
// config/mail.php

'mailers' => [

    'postmark' => [

        'transport' => 'postmark',

        // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),

        // 'client' => [

        //     'timeout' => 5,

        // ],

    ],

],



'from' => [

    'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),

    'name' => env('MAIL_FROM_NAME', 'Example'),

],



// .env

MAIL_MAILER=postmark

POSTMARK_TOKEN=your-server-token-here

MAIL_FROM_ADDRESS=noreply@yourdomain.com

MAIL_FROM_NAME="Your App Name"

2. Create Mailable

// app/Mail/WelcomeEmail.php
<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class WelcomeEmail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public User $user,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Welcome to our app',
            tags: ['welcome', 'onboarding'],
            metadata: [
                'user_id' => $this->user->id,
            ],
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.welcome',
        );
    }
}
// app/Mail/PasswordResetEmail.php
<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class PasswordResetEmail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public User $user,
        public string $resetUrl,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Reset your password',
            tags: ['password-reset'],
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.password-reset',
        );
    }
}

3. Email templates

{{-- resources/views/emails/welcome.blade.php --}}
@component('mail::message')
# Welcome, {{ $user->name }}!

Thanks for signing up. We're excited to have you on board.

@component('mail::button', ['url' => route('login')])
Log in to get started
@endcomponent

Thanks,<br>
{{ config('app.name') }}
@endcomponent
{{-- resources/views/emails/password-reset.blade.php --}}
@component('mail::message')
# Reset your password

Hi {{ $user->name }},

We received a request to reset your password. Click the button below to create a new password:

@component('mail::button', ['url' => $resetUrl])
Reset password
@endcomponent

This link will expire in 60 minutes.

If you didn't request a password reset, you can ignore this email.

Thanks,<br>
{{ config('app.name') }}
@endcomponent

4. Send from controller

// app/Http/Controllers/Auth/RegisterController.php
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:8|confirmed',
    ]);

    $user = User::create([
        'name' => $validated['name'],
        'email' => $validated['email'],
        'password' => Hash::make($validated['password']),
    ]);

    // Send welcome email in queue
    Mail::to($user)->queue(new WelcomeEmail($user));

    return redirect()->route('home')
        ->with('success', 'Account created! Check your email.');
}
// app/Http/Controllers/Auth/PasswordResetController.php
use App\Mail\PasswordResetEmail;
use Illuminate\Support\Facades\Password;

public function sendResetLink(Request $request)
{
    $request->validate(['email' => 'required|email']);

    $user = User::where('email', $request->email)->first();

    if ($user) {
        $token = Password::createToken($user);
        $resetUrl = route('password.reset', ['token' => $token, 'email' => $user->email]);
        
        Mail::to($user)->queue(new PasswordResetEmail($user, $resetUrl));
    }

    // Always return success to prevent email enumeration
    return back()->with('success', 'If that email exists, we sent reset instructions.');
}

5. Using Notifications

// app/Notifications/WelcomeNotification.php
<?php

namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class WelcomeNotification extends Notification
{
    public function via($notifiable): array
    {
        return ['mail'];
    }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Welcome to our app')
            ->greeting("Welcome, {$notifiable->name}!")
            ->line('Thanks for signing up. We're excited to have you on board.')
            ->action('Log in to get started', route('login'))
            ->tag('welcome')
            ->metadata('user_id', $notifiable->id);
    }
}

// Send from anywhere
$user->notify(new WelcomeNotification());

What to do with this code #

1. Install dependencies: Run composer install after adding the Postmark package.

2. Configure environment: Add your Postmark Server API token to .env:

POSTMARK_TOKEN=your-server-token-here

3. Set up queue worker: Since emails are queued, run a queue worker:

php artisan queue:work

4. Test locally: Use the log driver during development:

MAIL_MAILER=log

Emails will be written to storage/logs/laravel.log.

5. Verify your sender: Make sure you've verified your from address in Postmark before sending production emails.

Next steps #

  • Use Postmark templates instead of Blade templates for easier updates
  • Set up webhooks to handle bounces and spam complaints
  • Configure message streams to separate transactional and broadcast emails
  • Track opens and clicks to measure engagement

Related resources #


Build a password reset flow with Postmark #

Send secure password reset emails and handle the complete reset workflow in your application.

What you'll build #

A complete password reset system that generates secure tokens, sends reset emails through Postmark, validates reset requests, and updates user passwords.

Prerequisites #

  • A working authentication system with user accounts
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token
  • A database to store password reset tokens

Prompt (Node.js) #

Copy and paste this prompt into your preferred AI chat:

Build a secure password reset flow using Postmark for email delivery. Include:

1. Generate password reset tokens:
   - Create cryptographically secure random tokens
   - Store tokens in the database with expiration time (1 hour)
   - Associate tokens with user accounts
   - Hash tokens before storing

2. Send reset email via Postmark:
   - Create an endpoint that initiates password reset
   - Validate the user's email exists
   - Generate reset URL with token
   - Send email using Postmark's sendEmail method
   - Use a clear, actionable email template
   - Include security messaging (didn't request this? ignore it)

3. Validate reset tokens:
   - Create an endpoint to verify reset tokens
   - Check token exists and hasn't expired
   - Verify token hasn't been used already
   - Return appropriate error messages

4. Update password:
   - Create an endpoint to set new password
   - Validate token one final time
   - Hash new password securely (bcrypt or argon2)
   - Invalidate the reset token after use
   - Send confirmation email that password was changed

5. Security best practices:
   - Rate limit reset requests to prevent abuse
   - Don't reveal whether email exists in system
   - Use HTTPS for all reset URLs
   - Log all password reset attempts
   - Set tokens to expire after 1 hour
   - Allow only one active reset token per user

Use Node.js/Express with TypeScript. Include error handling and validation.

Example response #

Your AI assistant should generate code similar to this structure:

1. Generate and store reset tokens

import crypto from 'crypto';
import { ServerClient } from 'postmark';

const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);

// Generate secure token
function generateResetToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Store token in database
async function createResetToken(userId: string): Promise<string> {
  const token = generateResetToken();
  const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour

  // Invalidate any existing tokens for this user
  await db.passwordResets.deleteMany({ userId });

  // Store new token
  await db.passwordResets.create({
    userId,
    token: crypto.createHash('sha256').update(token).digest('hex'),
    expiresAt,
    used: false
  });

  return token;
}

2. Send reset email

app.post('/api/auth/forgot-password', async (req, res) => {
  const { email } = req.body;

  // Find user (don't reveal if user exists)
  const user = await db.users.findOne({ email });

  if (user) {
    const token = await createResetToken(user.id);
    const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;

    try {
      await client.sendEmail({
        From: 'support@yourdomain.com',
        To: email,
        Subject: 'Reset your password',
        HtmlBody: `
          <p>Hi ${user.name},</p>
          <p>You requested to reset your password. Click the link below to continue:</p>
          <p><a href="${resetUrl}">Reset my password</a></p>
          <p>This link expires in 1 hour.</p>
          <p>If you didn't request this, you can safely ignore this email.</p>
        `,
        TextBody: `Hi ${user.name}, you requested to reset your password. Visit this link: ${resetUrl} (expires in 1 hour)`,
        MessageStream: 'outbound'
      });
    } catch (error) {
      console.error('Failed to send reset email:', error);
    }
  }

  // Always return success to prevent email enumeration
  res.json({ 
    message: 'If that email exists, we sent a password reset link' 
  });
});

Validate reset token

app.get('/api/auth/validate-reset-token', async (req, res) => {
  const { token } = req.query;

  if (!token) {
    return res.status(400).json({ error: 'Token required' });
  }

  const hashedToken = crypto.createHash('sha256')
    .update(token as string)
    .digest('hex');

  const resetToken = await db.passwordResets.findOne({
    token: hashedToken,
    used: false,
    expiresAt: { $gt: new Date() }
  });

  if (!resetToken) {
    return res.status(400).json({ 
      error: 'Invalid or expired reset token' 
    });
  }

  res.json({ valid: true });
});

4. Reset password

import bcrypt from 'bcrypt';

app.post('/api/auth/reset-password', async (req, res) => {
  const { token, newPassword } = req.body;

  // Validate token
  const hashedToken = crypto.createHash('sha256')
    .update(token)
    .digest('hex');

  const resetToken = await db.passwordResets.findOne({
    token: hashedToken,
    used: false,
    expiresAt: { $gt: new Date() }
  });

  if (!resetToken) {
    return res.status(400).json({ error: 'Invalid or expired token' });
  }

  // Update password
  const hashedPassword = await bcrypt.hash(newPassword, 10);
  
  await db.users.updateOne(
    { id: resetToken.userId },
    { password: hashedPassword }
  );

  // Mark token as used
  await db.passwordResets.updateOne(
    { id: resetToken.id },
    { used: true }
  );

  // Send confirmation email
  const user = await db.users.findOne({ id: resetToken.userId });
  
  await client.sendEmail({
    From: 'support@yourdomain.com',
    To: user.email,
    Subject: 'Your password was changed',
    HtmlBody: `
      <p>Hi ${user.name},</p>
      <p>Your password was successfully changed.</p>
      <p>If you didn't make this change, contact support immediately.</p>
    `,
    MessageStream: 'outbound'
  });

  res.json({ message: 'Password reset successful' });
});

What to do with this code #

  1. Set up your database: Create a password_resets table with columns for userId, token, expiresAt, and used.
  2. Add rate limiting: Implement rate limiting on the forgot-password endpoint to prevent abuse (e.g., max 3 requests per email per hour).
  3. Create frontend forms: Build forms for requesting reset and entering new password.
  4. Test the flow: Use a test email address to verify the complete password reset experience.
  5. Configure security headers: Ensure your reset password page uses HTTPS and appropriate security headers.

Next steps #

  • Use email templates for consistent reset email styling
  • Track email opens to see if users receive reset emails
  • Handle bounces for invalid email addresses
  • Set up two-factor authentication for additional security

Related resources #


Set up email notifications for user actions #

Send automated transactional emails when users perform actions in your application.

What you'll build #

An event-driven notification system that sends emails when users complete key actions like signing up, making purchases, or updating settings.

Prerequisites #

  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token
  • An application with user actions to track
  • A way to trigger events (event emitter, message queue, or webhooks)

Prompt (Node.js/Express) #

Copy and paste this prompt into your preferred AI chat:

Build an email notification system using Postmark that sends emails based on user actions. Include:

1. Event system setup:
   - Create an event emitter or use a message queue (like Bull/BullMQ)
   - Define common user action events (signup, purchase, profile_update, etc.)
   - Create event handlers that trigger email sends
   - Handle async email sending to not block user actions

2. Notification email types:
   - Welcome email on user signup
   - Order confirmation after purchase
   - Profile update confirmation
   - Password changed notification
   - Account activity alert (new login from unfamiliar device)
   
3. Email sending logic:
   - Configure Postmark client
   - Create reusable email sending function
   - Pass dynamic user data to emails
   - Include user preferences (don't send if user opted out)
   - Handle email send failures gracefully

4. Template or inline content:
   - Show both approaches: using templates vs. inline HTML
   - Include personalization with user data
   - Make emails actionable with clear CTAs
   - Ensure mobile-responsive HTML

5. Logging and monitoring:
   - Log all notification events
   - Track email send success/failure
   - Store notification history per user
   - Handle retry logic for failed sends

6. User preferences:
   - Check notification preferences before sending
   - Allow users to opt out of certain notification types
   - Always send critical notifications (security alerts)

Use Node.js/Express with TypeScript. Use async/await for all email operations.

Example response #

Your AI assistant should generate code similar to this structure:

1. Event system setup

import EventEmitter from 'events';
import { ServerClient } from 'postmark';

const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);
const eventBus = new EventEmitter();

// Define event types
enum UserEvent {
  SIGNUP = 'user.signup',
  PURCHASE = 'user.purchase',
  PROFILE_UPDATE = 'user.profile_update',
  PASSWORD_CHANGE = 'user.password_change',
  NEW_LOGIN = 'user.new_login'
}

// Emit events from your application
app.post('/api/users/signup', async (req, res) => {
  const user = await createUser(req.body);
  
  // Emit event without blocking response
  eventBus.emit(UserEvent.SIGNUP, { user });
  
  res.json({ user });
});

2. Email notification handlers

// Welcome email handler
eventBus.on(UserEvent.SIGNUP, async ({ user }) => {
  try {
    await client.sendEmail({
      From: 'welcome@yourdomain.com',
      To: user.email,
      Subject: 'Welcome to our app',
      HtmlBody: `
        <h1>Welcome, ${user.name}!</h1>
        <p>Thanks for joining us. Here's how to get started:</p>
        <ul>
          <li>Complete your profile</li>
          <li>Explore our features</li>
          <li>Invite your team</li>
        </ul>
        <a href="${process.env.APP_URL}/getting-started">Get started</a>
      `,
      MessageStream: 'outbound'
    });
    
    console.log(`Welcome email sent to ${user.email}`);
  } catch (error) {
    console.error('Failed to send welcome email:', error);
  }
});

// Purchase confirmation handler
eventBus.on(UserEvent.PURCHASE, async ({ user, order }) => {
  // Check if user wants purchase notifications
  if (!user.preferences.notifications.purchases) {
    return;
  }

  try {
    await client.sendEmail({
      From: 'orders@yourdomain.com',
      To: user.email,
      Subject: `Order confirmation #${order.id}`,
      HtmlBody: `
        <h2>Thanks for your order!</h2>
        <p>Hi ${user.name},</p>
        <p>Your order #${order.id} is confirmed.</p>
        <h3>Order details:</h3>
        <ul>
          ${order.items.map(item => `
            <li>${item.name} - $${item.price}</li>
          `).join('')}
        </ul>
        <p><strong>Total: $${order.total}</strong></p>
        <a href="${process.env.APP_URL}/orders/${order.id}">View order</a>
      `,
      MessageStream: 'outbound',
      Metadata: {
        orderId: order.id,
        userId: user.id
      }
    });
  } catch (error) {
    console.error('Failed to send order confirmation:', error);
  }
});

// Security notification handler (always send)
eventBus.on(UserEvent.NEW_LOGIN, async ({ user, device, location }) => {
  try {
    await client.sendEmail({
      From: 'security@yourdomain.com',
      To: user.email,
      Subject: 'New login to your account',
      HtmlBody: `
        <h2>New sign-in detected</h2>
        <p>Hi ${user.name},</p>
        <p>Your account was accessed from a new device:</p>
        <ul>
          <li><strong>Device:</strong> ${device}</li>
          <li><strong>Location:</strong> ${location}</li>
          <li><strong>Time:</strong> ${new Date().toLocaleString()}</li>
        </ul>
        <p>If this wasn't you, <a href="${process.env.APP_URL}/security">secure your account</a> immediately.</p>
      `,
      MessageStream: 'outbound'
    });
  } catch (error) {
    console.error('Failed to send security notification:', error);
  }
});

3. Reusable notification function

interface NotificationData {
  userId: string;
  type: string;
  email: string;
  subject: string;
  htmlBody: string;
  metadata?: Record<string, string>;
}

async function sendNotification(data: NotificationData): Promise<void> {
  // Check user preferences
  const preferences = await getUserPreferences(data.userId);
  
  // Skip if user opted out (except critical notifications)
  if (!preferences.notifications[data.type] && data.type !== 'security') {
    console.log(`User ${data.userId} opted out of ${data.type} notifications`);
    return;
  }

  try {
    const response = await client.sendEmail({
      From: 'notifications@yourdomain.com',
      To: data.email,
      Subject: data.subject,
      HtmlBody: data.htmlBody,
      MessageStream: 'outbound',
      Metadata: {
        userId: data.userId,
        notificationType: data.type,
        ...data.metadata
      }
    });

    // Log successful send
    await logNotification({
      userId: data.userId,
      type: data.type,
      messageId: response.MessageID,
      status: 'sent'
    });

  } catch (error) {
    console.error('Notification send failed:', error);
    
    // Log failure and retry
    await logNotification({
      userId: data.userId,
      type: data.type,
      status: 'failed',
      error: error.message
    });
    
    // Add to retry queue
    await retryQueue.add({ data }, { delay: 60000 }); // retry in 1 minute
  }
}

4. Using templates instead of inline HTML

eventBus.on(UserEvent.SIGNUP, async ({ user }) => {
  try {
    await client.sendEmailWithTemplate({
      From: 'welcome@yourdomain.com',
      To: user.email,
      TemplateAlias: 'welcome',
      TemplateModel: {
        user_name: user.name,
        product_url: process.env.APP_URL,
        action_url: `${process.env.APP_URL}/getting-started`
      },
      MessageStream: 'outbound'
    });
  } catch (error) {
    console.error('Failed to send templated email:', error);
  }
});

What to do with this code #

  1. Choose your event system: Use EventEmitter for simple apps, or a message queue like BullMQ for production apps with high volume.
  2. Create notification preferences: Add a preferences table to your database where users can control which notifications they receive.
  3. Set up templates in Postmark: Create email templates for common notifications in your Postmark dashboard for easier updates.
  4. Test each notification type: Trigger each event type and verify emails send correctly with proper data.
  5. Monitor delivery: Use Postmark's activity feed to watch your notifications and identify any delivery issues.

Next steps #

  • Create email templates for consistent notification design
  • Use message streams to separate notification types
  • Set up webhooks to track delivery and engagement
  • Add batch sending for digest notifications

Related resources #


Handle inbound email with webhooks #

Receive and process incoming emails sent to your application's email addresses.

What you'll build #

An inbound email handler that receives emails via webhooks, parses message content, processes attachments, and routes messages to your application logic.

Prerequisites #

  • A Postmark account with an inbound stream configured
  • Your Postmark inbound webhook URL configured
  • A domain with MX records pointing to Postmark
  • A publicly accessible webhook endpoint (use ngrok for local development)

Prompt (Node.js/Express) #

Copy and paste this prompt into your preferred AI chat:

Build an inbound email handler using Postmark webhooks. Include:

1. Webhook endpoint setup:
   - Create POST endpoint to receive inbound email webhooks
   - Validate webhook authenticity (check for proper Postmark headers if needed)
   - Parse the incoming JSON payload
   - Return 200 status immediately to acknowledge receipt
   - Process email asynchronously

2. Parse email content:
   - Extract sender email and name
   - Get recipient address (To, Cc)
   - Parse subject line
   - Extract plain text body
   - Extract HTML body if present
   - Handle email threading (In-Reply-To, References headers)

3. Process attachments:
   - Iterate through attachments array
   - Decode base64 attachment content
   - Save attachments to file storage or S3
   - Extract attachment metadata (filename, content type, size)
   - Validate file types and sizes

4. Route emails to handlers:
   - Route based on recipient address (support@, help@, replies@)
   - Create support tickets from emails
   - Handle reply emails to link to existing conversations
   - Parse special commands in email body (like "unsubscribe")

5. Error handling and logging:
   - Log all inbound emails received
   - Handle malformed webhook data
   - Catch parsing errors gracefully
   - Store raw webhook data for debugging
   - Send error notifications if processing fails

6. Common use cases:
   - Create support ticket from email
   - Handle email replies to existing threads
   - Parse forms submitted via email
   - Extract structured data from email body

Use Node.js/Express with TypeScript. Include examples for saving attachments to disk and S3.

Example response #

Your AI assistant should generate code similar to this structure:

1. Webhook endpoint setup

import express from 'express';
import { InboundEmail } from './types';

app.post('/webhooks/inbound', express.json({ limit: '10mb' }), async (req, res) => {
  // Acknowledge receipt immediately
  res.status(200).send('OK');

  // Process email asynchronously
  try {
    const email: InboundEmail = req.body;
    await processInboundEmail(email);
  } catch (error) {
    console.error('Failed to process inbound email:', error);
    // Alert your team about processing failure
  }
});

interface InboundEmail {
  From: string;
  FromName: string;
  FromFull: { Email: string; Name: string };
  To: string;
  ToFull: Array<{ Email: string; Name: string }>;
  Cc: string;
  CcFull: Array<{ Email: string; Name: string }>;
  Subject: string;
  TextBody: string;
  HtmlBody: string;
  OriginalRecipient: string;
  ReplyTo: string;
  MailboxHash: string;
  MessageID: string;
  Date: string;
  Attachments: Array<{
    Name: string;
    Content: string; // base64
    ContentType: string;
    ContentLength: number;
    ContentID?: string;
  }>;
  Headers: Array<{ Name: string; Value: string }>;
}

2. Parse and route email

async function processInboundEmail(email: InboundEmail): Promise<void> {
  console.log(`Received email from ${email.From} to ${email.To}`);

  // Log the inbound email
  await db.inboundEmails.create({
    messageId: email.MessageID,
    from: email.From,
    to: email.To,
    subject: email.Subject,
    receivedAt: new Date(email.Date),
    rawData: email
  });

  // Route based on recipient
  const recipient = email.OriginalRecipient.toLowerCase();

  if (recipient.startsWith('support@')) {
    await handleSupportEmail(email);
  } else if (recipient.startsWith('replies@')) {
    await handleReplyEmail(email);
  } else if (recipient.startsWith('forms@')) {
    await handleFormEmail(email);
  } else {
    console.log(`No handler for recipient: ${recipient}`);
  }
}

3. Handle support ticket creation

async function handleSupportEmail(email: InboundEmail): Promise<void> {
  // Create support ticket
  const ticket = await db.tickets.create({
    email: email.From,
    name: email.FromName,
    subject: email.Subject,
    message: email.TextBody,
    status: 'open',
    createdAt: new Date()
  });

  // Process attachments
  if (email.Attachments && email.Attachments.length > 0) {
    for (const attachment of email.Attachments) {
      await saveAttachment(ticket.id, attachment);
    }
  }

  // Send confirmation email
  await client.sendEmail({
    From: 'support@yourdomain.com',
    To: email.From,
    Subject: `Re: ${email.Subject}`,
    HtmlBody: `
      <p>Hi ${email.FromName},</p>
      <p>We received your message and created ticket #${ticket.id}.</p>
      <p>We'll respond as soon as possible.</p>
      <p>— Support Team</p>
    `,
    MessageStream: 'outbound',
    Metadata: {
      ticketId: ticket.id.toString()
    }
  });

  console.log(`Created ticket #${ticket.id} from ${email.From}`);
}

4. Handle reply threading

async function handleReplyEmail(email: InboundEmail): Promise<void> {
  // Extract ticket ID from MailboxHash or subject line
  const ticketId = extractTicketId(email.MailboxHash || email.Subject);

  if (!ticketId) {
    console.log('Could not find ticket ID in reply email');
    return;
  }

  const ticket = await db.tickets.findOne({ id: ticketId });

  if (!ticket) {
    console.log(`Ticket #${ticketId} not found`);
    return;
  }

  // Add reply to ticket
  await db.ticketReplies.create({
    ticketId: ticket.id,
    from: email.From,
    message: email.TextBody,
    createdAt: new Date()
  });

  // Reopen ticket if it was closed
  if (ticket.status === 'closed') {
    await db.tickets.updateOne(
      { id: ticket.id },
      { status: 'open' }
    );
  }

  console.log(`Added reply to ticket #${ticket.id}`);
}

function extractTicketId(input: string): number | null {
  // Try to extract from mailbox hash (if you encode ticket ID)
  // Or parse from subject like "Re: [Ticket #123]"
  const match = input.match(/[#]?(\d+)/);
  return match ? parseInt(match[1]) : null;
}

5. Process and save attachments

import fs from 'fs/promises';
import path from 'path';

async function saveAttachment(
  ticketId: number,
  attachment: InboundEmail['Attachments'][0]
): Promise<void> {
  // Decode base64 content
  const buffer = Buffer.from(attachment.Content, 'base64');

  // Generate safe filename
  const safeFilename = attachment.Name.replace(/[^a-zA-Z0-9.-]/g, '_');
  const filepath = path.join(
    __dirname,
    '../uploads',
    `ticket-${ticketId}-${Date.now()}-${safeFilename}`
  );

  // Save to disk
  await fs.writeFile(filepath, buffer);

  // Store attachment record
  await db.attachments.create({
    ticketId,
    filename: attachment.Name,
    filepath,
    contentType: attachment.ContentType,
    size: attachment.ContentLength
  });

  console.log(`Saved attachment: ${attachment.Name} (${attachment.ContentLength} bytes)`);
}

// Alternative: Save to S3
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

async function saveAttachmentToS3(
  ticketId: number,
  attachment: InboundEmail['Attachments'][0]
): Promise<void> {
  const s3 = new S3Client({ region: 'us-east-1' });
  const buffer = Buffer.from(attachment.Content, 'base64');
  
  const key = `attachments/ticket-${ticketId}/${Date.now()}-${attachment.Name}`;

  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: buffer,
    ContentType: attachment.ContentType
  }));

  await db.attachments.create({
    ticketId,
    filename: attachment.Name,
    s3Key: key,
    contentType: attachment.ContentType,
    size: attachment.ContentLength
  });
}

6. Parse structured data from emails

async function handleFormEmail(email: InboundEmail): Promise<void> {
  // Parse form data from email body
  // Example: "Name: John Doe\nEmail: john@example.com\nMessage: Hello"
  
  const formData: Record<string, string> = {};
  const lines = email.TextBody.split('\n');

  for (const line of lines) {
    const match = line.match(/^([^:]+):\s*(.+)$/);
    if (match) {
      const [, key, value] = match;
      formData[key.trim().toLowerCase()] = value.trim();
    }
  }

  // Save form submission
  await db.formSubmissions.create({
    formType: 'contact',
    data: formData,
    submittedAt: new Date()
  });

  console.log('Processed form submission:', formData);
}

What to do with this code #

  1. Configure inbound domain: Set up MX records for your domain to point to Postmark's inbound servers (mx.inbound.postmarkapp.com).
  2. Set webhook URL: In your Postmark inbound stream settings, add your webhook endpoint URL.
  3. Test with ngrok: For local development, use ngrok to expose your local server: ngrok http 3000
  4. Create routing rules: In Postmark, set up rules to forward emails to specific addresses based on patterns.
  5. Handle security: Consider validating that webhooks come from Postmark by checking request headers or implementing webhook signatures.

Next steps #

  • Set up spam filtering to block unwanted inbound email
  • Use templates for automated reply emails
  • Track outbound replies to support conversations
  • Parse email metadata for advanced routing

Related resources #


Integrate with Better Auth #

Set up Postmark as your email provider for Better Auth authentication flows including email verification and password resets.

What you'll build #

A Better Auth integration that uses Postmark to send authentication emails including email verification links and password reset tokens.

Prerequisites #

  • A Next.js or Node.js application
  • Better Auth installed (npm install better-auth)
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token

Prompt #

Copy and paste this prompt into your preferred AI chat:

Integrate Postmark with Better Auth for authentication emails. Include:

1. Better Auth setup with Postmark:
   - Install required packages (better-auth, postmark)
   - Configure Better Auth with email and password authentication
   - Set up Postmark as the email provider
   - Configure environment variables

2. Email verification flow:
   - Implement sendVerificationEmail function using Postmark
   - Send verification emails on signup
   - Handle email verification callback
   - Optionally auto sign-in after verification
   - Create verification email template with clear CTA

3. Password reset flow:
   - Implement sendResetPassword function using Postmark
   - Trigger password reset from client
   - Send secure password reset emails via Postmark
   - Create password reset email template
   - Handle reset token validation

4. Email templates:
   - Design mobile-responsive email verification template
   - Design password reset template
   - Include security messaging
   - Use Better Auth's provided URL and token
   - Brand emails with your company identity

5. Client-side integration:
   - Set up Better Auth client
   - Handle signup with email verification
   - Trigger password reset flow
   - Handle verification callbacks
   - Display appropriate user feedback

6. Best practices:
   - Use environment variables for API tokens
   - Implement proper error handling
   - Add rate limiting for email sends
   - Log authentication events
   - Test email delivery in development

Use Next.js with TypeScript. Show both server-side Better Auth config and client-side React components.

Example response #

Your AI assistant should generate code similar to this structure:

1. Install dependencies

npm install better-auth postmark

2. Configure Better Auth with Postmark

// lib/auth.ts
import { betterAuth } from 'better-auth';
import { ServerClient } from 'postmark';

const postmark = new ServerClient(process.env.POSTMARK_SERVER_TOKEN!);

export const auth = betterAuth({
  database: {
    provider: 'postgresql',
    url: process.env.DATABASE_URL!
  },
  
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendResetPassword: async ({ user, url, token }, request) => {
      try {
        await postmark.sendEmail({
          From: 'auth@yourdomain.com',
          To: user.email,
          Subject: 'Reset your password',
          HtmlBody: `
            <!DOCTYPE html>
            <html>
              <body style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
                <h1>Reset your password</h1>
                <p>Hi ${user.name || 'there'},</p>
                <p>You requested to reset your password. Click the button below to continue:</p>
                <a href="${url}" style="display: inline-block; padding: 12px 30px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0;">
                  Reset password
                </a>
                <p>This link expires in 1 hour.</p>
                <p>If you didn't request this, you can safely ignore this email.</p>
                <p style="color: #666; font-size: 12px; margin-top: 30px;">
                  Or copy this link: ${url}
                </p>
              </body>
            </html>
          `,
          TextBody: `
            Reset your password
            
            Hi ${user.name || 'there'},
            
            You requested to reset your password. Visit this link to continue:
            ${url}
            
            This link expires in 1 hour.
            
            If you didn't request this, you can safely ignore this email.
          `,
          MessageStream: 'outbound',
          Metadata: {
            userId: user.id,
            emailType: 'password-reset'
          }
        });
        
        console.log(`Password reset email sent to ${user.email}`);
      } catch (error) {
        console.error('Failed to send password reset email:', error);
        throw error;
      }
    }
  },
  
  emailVerification: {
    sendOnSignUp: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      try {
        await postmark.sendEmail({
          From: 'auth@yourdomain.com',
          To: user.email,
          Subject: 'Verify your email address',
          HtmlBody: `
            <!DOCTYPE html>
            <html>
              <body style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
                <div style="background: #007bff; color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
                  <h1>Welcome to Your App!</h1>
                </div>
                <div style="padding: 30px; border: 1px solid #e0e0e0; border-top: none;">
                  <p>Hi ${user.name || 'there'},</p>
                  <p>Thanks for signing up! Please verify your email address to get started:</p>
                  <a href="${url}" style="display: inline-block; padding: 12px 30px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0;">
                    Verify email address
                  </a>
                  <p>This link expires in 24 hours.</p>
                  <p>If you didn't create an account, you can safely ignore this email.</p>
                  <p style="color: #666; font-size: 12px; margin-top: 30px;">
                    Or copy this link: ${url}
                  </p>
                </div>
              </body>
            </html>
          `,
          TextBody: `
            Welcome to Your App!
            
            Hi ${user.name || 'there'},
            
            Thanks for signing up! Please verify your email address to get started:
            ${url}
            
            This link expires in 24 hours.
            
            If you didn't create an account, you can safely ignore this email.
          `,
          MessageStream: 'outbound',
          Metadata: {
            userId: user.id,
            emailType: 'email-verification'
          }
        });
        
        console.log(`Verification email sent to ${user.email}`);
      } catch (error) {
        console.error('Failed to send verification email:', error);
        throw error;
      }
    },
    autoSignInAfterVerification: true
  }
});

3. Create API route handler

// app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';

export const { GET, POST } = toNextJsHandler(auth);

4. Set up client-side auth

// lib/auth-client.ts
import { createAuthClient } from 'better-auth/react';

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL || 'https://localhost:3000'
});

5. Signup component with email verification

// components/signup-form.tsx
'use client';

import { useState } from 'react';
import { authClient } from '@/lib/auth-client';

export function SignupForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);

  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    
    try {
      await authClient.signUp.email({
        email,
        password,
        name
      }, {
        onSuccess: () => {
          setSuccess(true);
        },
        onError: (ctx) => {
          setError(ctx.error.message);
        }
      });
    } catch (err) {
      setError('Signup failed. Please try again.');
    }
  };

  if (success) {
    return (
      <div className="p-4 bg-green-50 border border-green-200 rounded">
        <h3 className="font-semibold text-green-900">Check your email</h3>
        <p className="text-green-700 mt-2">
          We sent a verification link to {email}. Click the link to verify your account.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSignup} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          Name
        </label>
        <input
          id="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
          className="mt-1 block w-full rounded border px-3 py-2"
        />
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded border px-3 py-2"
        />
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          minLength={8}
          className="mt-1 block w-full rounded border px-3 py-2"
        />
      </div>

      {error && (
        <div className="p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
          {error}
        </div>
      )}

      <button
        type="submit"
        className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
      >
        Sign up
      </button>
    </form>
  );
}

6. Password reset component

// components/forgot-password-form.tsx
'use client';

import { useState } from 'react';
import { authClient } from '@/lib/auth-client';

export function ForgotPasswordForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    
    try {
      await authClient.forgetPassword({
        email,
        redirectTo: '/reset-password'
      }, {
        onSuccess: () => {
          setSuccess(true);
        },
        onError: (ctx) => {
          setError(ctx.error.message);
        }
      });
    } catch (err) {
      setError('Failed to send reset email. Please try again.');
    }
  };

  if (success) {
    return (
      <div className="p-4 bg-green-50 border border-green-200 rounded">
        <h3 className="font-semibold text-green-900">Check your email</h3>
        <p className="text-green-700 mt-2">
          If an account exists for {email}, we sent a password reset link.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email address
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded border px-3 py-2"
          placeholder="you@example.com"
        />
      </div>

      {error && (
        <div className="p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
          {error}
        </div>
      )}

      <button
        type="submit"
        className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
      >
        Send reset link
      </button>
    </form>
  );
}

7. Environment variables

# .env
POSTMARK_SERVER_TOKEN=your-server-token-here
DATABASE_URL=your-database-url
BETTER_AUTH_SECRET=your-secret-key
NEXT_PUBLIC_APP_URL=https://localhost:3000

What to do with this code #

  1. Set up your database: Better Auth requires a database to store user data and tokens. Follow Better Auth's database setup guide.
  2. Verify sender in Postmark: Make sure the From address in your emails is verified in your Postmark account.
  3. Test email delivery: Sign up with a test account and verify you receive both verification and password reset emails.
  4. Customize email design: Update the HTML templates to match your brand identity and design system.
  5. Set up proper error handling: Add logging and monitoring for authentication failures and email delivery issues.

Next steps #

  • Add social login with other Better Auth providers
  • Implement two-factor authentication for enhanced security
  • Use Postmark templates for easier email management
  • Track email engagement with opens and clicks

Related resources #