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 TypeScript or Node.js application using Postmark's official library. For production use, templates are the recommended path — they keep your email layouts separate from your application code and give Postmark the context it needs to optimize delivery. The prompt below uses sendEmailWithTemplate as the default and includes sendEmail for cases where you need full control over the HTML.

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
   - Import ServerClient from postmark using ESM syntax (import, not require)
   - Configure the Postmark client with POSTMARK_SERVER_TOKEN from environment variables
   - Export the client as a shared instance

2. Send a welcome email using a template:
   - Create a function that sends a welcome email using sendEmailWithTemplate
   - Use TemplateAlias "welcome" and pass name in the TemplateModel
   - Set MessageStream to "outbound"
   - Return the MessageID from the response

3. Send a plain email without a template (fallback):
   - Create an endpoint that sends a basic email using sendEmail
   - Use this only when you need full control over the HTML body
   - Include HtmlBody and TextBody

4. Handle delivery webhooks:
   - Create a POST endpoint to receive webhook events from Postmark
   - Use express.raw() to read the raw body — do not use express.json() for this route
   - Parse RecordType and handle Delivery and Bounce events
   - Always respond 200 to acknowledge receipt

5. Error handling:
   - Handle network failures and Postmark API errors
   - Use async/await throughout
   - Log the MessageID on successful send

Use TypeScript if possible. Prefer sendEmailWithTemplate over inline HTML for any email where the layout is reusable.

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
  • Use the TypeScript integration guide → for a complete implementation including batch sends, webhook verification, and an agent repair guide

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


Send batch notifications with failure handling #

Send batch notifications with failure handling

Send a batch of transactional emails to multiple recipients in a single operation — with per-recipient success and failure reporting so partial failures don't get silently swallowed.

What you'll build

A TypeScript function that sends templated emails to an array of recipients using Promise.allSettled, returning a structured result per recipient that includes the MessageID for successful sends and the error reason for failures.

Prerequisites

  • Node.js 18+ and a TypeScript project
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token
  • A template in your Postmark account (any alias works — substitute your own)

Prompt

Copy and paste this prompt into your preferred AI chat:

Add batch email sending to my TypeScript project using Postmark. Include:

1. Shared client:
   - Import ServerClient from postmark
   - Configure with POSTMARK_SERVER_TOKEN from environment variables

2. Batch send function:
   - Accept an array of recipients, each with email and name fields
   - Send a templated email to each recipient using sendEmailWithTemplate
   - Use TemplateAlias "welcome" and pass name in the TemplateModel
   - Use Promise.allSettled so one failure does not stop the other sends
   - Return an array of per-recipient results with fields: email, ok, messageId, error
   - Include messageId for fulfilled results and error string for rejected results

3. Log partial failures:
   - After the batch completes, log which recipients failed and why
   - Log a summary of how many succeeded vs. failed

4. Smoke test:
   - Include a hardcoded array of two test recipients
   - Run the batch and print the results

Use TypeScript throughout. Use async/await. Do not use Promise.all.

Example response

Your AI assistant should generate code similar to this structure:

1. Batch send with partial-failure handling

import { ServerClient } from "postmark";

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

export async function sendBatchWelcome(
  recipients: Array<{ email: string; name: string }>
) {
  const results = await Promise.allSettled(
    recipients.map((r) =>
      postmark.sendEmailWithTemplate({
        From: "welcome@acme.example",
        To: r.email,
        TemplateAlias: "welcome",
        TemplateModel: { name: r.name },
        MessageStream: "outbound",
      })
    )
  );

  return results.map((result, index) => ({
    email: recipients[index].email,
    ok: result.status === "fulfilled",
    messageId:
      result.status === "fulfilled" ? result.value.MessageID : undefined,
    error:
      result.status === "rejected" ? String(result.reason) : undefined,
  }));
}

2. Log partial failures

const results = await sendBatchWelcome(recipients);

const succeeded = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);

console.log(`Sent: ${succeeded.length}, Failed: ${failed.length}`);
failed.forEach((r) => console.error(`Failed for ${r.email}: ${r.error}`));

3. Smoke test

const results = await sendBatchWelcome([
  { email: "ada@example.com", name: "Ada" },
  { email: "grace@example.com", name: "Grace" },
]);
console.log(results);

