Transactional emails — password resets, order confirmations, welcome messages, account alerts — need to reach the inbox reliably. This guide shows how to integrate the NexusProMail REST API into a Node.js application with production-ready patterns: environment-based configuration, structured error handling, retry logic and webhook verification.
Prerequisites
- Node.js 18 or later
- A NexusProMail account (free — sandbox key available immediately on signup)
- A verified sending domain for production sends (DKIM + SPF)
Step 1 — Install Dependencies
The NexusProMail API is a standard REST API. You can use the native fetch API (Node.js 18+) or node-fetch for earlier versions. No proprietary SDK is required.
npm install dotenv
# node-fetch only if using Node.js < 18
npm install node-fetch
Step 2 — Configure Environment Variables
Never hardcode API keys. Store them in environment variables:
# .env (add to .gitignore — never commit this file)
NEXUSPROMAIL_API_KEY=your_api_key_here
NEXUSPROMAIL_API_URL=https://app.nexuspromail.com/api
FROM_EMAIL=hello@mail.yourdomain.com
FROM_NAME=Your App
Step 3 — Create a Send Helper
Create a reusable function that handles the API call, error classification and logging:
// lib/email.js
require('dotenv').config()
const API_URL = process.env.NEXUSPROMAIL_API_URL
const API_KEY = process.env.NEXUSPROMAIL_API_KEY
/**
* Send a single transactional email.
* Returns { success: true, messageId } or { success: false, error, code }
*/
async function sendTransactional({ to, subject, html, text, replyTo }) {
if (!API_KEY) throw new Error('NEXUSPROMAIL_API_KEY environment variable not set')
const payload = {
to: typeof to === 'string' ? { email: to } : to,
from: { email: process.env.FROM_EMAIL, name: process.env.FROM_NAME },
subject,
html,
...(text && { text }),
...(replyTo && { reply_to: { email: replyTo } }),
}
const response = await fetch(`${API_URL}/v1/transactional/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const body = await response.json()
if (response.ok) {
return { success: true, messageId: body.message_id }
}
// Classify the error
if (response.status === 422 && body.error === 'suppressed') {
return { success: false, code: 'SUPPRESSED', error: 'Address is suppressed' }
}
if (response.status === 422) {
return { success: false, code: 'VALIDATION', error: body.message }
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || '60'
return { success: false, code: 'RATE_LIMITED', retryAfter: parseInt(retryAfter, 10) }
}
return { success: false, code: 'API_ERROR', error: body.message, status: response.status }
}
module.exports = { sendTransactional }
Step 4 — Send a Password Reset Email
// Example: password reset flow
const { sendTransactional } = require('./lib/email')
async function sendPasswordReset(userEmail, resetToken) {
const resetUrl = `https://yourapp.com/reset?token=${resetToken}`
const result = await sendTransactional({
to: userEmail,
subject: 'Reset your password',
html: `
You requested a password reset.
Click here to reset your password
This link expires in 1 hour. If you did not request this, you can safely ignore this email.
`,
text: `Reset your password: ${resetUrl}
This link expires in 1 hour.`,
})
if (!result.success) {
if (result.code === 'SUPPRESSED') {
// Address is suppressed — user unsubscribed or complained
// Log the event; do not send. This is expected behaviour.
console.log(`Password reset skipped — address suppressed: ${userEmail}`)
return
}
if (result.code === 'RATE_LIMITED') {
// Queue for retry after result.retryAfter seconds
throw new Error(`Rate limited — retry after ${result.retryAfter}s`)
}
throw new Error(`Failed to send password reset: ${result.error}`)
}
console.log(`Password reset sent. Message ID: ${result.messageId}`)
}
Step 5 — Add Retry Logic for Transient Failures
Wrap your send function with retry logic for 429 and 500 responses:
async function sendWithRetry(params, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const result = await sendTransactional(params)
if (result.success || result.code === 'SUPPRESSED' || result.code === 'VALIDATION') {
return result // Terminal outcomes — do not retry
}
if (result.code === 'RATE_LIMITED') {
const delay = (result.retryAfter || 30) * 1000 * attempt
console.log(`Rate limited. Waiting ${delay}ms before retry ${attempt}/${maxAttempts}`)
await new Promise(r => setTimeout(r, delay))
continue
}
// API_ERROR — exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000)
console.log(`API error. Waiting ${delay}ms before retry ${attempt}/${maxAttempts}`)
await new Promise(r => setTimeout(r, delay))
}
throw new Error('Max retry attempts exceeded')
}
Step 6 — Handle Delivery Webhooks
Register a webhook endpoint to receive delivery events — bounces, complaints and opens. Always verify the signature before processing.
// routes/webhooks.js (Express)
const crypto = require('crypto')
const express = require('express')
const router = express.Router()
function verifySignature(rawBody, signature) {
const secret = process.env.WEBHOOK_SECRET
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature, 'hex'))
} catch {
return false
}
}
router.post('/email', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-nexuspromail-signature']
if (!signature || !verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(req.body)
switch (event.type) {
case 'bounced.hard':
// Suppress in your own database immediately
await db.users.update({ email: event.data.email }, { emailBounced: true })
break
case 'complained':
// Suppress and mark — do not contact again
await db.users.update({ email: event.data.email }, { complained: true })
break
case 'unsubscribed':
await db.users.update({ email: event.data.email }, { marketingOptOut: true })
break
}
res.sendStatus(200) // Always return 200 promptly to acknowledge receipt
})
module.exports = router
Testing
Use your sandbox API key for development and CI. Sandbox sends do not require domain verification and do not deliver real email. Flip to your production key (and a verified sending domain) when you go live. Both environments use identical API endpoints and response formats.
Further reading
- API integration guide — authentication, rate limits, SMTP vs REST
- Transactional email API overview
- Email deliverability guide
- Domain warming for new sending domains