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.
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:
Get working code The AI will generate complete, functional code based on the prompt, including error handling, best practices, and proper Postmark API usage.
Each prompt is designed to generate production-ready code that includes:
Note: AI can make mistakes! Always verify the output before using AI generated code.
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.
Send transactional email from your Node.js application using Postmark's official library.
A Node.js/Express app that sends transactional emails through Postmark's API, handles delivery webhooks, and processes bounce notifications.
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.
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');
});
```
Configure ActionMailer to send transactional email through Postmark's SMTP or API integration.
A Rails application that sends transactional emails using Postmark, with proper ActionMailer configuration, background job processing, and delivery handling.
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.
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
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.
Configure Laravel Mail to send transactional email through Postmark's API.
A Laravel application that sends transactional emails using Postmark, with proper mail driver configuration, Mailable classes, and queue integration.
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.
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());
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.
Send secure password reset emails and handle the complete reset workflow in your application.
A complete password reset system that generates secure tokens, sends reset emails through Postmark, validates reset requests, and updates user passwords.
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.
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' });
});
Send automated transactional emails when users perform actions in your application.
An event-driven notification system that sends emails when users complete key actions like signing up, making purchases, or updating settings.
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.
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);
}
});
Receive and process incoming emails sent to your application's email addresses.
An inbound email handler that receives emails via webhooks, parses message content, processes attachments, and routes messages to your application logic.
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.
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);
}
Set up Postmark as your email provider for Better Auth authentication flows including email verification and password resets.
A Better Auth integration that uses Postmark to send authentication emails including email verification links and password reset tokens.
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.
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