What to do with this code

  1. Use Promise.allSettled, not Promise.all: Promise.all throws and aborts as soon as one send fails. Promise.allSettled waits for every send to complete and returns a result for each — fulfilled or rejected. For transactional batch sends, you almost always want allSettled.
  2. Store the MessageIDs: Save each returned MessageID alongside the recipient record so you can correlate delivery and bounce webhooks back to specific sends.
  3. Handle the failures: Don't just log failed sends — act on them. Invalid addresses should be removed from your list; temporary failures can be retried.
  4. Watch your batch size: Postmark's /email/batch endpoint accepts up to 500 messages per call. For larger batches, chunk your recipients array and call the function in sequence.

Next steps

  • Add delivery and bounce webhook handlers to close the operational loop
  • Store per-recipient MessageID values in your database for webhook correlation
  • See the TypeScript integration guide → for webhook verification

Related resources

Handle delivery and bounce webhooks #

Handle delivery and bounce webhooks

Receive real-time delivery and bounce events from Postmark and act on them in your application — with raw-body signature verification so you only process legitimate payloads.

What you'll build

A TypeScript webhook handler that verifies the Postmark signature using HMAC-SHA256, parses Delivery and Bounce event types, returns normalized results your application can use, and includes an idempotency check so retried webhooks don't cause duplicate processing.

Prerequisites

  • Node.js 18+ and a TypeScript project
  • A Postmark account with at least one server configured
  • Your Postmark webhook secret (set when configuring a webhook in your server settings)
  • A publicly accessible endpoint (use ngrok for local development)

Prompt

Copy and paste this prompt into your preferred AI chat:

Add Postmark webhook handling to my TypeScript project. Include:

1. Signature verification:
   - Create a verifyPostmarkWebhook function that accepts rawBody and signature
   - Use crypto.createHmac with SHA-256 and POSTMARK_WEBHOOK_SECRET from environment variables
   - Use crypto.timingSafeEqual to compare the expected and received signatures
   - Return a boolean

2. Webhook handler:
   - Accept rawBody as a string and signature as a string
   - Call verifyPostmarkWebhook and throw an error if verification fails
   - Parse the raw body as JSON after verification — not before
   - Handle RecordType "Delivery": return { ok: true, id: MessageID }
   - Handle RecordType "Bounce": return { ok: false, id: MessageID, reason: Description }
   - Handle unknown RecordTypes gracefully

3. Idempotency:
   - Add a comment explaining that Postmark may retry webhooks on non-200 responses
   - Show how to check if a MessageID has already been processed before acting on it

4. Express route:
   - Create a POST /webhooks/postmark route
   - Read the raw request body as a string (not parsed JSON)
   - Extract the X-Postmark-Signature header
   - Call the handler and return 200 on success
   - Always return 200 to acknowledge receipt, even for unknown event types

Use TypeScript throughout. Use node:crypto — do not use any third-party verification library.

Example response

Your AI assistant should generate code similar to this structure:

1. Signature verification

import crypto from "node:crypto";

export function verifyPostmarkWebhook(
  rawBody: string,
  signature: string
): boolean {
  const expected = crypto
    .createHmac("sha256", process.env.POSTMARK_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

2. Webhook handler

export async function handleEmailWebhook(
  rawBody: string,
  signature: string
) {
  if (!verifyPostmarkWebhook(rawBody, signature)) {
    throw new Error("Invalid Postmark webhook signature");
  }

  // Parse after verification — never before
  const event = JSON.parse(rawBody) as {
    RecordType: string;
    MessageID: string;
    Description?: string;
  };

  if (event.RecordType === "Delivery") {
    return { ok: true, id: event.MessageID };
  }
  if (event.RecordType === "Bounce") {
    return { ok: false, id: event.MessageID, reason: event.Description };
  }

  // Return ok for unhandled types — still acknowledge receipt
  return { ok: true };
}

3. Idempotency check

// Postmark retries webhooks if your endpoint returns a non-200 response.
// Check whether you've already processed a MessageID before acting on it.
const alreadyProcessed = await db.webhookEvents.findOne({
  messageId: event.MessageID,
});
if (alreadyProcessed) return { ok: true };

await db.webhookEvents.create({ messageId: event.MessageID });

4. Express route

import express from "express";

app.post(
  "/webhooks/postmark",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    // Always respond 200 — Postmark will retry on any other status
    res.sendStatus(200);

    const rawBody = req.body.toString();
    const signature = req.headers["x-postmark-signature"] as string;

    try {
      await handleEmailWebhook(rawBody, signature);
    } catch (error) {
      console.error("Webhook processing error:", error);
    }
  }
);

What to do with this code

  1. Set POSTMARK_WEBHOOK_SECRET: Copy the webhook secret from your Postmark server settings and add it to your .env file.
  2. Use express.raw(), not express.json(): Your route must receive the raw body as a Buffer or string — if Express parses it as JSON first, the signature check will fail because the raw bytes have changed.
  3. Always return 200 first: Acknowledge receipt immediately, then process asynchronously. If your handler throws before you respond, Postmark will retry — which is why the idempotency check matters.
  4. Test locally with ngrok: Run ngrok http 3000 to get a public URL, paste it into your Postmark server's webhook settings, and select Delivery and Bounce as the event types to receive.
  5. Act on bounces: A hard bounce means the address is invalid — pause or remove it from your sending list. Continuing to send to hard-bounced addresses hurts your sending reputation.

Next steps

  • Add handlers for SpamComplaint events using the same pattern
  • Store bounce data to suppress future sends to affected addresses
  • See the TypeScript integration guide → for the full send-to-webhook implementation in one place

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


Send a receipt with templates #

Send a receipt with templates

Send structured transactional receipts using Postmark's template system — the recommended path for any email where the layout stays the same but the data changes per send.

What you'll build

A TypeScript function that sends a receipt email using a Postmark template, with a typed TemplateModel, a returned MessageID, and a smoke-test you can run immediately to confirm the integration works.

Prerequisites

  • Node.js 18+ and a TypeScript project
  • A Postmark account with a verified sender signature or domain
  • Your Postmark Server API token (found in your server's API Tokens tab)
  • A template in your Postmark account with the alias receipt (or substitute your own alias)

Prompt

Copy and paste this prompt into your preferred AI chat:

Add Postmark transactional email to my TypeScript project using templates. Include:

1. Installation and setup:
   - Install the postmark npm package
   - Import ServerClient from postmark (ESM import, not require)
   - Configure the client using POSTMARK_SERVER_TOKEN from environment variables
   - Export the client as a shared instance

2. Send a receipt email with a template:
   - Use sendEmailWithTemplate with a TemplateAlias of "receipt"
   - Define a typed ReceiptEmail interface with fields: to, receiptId, amount, dashboardUrl
   - Pass a TemplateModel object with receipt_id, amount, and dashboard_url
   - Set MessageStream to "outbound"
   - Return the MessageID from the response

3. Send a welcome email with a template:
   - Use sendEmailWithTemplate with a TemplateAlias of "welcome"
   - Accept to and name as parameters
   - Return the MessageID

4. Smoke test:
   - Add a runnable script that sends a receipt to a TEST_EMAIL environment variable
   - Log the returned MessageID on success

Use TypeScript throughout. Use async/await. Do not use inline HTML as the email body.

Example response

Your AI assistant should generate code similar to this structure:

1. Install and configure

// npm install postmark

import { ServerClient } from "postmark";

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

2. Send a typed receipt email

type ReceiptEmail = {
  to: string;
  receiptId: string;
  amount: string;
  dashboardUrl: string;
};

export async function sendReceiptEmail(input: ReceiptEmail) {
  const message = await postmark.sendEmailWithTemplate({
    From: "billing@acme.example",
    To: input.to,
    TemplateAlias: "receipt",
    TemplateModel: {
      receipt_id: input.receiptId,
      amount: input.amount,
      dashboard_url: input.dashboardUrl,
    },
    MessageStream: "outbound",
  });
  return message.MessageID;
}

3. Send a welcome email

export async function sendWelcomeEmail(to: string, name: string) {
  const message = await postmark.sendEmailWithTemplate({
    From: "welcome@acme.example",
    To: to,
    TemplateAlias: "welcome",
    TemplateModel: { name },
    MessageStream: "outbound",
  });
  return message.MessageID;
}

4. Smoke test

const messageId = await sendReceiptEmail({
  to: process.env.TEST_EMAIL!,
  receiptId: "rcpt_123",
  amount: "$49.00",
  dashboardUrl: "https://acme.example/dashboard",
});
console.log({ messageId });

What to do with this code

  1. Set your environment variables: Add POSTMARK_SERVER_TOKEN and TEST_EMAIL to your .env file.
  2. Create a template in Postmark: Go to your server's Templates tab and create a template with the alias receipt. Add {{receipt_id}}, {{amount}}, and {{dashboard_url}} as variables in the template body.
  3. Run the smoke test: Execute the script to confirm you get a MessageID back. If the send fails, check that your From address is verified and that the template alias matches exactly.
  4. Store the MessageID: Save the returned MessageID in your database alongside the transaction record — you'll need it to correlate any bounce or delivery webhooks.

Next steps

  • Add a delivery webhook handler to confirm receipts were delivered
  • Set up a bounce handler to pause billing emails to addresses that hard-bounce
  • Use the TypeScript integration guide → for the full picture including batch sends and webhook verification

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