postmark-send-email

Public
0

Repository: activecampaign/postmark-skills

Log in or sign up to clone this skill.

A
activecampaign
Imported Mar 3, 2026

Low Risk

No security issues found

INFO

Skill manifest does not include a 'license' field. Specifying a license helps users understand usage terms.

Remediation Add 'license' field to SKILL.md frontmatter (e.g., MIT, Apache-2.0)

Scanned in 0.010s

Description

Use when sending transactional or broadcast emails through Postmark — single sends, batch (up to 500), bulk, or template-based emails with support for attachments, tracking, and message streams.

Details

License MIT
Metadata
author
postmark
version
1.0.0

Skill Files

Download .zip
SKILL.md
# Send Email with Postmark

## Overview

Postmark provides multiple endpoints for sending emails:

| Approach | Endpoint | Use Case | Limits |
|----------|----------|----------|--------|
| **Single** | `POST /email` | Individual transactional emails | 1 email, 10 MB payload including attachments |
| **Batch** | `POST /email/batch` | Up to 500 emails in one request | 500 emails, 50 MB payload including attachments |
| **Template** | `POST /email/withTemplate` | Dynamic content with server-side templates | 1 email, 10 MB payload including attachments |
| **Batch Template** | `POST /email/batchWithTemplates` | Bulk templated emails | 500 emails, 50 MB payload including attachments |
| **Bulk** | `POST /email/bulk` | Broadcast stream campaigns | No fixed recipient cap, 50 MB payload including attachments |

**Choose batch when:** sending 2+ distinct emails at once, performance matters, or attachments are needed.
**Choose single when:** sending one email or real-time per-message error handling is needed.

## Message Streams (CRITICAL)

Postmark separates emails by intent. **Always specify MessageStream**:

| Stream | Value | Purpose | SMTP Endpoint |
|--------|-------|---------|---------------|
| **Transactional** | `outbound` | 1:1 triggered emails (default) | smtp.postmarkapp.com |
| **Broadcast** | `broadcast` | Marketing, newsletters | smtp-broadcasts.postmarkapp.com |

Never mix transactional and broadcast in the same stream — it damages deliverability. Servers can have up to 10 message streams.

## Quick Start

1. **Get API Token** from your [Postmark server settings](https://account.postmarkapp.com/servers)
2. **Verify sender** domain or email address
3. **Install SDK** — see [references/installation.md](references/installation.md)
4. **Choose endpoint** based on the decision matrix above

## Authentication

All API requests require the Server API Token:

```
X-Postmark-Server-Token: your-server-token-here
```

Store the token in `POSTMARK_SERVER_TOKEN`. For testing without sending, use `POSTMARK_API_TEST` as the token value.

## Single Email

**Endpoint:** `POST https://api.postmarkapp.com/email`

### Required Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `From` | string | Sender address (must be a verified domain or sender signature) |
| `To` | string | Recipients (comma-separated, max 50 total with Cc/Bcc) |
| `Subject` | string | Email subject line |
| `TextBody` or `HtmlBody` | string | Message content (at least one required) |

### Optional Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `Cc` | string | CC recipients |
| `Bcc` | string | BCC recipients |
| `ReplyTo` | string | Reply-to address |
| `MessageStream` | string | `outbound` (default) or `broadcast` |
| `Tag` | string | Category for statistics (one per message, max 1000 chars) |
| `Metadata` | object | Key-value pairs for custom tracking data (returned in webhook payloads) |
| `TrackOpens` | boolean | Enable open tracking |
| `TrackLinks` | string | `None`, `HtmlAndText`, `HtmlOnly`, `TextOnly` |
| `Headers` | array | Custom email headers `[{Name, Value}]` |
| `Attachments` | array | File attachments (max 10 MB total) |

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const result = await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Subject: 'Your order has shipped',
  TextBody: 'Your order #12345 is on its way!',
  HtmlBody: '<p>Your order <strong>#12345</strong> is on its way!</p>',
  MessageStream: 'outbound',
  Tag: 'order-shipped'
});

console.log('MessageID:', result.MessageID);
```

Response includes `MessageID` — use it for tracking via webhooks or the Messages API.

See [references/single-email-examples.md](references/single-email-examples.md) for Python, Ruby, PHP, .NET, and cURL examples.

## Batch Email

**Endpoint:** `POST https://api.postmarkapp.com/email/batch`

Send up to **500 emails** in a single API call. Each message is independently validated and can have its own attachments, tags, and metadata.

```javascript
const results = await client.sendEmailBatch([
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Order Shipped',
    TextBody: 'Your order has shipped!',
    MessageStream: 'outbound'
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Order Confirmed',
    TextBody: 'Your order is confirmed!',
    MessageStream: 'outbound'
  }
]);

// Check individual results — always handle partial failures
results.forEach((result, index) => {
  if (result.ErrorCode === 0) {
    console.log(`Email ${index + 1} sent: ${result.MessageID}`);
  } else {
    console.error(`Email ${index + 1} failed: ${result.Message}`);
  }
});
```

For more than 500 emails, chunk the array into groups of 500 and send sequentially. See [references/batch-email-examples.md](references/batch-email-examples.md) for chunking patterns, attachments, and Python/Ruby/cURL examples.

## Send with Template

**Endpoint:** `POST https://api.postmarkapp.com/email/withTemplate`

Use server-side Handlebars templates — no client-side rendering needed. Always use `TemplateAlias` over `TemplateId` — aliases survive re-creation and work across environments.

```javascript
const result = await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateAlias: 'order-confirmation',
  TemplateModel: {
    customer_name: 'Jane Doe',
    order_number: 'ORD-67890',
    items: [
      { name: 'Widget', price: '$19.99' },
      { name: 'Gadget', price: '$29.99' }
    ]
  },
  MessageStream: 'outbound'
});
```

For batch template sends (up to 500), use `POST /email/batchWithTemplates` via `client.sendEmailBatchWithTemplates([...])`. See [references/template-examples.md](references/template-examples.md) for full examples.

## Attachments

Include attachments as Base64-encoded content:

```json
{
  "Attachments": [
    {
      "Name": "invoice.pdf",
      "Content": "base64-encoded-content-here",
      "ContentType": "application/pdf"
    }
  ]
}
```

Embed inline images using `ContentID`:

```json
{
  "HtmlBody": "<img src=\"cid:logo123\">",
  "Attachments": [
    {
      "Name": "logo.png",
      "Content": "base64-encoded-image",
      "ContentType": "image/png",
      "ContentID": "cid:logo123"
    }
  ]
}
```

**Size limits:** TextBody/HtmlBody 5 MB each · Single message with attachments 10 MB · Batch payload 50 MB · Base64 encoding adds ~33% · Certain file types blocked (.exe, .bat)

## Tracking

Configure per-email or at server level:

```json
{ "TrackOpens": true, "TrackLinks": "HtmlAndText" }
```

**TrackLinks options:** `None` | `HtmlAndText` | `HtmlOnly` | `TextOnly`

Disable tracking for sensitive transactional emails (password resets, security alerts) to maximize deliverability.

## Testing

| Method | Address/Token | Result |
|--------|---------------|--------|
| **API Test Token** | `POSTMARK_API_TEST` | Validates request without sending |
| **Black Hole** | `[email protected]` | Dropped but appears in activity |
| **Sandbox Server** | Create sandbox server in dashboard | Full processing, no delivery |
| **Hard Bounce** | `[email protected]` | Simulates hard bounce |
| **Soft Bounce** | `[email protected]` | Simulates soft bounce |

**Never** test with fake addresses at real providers (e.g., [email protected]) — damages sender reputation.

## Error Handling

| Code | Meaning | Action |
|------|---------|--------|
| 200 | Success | Continue |
| 401 | Unauthorized | Check API token — do not retry |
| 406 | Inactive recipient | Check suppression list — do not retry |
| 409 | JSON required | Fix `Accept`/`Content-Type` headers |
| 410 | Too many batch messages | Reduce to 500 or fewer per batch |
| 413 | Payload too large | Reduce payload (10 MB single, 50 MB batch) |
| 422 | Validation error | Fix request parameters — do not retry |
| 429 | Rate limited | Retry with exponential backoff |
| 500 | Server error | Retry with exponential backoff |

```javascript
async function sendWithRetry(client, email, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await client.sendEmail(email);
    } catch (error) {
      const isRetryable = error.statusCode === 429 || error.statusCode === 500;
      if (!isRetryable || attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}
```

See [references/error-handling.md](references/error-handling.md) for complete error patterns including batch partial failures and typed error classes.

## SMTP

Postmark supports SMTP for legacy integrations — host `smtp.postmarkapp.com` (transactional) or `smtp-broadcasts.postmarkapp.com` (broadcast), ports 25/2525/587, Server API Token as username and password. See [references/smtp-migration.md](references/smtp-migration.md) for migration examples and custom header reference.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Missing MessageStream | Always specify `outbound` or `broadcast` |
| Using broadcast for transactional | Use separate streams for different email types |
| Testing with real addresses | Use `POSTMARK_API_TEST` or sandbox mode |
| Retrying 422 errors | These are validation errors — fix request, don't retry |
| Not handling partial batch failures | Check each result in batch response array |
| Tracking on sensitive transactional emails | Disable for password resets, security alerts, receipts |
| Exceeding 50 recipients per email | Split into multiple emails or use batch |
| Not verifying sender | Domain or address must be verified before sending |

## Notes

- `From` address must use a verified domain or sender signature
- Store API key in `POSTMARK_SERVER_TOKEN` environment variable
- Maximum 50 recipients total per email (To + Cc + Bcc)
- `MessageID` returned in response is used for bounce/webhook/API correlation
- For broadcast campaigns to large lists, use the Bulk API endpoint (`POST /email/bulk`)
- Use `POSTMARK_API_TEST` as the token value in development and CI — validates without sending
- New domains require gradual volume warm-up — see [`postmark-email-best-practices`](../postmark-email-best-practices/) for the schedule
references/batch-email-examples.md Reference
# Batch Email Examples

Complete examples for sending batch emails via `POST /email/batch` (up to 500 per call).

## Node.js / TypeScript

### Basic Batch

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const results = await client.sendEmailBatch([
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Welcome!',
    TextBody: 'Welcome to our service, User 1.',
    MessageStream: 'outbound'
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Welcome!',
    TextBody: 'Welcome to our service, User 2.',
    MessageStream: 'outbound'
  }
]);

results.forEach((result, index) => {
  if (result.ErrorCode === 0) {
    console.log(`Email ${index + 1}: sent (${result.MessageID})`);
  } else {
    console.error(`Email ${index + 1}: failed (${result.Message})`);
  }
});
```

### Batch with Attachments

```javascript
const fs = require('fs');

const invoiceContent = fs.readFileSync('./invoice-template.pdf').toString('base64');

const results = await client.sendEmailBatch([
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Invoice #001',
    TextBody: 'Please find your invoice attached.',
    MessageStream: 'outbound',
    Attachments: [
      {
        Name: 'invoice-001.pdf',
        Content: invoiceContent,
        ContentType: 'application/pdf'
      }
    ]
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Invoice #002',
    TextBody: 'Please find your invoice attached.',
    MessageStream: 'outbound',
    Attachments: [
      {
        Name: 'invoice-002.pdf',
        Content: invoiceContent,
        ContentType: 'application/pdf'
      }
    ]
  }
]);
```

### Chunking Large Lists (500+ Emails)

```javascript
function chunkArray(array, size) {
  const chunks = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

async function sendLargeBatch(client, emails) {
  const chunks = chunkArray(emails, 500);
  const allResults = [];

  for (const chunk of chunks) {
    const results = await client.sendEmailBatch(chunk);
    allResults.push(...results);

    // Log progress
    const sent = results.filter(r => r.ErrorCode === 0).length;
    const failed = results.filter(r => r.ErrorCode !== 0).length;
    console.log(`Chunk: ${sent} sent, ${failed} failed`);
  }

  return allResults;
}

// Usage
const emails = users.map(user => ({
  From: '[email protected]',
  To: user.email,
  Subject: `Hello ${user.name}`,
  TextBody: `Hi ${user.name}, here is your weekly update.`,
  MessageStream: 'outbound',
  Tag: 'weekly-update',
  Metadata: { user_id: user.id }
}));

const results = await sendLargeBatch(client, emails);
```

### Handling Partial Failures

```javascript
const results = await client.sendEmailBatch(emails);

const succeeded = [];
const failed = [];

results.forEach((result, index) => {
  if (result.ErrorCode === 0) {
    succeeded.push({ index, messageId: result.MessageID });
  } else {
    failed.push({
      index,
      errorCode: result.ErrorCode,
      message: result.Message,
      email: emails[index].To
    });
  }
});

console.log(`Sent: ${succeeded.length}, Failed: ${failed.length}`);

if (failed.length > 0) {
  console.log('Failed emails:', failed);

  // Retry only retryable failures
  const retryable = failed.filter(f =>
    f.errorCode === 429 || f.errorCode === 500
  );

  if (retryable.length > 0) {
    const retryEmails = retryable.map(f => emails[f.index]);
    // Retry after delay...
  }
}
```

## Python

### Basic Batch

```python
from postmarker.core import PostmarkClient
import os

postmark = PostmarkClient(server_token=os.environ['POSTMARK_SERVER_TOKEN'])

results = postmark.emails.send_batch(
    {
        'From': '[email protected]',
        'To': '[email protected]',
        'Subject': 'Welcome!',
        'TextBody': 'Welcome to our service, User 1.',
        'MessageStream': 'outbound'
    },
    {
        'From': '[email protected]',
        'To': '[email protected]',
        'Subject': 'Welcome!',
        'TextBody': 'Welcome to our service, User 2.',
        'MessageStream': 'outbound'
    }
)

for i, result in enumerate(results):
    if result.get('ErrorCode') == 0:
        print(f"Email {i + 1}: sent ({result['MessageID']})")
    else:
        print(f"Email {i + 1}: failed ({result.get('Message', 'Unknown error')})")
```

### Chunking in Python

```python
def chunk_list(lst, size):
    for i in range(0, len(lst), size):
        yield lst[i:i + size]

emails = [
    {
        'From': '[email protected]',
        'To': user['email'],
        'Subject': f'Hello {user["name"]}',
        'TextBody': f'Hi {user["name"]}, here is your update.',
        'MessageStream': 'outbound'
    }
    for user in users
]

all_results = []
for chunk in chunk_list(emails, 500):
    results = postmark.emails.send_batch(*chunk)
    all_results.extend(results)
```

## Ruby

### Basic Batch

```ruby
require 'postmark'

client = Postmark::ApiClient.new(ENV['POSTMARK_SERVER_TOKEN'])

results = client.deliver_messages([
  {
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome!',
    text_body: 'Welcome to our service, User 1.',
    message_stream: 'outbound'
  },
  {
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome!',
    text_body: 'Welcome to our service, User 2.',
    message_stream: 'outbound'
  }
])
```

## cURL

### Batch Request

```bash
curl "https://api.postmarkapp.com/email/batch" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '[
    {
      "From": "[email protected]",
      "To": "[email protected]",
      "Subject": "Welcome!",
      "TextBody": "Welcome to our service.",
      "MessageStream": "outbound"
    },
    {
      "From": "[email protected]",
      "To": "[email protected]",
      "Subject": "Welcome!",
      "TextBody": "Welcome to our service.",
      "MessageStream": "outbound"
    }
  ]'
```

## Batch with Templates

**Endpoint:** `POST /email/batchWithTemplates`

```javascript
const results = await client.sendEmailBatchWithTemplates([
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: { name: 'User 1', action_url: 'https://app.yourdomain.com' },
    MessageStream: 'outbound'
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: { name: 'User 2', action_url: 'https://app.yourdomain.com' },
    MessageStream: 'outbound'
  }
]);
```

## Key Notes

- Maximum **500 emails** per batch call
- Maximum **50 MB** total payload size
- Each email is independently validated — one failure does not affect others
- Attachments are supported in batch (unlike some alternatives)
- Always check each result's `ErrorCode` — partial failures are possible
- For more than 500 emails, chunk into groups of 500 and send sequentially
references/error-handling.md Reference
# Error Handling

Complete reference for handling Postmark API errors.

## HTTP Status Codes

| Code | Meaning | Retryable | Action |
|------|---------|-----------|--------|
| 200 | Success | — | Continue |
| 401 | Unauthorized | No | Check `X-Postmark-Server-Token` value |
| 406 | Inactive recipient | No | Recipient is suppressed — check suppression list |
| 409 | JSON required | No | Set `Content-Type: application/json` and `Accept: application/json` |
| 410 | Too many batch messages | No | Reduce batch to 500 or fewer messages |
| 413 | Payload too large | No | Reduce payload size (10 MB single, 50 MB batch) |
| 422 | Validation error | No | Fix request parameters (invalid From, missing required fields, etc.) |
| 429 | Rate limited | Yes | Retry with exponential backoff |
| 500 | Server error | Yes | Retry with exponential backoff |

## Postmark Error Codes

Beyond HTTP status, Postmark returns specific `ErrorCode` values in the response body:

| ErrorCode | Meaning |
|-----------|---------|
| 0 | No error (success) |
| 10 | Bad or missing API token |
| 300 | Invalid email request |
| 406 | Inactive recipient (suppressed due to bounce/complaint) |
| 409 | JSON body required |
| 410 | Too many batch messages (max 500) |

## Retry Strategy

Only retry **429** (rate limited) and **500** (server error). Never retry 401, 406, 422, or other client errors.

### Node.js

```javascript
async function sendWithRetry(client, email, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await client.sendEmail(email);
    } catch (error) {
      const statusCode = error.statusCode || error.code;
      const isRetryable = statusCode === 429 || statusCode === 500;

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
      console.log(`Attempt ${attempt + 1} failed (${statusCode}), retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
try {
  const result = await sendWithRetry(client, {
    From: '[email protected]',
    To: '[email protected]',
    Subject: 'Hello',
    TextBody: 'Hello!',
    MessageStream: 'outbound'
  });
  console.log('Sent:', result.MessageID);
} catch (error) {
  console.error('Failed after retries:', error.message);
}
```

### Python

```python
import time

def send_with_retry(postmark, email, max_retries=3):
    for attempt in range(max_retries + 1):
        try:
            return postmark.emails.send(**email)
        except Exception as e:
            status_code = getattr(e, 'status_code', None)
            is_retryable = status_code in (429, 500)

            if not is_retryable or attempt == max_retries:
                raise

            delay = (2 ** attempt)
            print(f"Attempt {attempt + 1} failed ({status_code}), retrying in {delay}s...")
            time.sleep(delay)

# Usage
try:
    result = send_with_retry(postmark, {
        'From': '[email protected]',
        'To': '[email protected]',
        'Subject': 'Hello',
        'TextBody': 'Hello!',
        'MessageStream': 'outbound'
    })
    print('Sent:', result['MessageID'])
except Exception as e:
    print('Failed after retries:', str(e))
```

## Handling Batch Partial Failures

Batch responses return individual results. Some emails may succeed while others fail:

```javascript
const results = await client.sendEmailBatch(emails);

const succeeded = [];
const permanentFailures = [];
const retryableFailures = [];

results.forEach((result, index) => {
  if (result.ErrorCode === 0) {
    succeeded.push({ index, messageId: result.MessageID });
  } else if (result.ErrorCode === 429 || result.ErrorCode >= 500) {
    retryableFailures.push({ index, error: result.Message });
  } else {
    permanentFailures.push({ index, error: result.Message, code: result.ErrorCode });
  }
});

console.log(`Sent: ${succeeded.length}`);
console.log(`Permanent failures: ${permanentFailures.length}`);
console.log(`Retryable failures: ${retryableFailures.length}`);

// Retry only retryable failures
if (retryableFailures.length > 0) {
  const retryEmails = retryableFailures.map(f => emails[f.index]);
  await new Promise(r => setTimeout(r, 2000));
  const retryResults = await client.sendEmailBatch(retryEmails);
  // Process retry results...
}
```

## Handling Inactive Recipients (406)

When a recipient is suppressed (due to previous bounce or spam complaint):

```javascript
const postmark = require('postmark');

try {
  await client.sendEmail(emailData);
} catch (error) {
  if (error.statusCode === 406) {
    // Recipient is inactive — check suppression list
    console.log('Inactive recipient:', emailData.To);

    // Optionally check the suppression list
    const suppressions = await client.getSuppressions('outbound');
    const suppressed = suppressions.Suppressions.find(
      s => s.EmailAddress === emailData.To
    );

    if (suppressed) {
      console.log('Suppression reason:', suppressed.SuppressionReason);
      // SuppressionReason: "HardBounce" | "SpamComplaint" | "ManualSuppression"
    }
  }
}
```

## Error Object Structure (Node.js SDK)

The `postmark` SDK throws typed errors:

```javascript
const { Errors } = require('postmark');

try {
  await client.sendEmail(emailData);
} catch (error) {
  if (error instanceof Errors.InactiveRecipientsError) {
    // 406 — Inactive recipient
    console.log('Inactive recipients:', error.recipients);
  } else if (error instanceof Errors.InvalidAPIKeyError) {
    // 401 — Bad API token
    console.log('Invalid API key');
  } else if (error instanceof Errors.RateLimitExceededError) {
    // 429 — Rate limited
    console.log('Rate limited, retry after delay');
  } else if (error instanceof Errors.ApiInputError) {
    // 422 — Validation error
    console.log('Invalid input:', error.message);
  } else if (error instanceof Errors.InternalServerError) {
    // 500 — Server error
    console.log('Server error, retry');
  }
}
```

## Key Guidelines

- **Never retry** 401, 406, 409, 410, 413, or 422 errors — fix the request
- **Always retry** 429 and 500 with exponential backoff (1s, 2s, 4s)
- **Max retries:** 3–5 for most use cases
- **Batch responses** can have mixed results — check each entry
- **406 (Inactive recipient)** means the address was suppressed — check suppression list for reason
- **Postmark handles rate limiting** automatically for optimal deliverability — 429 errors are rare
references/installation.md Reference
# SDK Installation

## Detect Project Language

| File | Language | SDK Package | Install Command |
|------|----------|-------------|-----------------|
| `package.json` | Node.js / TypeScript | `postmark` | `npm install postmark` |
| `requirements.txt` / `pyproject.toml` | Python | `postmarker` | `pip install postmarker` |
| `Gemfile` | Ruby | `postmark` | `gem install postmark` |
| `composer.json` | PHP | `wildbit/postmark-php` | `composer require wildbit/postmark-php` |
| `*.csproj` / `*.sln` | .NET | `Postmark` | `dotnet add package Postmark` |

## Node.js / TypeScript

```bash
npm install postmark
```

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);
```

Or with ES modules:

```typescript
import { ServerClient } from 'postmark';
const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);
```

**Source:** [github.com/ActiveCampaign/postmark.js](https://github.com/ActiveCampaign/postmark.js)

## Python

```bash
pip install postmarker
```

```python
import os
from postmarker.core import PostmarkClient

postmark = PostmarkClient(server_token=os.environ['POSTMARK_SERVER_TOKEN'])
```

**Source:** [github.com/Stranger6667/postmarker](https://github.com/Stranger6667/postmarker)

## Ruby

```bash
gem install postmark
```

```ruby
require 'postmark'

client = Postmark::ApiClient.new(ENV['POSTMARK_SERVER_TOKEN'])
```

**Source:** [github.com/ActiveCampaign/postmark-gem](https://github.com/ActiveCampaign/postmark-gem)

## PHP

```bash
composer require wildbit/postmark-php
```

```php
use Postmark\PostmarkClient;

$client = new PostmarkClient(getenv('POSTMARK_SERVER_TOKEN'));
```

**Source:** [github.com/ActiveCampaign/postmark-php](https://github.com/ActiveCampaign/postmark-php)

## .NET

```bash
dotnet add package Postmark
```

```csharp
using PostmarkDotNet;

var client = new PostmarkClient(Environment.GetEnvironmentVariable("POSTMARK_SERVER_TOKEN"));
```

**Source:** [github.com/ActiveCampaign/postmark-dotnet](https://github.com/ActiveCampaign/postmark-dotnet)

## Environment Variable

All SDKs should read the API token from the `POSTMARK_SERVER_TOKEN` environment variable:

```bash
export POSTMARK_SERVER_TOKEN=your-server-token-here
```

For testing, use the test token:

```bash
export POSTMARK_SERVER_TOKEN=POSTMARK_API_TEST
```
references/single-email-examples.md Reference
# Single Email Examples

Complete examples for sending a single email via `POST /email` in all supported SDKs.

## Node.js / TypeScript

### Basic Send

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const result = await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Subject: 'Hello from Postmark',
  TextBody: 'This is a plain text email.',
  HtmlBody: '<p>This is an <strong>HTML</strong> email.</p>',
  MessageStream: 'outbound'
});

console.log('Sent:', result.MessageID);
```

### With All Options

```javascript
const result = await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Cc: '[email protected]',
  Bcc: '[email protected]',
  ReplyTo: '[email protected]',
  Subject: 'Order Confirmation',
  TextBody: 'Your order #12345 is confirmed.',
  HtmlBody: '<h1>Order Confirmed</h1><p>Your order #12345 is confirmed.</p>',
  MessageStream: 'outbound',
  Tag: 'order-confirmation',
  Metadata: {
    customer_id: '12345',
    order_id: 'ORD-67890'
  },
  TrackOpens: true,
  TrackLinks: 'HtmlAndText',
  Headers: [
    { Name: 'X-Custom-Header', Value: 'custom-value' }
  ]
});
```

### With Attachments

```javascript
const fs = require('fs');

const result = await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Subject: 'Your Invoice',
  TextBody: 'Please find your invoice attached.',
  MessageStream: 'outbound',
  Attachments: [
    {
      Name: 'invoice.pdf',
      Content: fs.readFileSync('./invoice.pdf').toString('base64'),
      ContentType: 'application/pdf'
    }
  ]
});
```

### With Inline Image

```javascript
const result = await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Subject: 'Newsletter',
  HtmlBody: '<h1>Our Newsletter</h1><img src="cid:logo">',
  MessageStream: 'broadcast',
  Attachments: [
    {
      Name: 'logo.png',
      Content: fs.readFileSync('./logo.png').toString('base64'),
      ContentType: 'image/png',
      ContentID: 'cid:logo'
    }
  ]
});
```

## Python

### Basic Send

```python
from postmarker.core import PostmarkClient
import os

postmark = PostmarkClient(server_token=os.environ['POSTMARK_SERVER_TOKEN'])

result = postmark.emails.send(
    From='[email protected]',
    To='[email protected]',
    Subject='Hello from Postmark',
    TextBody='This is a plain text email.',
    HtmlBody='<p>This is an <strong>HTML</strong> email.</p>',
    MessageStream='outbound'
)

print('Sent:', result['MessageID'])
```

### With All Options

```python
result = postmark.emails.send(
    From='[email protected]',
    To='[email protected]',
    Cc='[email protected]',
    Bcc='[email protected]',
    ReplyTo='[email protected]',
    Subject='Order Confirmation',
    TextBody='Your order #12345 is confirmed.',
    HtmlBody='<h1>Order Confirmed</h1><p>Your order #12345 is confirmed.</p>',
    MessageStream='outbound',
    Tag='order-confirmation',
    Metadata={
        'customer_id': '12345',
        'order_id': 'ORD-67890'
    },
    TrackOpens=True,
    TrackLinks='HtmlAndText',
    Headers=[
        {'Name': 'X-Custom-Header', 'Value': 'custom-value'}
    ]
)
```

### With Attachments

```python
import base64

with open('./invoice.pdf', 'rb') as f:
    content = base64.b64encode(f.read()).decode('utf-8')

result = postmark.emails.send(
    From='[email protected]',
    To='[email protected]',
    Subject='Your Invoice',
    TextBody='Please find your invoice attached.',
    MessageStream='outbound',
    Attachments=[
        {
            'Name': 'invoice.pdf',
            'Content': content,
            'ContentType': 'application/pdf'
        }
    ]
)
```

## Ruby

### Basic Send

```ruby
require 'postmark'

client = Postmark::ApiClient.new(ENV['POSTMARK_SERVER_TOKEN'])

result = client.deliver(
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello from Postmark',
  text_body: 'This is a plain text email.',
  html_body: '<p>This is an <strong>HTML</strong> email.</p>',
  message_stream: 'outbound'
)

puts "Sent: #{result[:message_id]}"
```

## PHP

### Basic Send

```php
use Postmark\PostmarkClient;

$client = new PostmarkClient(getenv('POSTMARK_SERVER_TOKEN'));

$result = $client->sendEmail(
    '[email protected]',       // From
    '[email protected]',        // To
    'Hello from Postmark',          // Subject
    '<p>This is an <strong>HTML</strong> email.</p>', // HtmlBody
    'This is a plain text email.',  // TextBody
    null,                           // Tag
    true,                           // TrackOpens
    null,                           // ReplyTo
    null,                           // Cc
    null,                           // Bcc
    null,                           // Headers
    null,                           // Attachments
    'outbound'                      // MessageStream
);

echo 'Sent: ' . $result->MessageID;
```

## .NET

### Basic Send

```csharp
using PostmarkDotNet;

var client = new PostmarkClient(Environment.GetEnvironmentVariable("POSTMARK_SERVER_TOKEN"));

var message = new PostmarkMessage
{
    From = "[email protected]",
    To = "[email protected]",
    Subject = "Hello from Postmark",
    TextBody = "This is a plain text email.",
    HtmlBody = "<p>This is an <strong>HTML</strong> email.</p>",
    MessageStream = "outbound"
};

var result = await client.SendMessageAsync(message);
Console.WriteLine($"Sent: {result.MessageID}");
```

## cURL

### Basic Send

```bash
curl "https://api.postmarkapp.com/email" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "From": "[email protected]",
    "To": "[email protected]",
    "Subject": "Hello from Postmark",
    "TextBody": "This is a plain text email.",
    "HtmlBody": "<p>This is an <strong>HTML</strong> email.</p>",
    "MessageStream": "outbound"
  }'
```

### With Metadata and Tracking

```bash
curl "https://api.postmarkapp.com/email" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "From": "[email protected]",
    "To": "[email protected]",
    "Subject": "Order Shipped",
    "TextBody": "Your order has shipped.",
    "HtmlBody": "<p>Your order has shipped.</p>",
    "MessageStream": "outbound",
    "Tag": "order-shipped",
    "TrackOpens": true,
    "TrackLinks": "HtmlAndText",
    "Metadata": {
      "customer_id": "12345",
      "order_id": "ORD-67890"
    }
  }'
```
references/smtp-migration.md Reference
# SMTP Migration Guide

How to migrate from SMTP to the Postmark API, or use Postmark's SMTP interface.

## SMTP Settings

| Setting | Transactional | Broadcast |
|---------|--------------|-----------|
| **Host** | `smtp.postmarkapp.com` | `smtp-broadcasts.postmarkapp.com` |
| **Ports** | 25, 2525, or 587 | 25, 2525, or 587 |
| **TLS** | Required (STARTTLS) | Required (STARTTLS) |
| **Username** | Your Server API Token | Your Server API Token |
| **Password** | Your Server API Token | Your Server API Token |

## SMTP Authentication

Postmark supports two SMTP authentication methods:

### 1. API Token (Recommended)

Use your Server API Token as both username and password:

```
Username: your-server-api-token
Password: your-server-api-token
```

### 2. SMTP Tokens

Unique tokens per message stream, generated in the Postmark dashboard under Server → SMTP.

## Custom Headers via SMTP

Control Postmark features by adding custom headers to your SMTP messages:

| Header | Values | Description |
|--------|--------|-------------|
| `X-PM-Message-Stream` | `outbound`, `broadcast` | Message stream selection |
| `X-PM-Tag` | Any string (max 1000 chars) | Tag for filtering and stats |
| `X-PM-Track-Opens` | `true`, `false` | Enable/disable open tracking |
| `X-PM-Track-Links` | `None`, `HtmlAndText`, `HtmlOnly`, `TextOnly` | Link click tracking |
| `X-PM-Metadata-KEY` | Any string | Custom metadata (replace KEY with your key name) |

### Example Headers

```
X-PM-Message-Stream: outbound
X-PM-Tag: password-reset
X-PM-Track-Opens: true
X-PM-Track-Links: HtmlAndText
X-PM-Metadata-customer-id: 12345
X-PM-Metadata-order-id: ORD-67890
```

## Migration Examples

### Node.js (Nodemailer → Postmark API)

**Before (Nodemailer/SMTP):**

```javascript
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  host: 'smtp.old-provider.com',
  port: 587,
  auth: { user: 'username', pass: 'password' }
});

await transporter.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello',
  text: 'Hello World',
  html: '<p>Hello World</p>'
});
```

**After (Postmark API):**

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

await client.sendEmail({
  From: '[email protected]',
  To: '[email protected]',
  Subject: 'Hello',
  TextBody: 'Hello World',
  HtmlBody: '<p>Hello World</p>',
  MessageStream: 'outbound'
});
```

### Node.js (Nodemailer → Postmark SMTP)

If you prefer to keep using SMTP (e.g., existing infrastructure):

```javascript
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  host: 'smtp.postmarkapp.com',
  port: 587,
  secure: false,
  auth: {
    user: process.env.POSTMARK_SERVER_TOKEN,
    pass: process.env.POSTMARK_SERVER_TOKEN
  }
});

await transporter.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello',
  text: 'Hello World',
  html: '<p>Hello World</p>',
  headers: {
    'X-PM-Message-Stream': 'outbound',
    'X-PM-Tag': 'migration-test'
  }
});
```

### Python (smtplib → Postmark API)

**Before (smtplib/SMTP):**

```python
import smtplib
from email.mime.text import MIMEText

msg = MIMEText('Hello World')
msg['Subject'] = 'Hello'
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'

with smtplib.SMTP('smtp.old-provider.com', 587) as server:
    server.starttls()
    server.login('username', 'password')
    server.send_message(msg)
```

**After (Postmark API):**

```python
from postmarker.core import PostmarkClient
import os

postmark = PostmarkClient(server_token=os.environ['POSTMARK_SERVER_TOKEN'])

postmark.emails.send(
    From='[email protected]',
    To='[email protected]',
    Subject='Hello',
    TextBody='Hello World',
    HtmlBody='<p>Hello World</p>',
    MessageStream='outbound'
)
```

### Django (SMTP Backend → Postmark)

**Before (Django SMTP):**

```python
# settings.py
EMAIL_HOST = 'smtp.old-provider.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'username'
EMAIL_HOST_PASSWORD = 'password'
```

**After (Postmark SMTP via Django):**

```python
# settings.py
EMAIL_HOST = 'smtp.postmarkapp.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ['POSTMARK_SERVER_TOKEN']
EMAIL_HOST_PASSWORD = os.environ['POSTMARK_SERVER_TOKEN']
```

### Rails (ActionMailer → Postmark)

**Before (generic SMTP):**

```ruby
# config/environments/production.rb
config.action_mailer.smtp_settings = {
  address: 'smtp.old-provider.com',
  port: 587,
  user_name: 'username',
  password: 'password',
  authentication: :plain,
  enable_starttls_auto: true
}
```

**After (Postmark SMTP via ActionMailer):**

```ruby
# config/environments/production.rb
config.action_mailer.smtp_settings = {
  address: 'smtp.postmarkapp.com',
  port: 587,
  user_name: ENV['POSTMARK_SERVER_TOKEN'],
  password: ENV['POSTMARK_SERVER_TOKEN'],
  authentication: :plain,
  enable_starttls_auto: true
}
```

Or use the Postmark Rails gem for API-based sending:

```ruby
# Gemfile
gem 'postmark-rails'

# config/application.rb
config.action_mailer.delivery_method = :postmark
config.action_mailer.postmark_settings = {
  api_token: ENV['POSTMARK_SERVER_TOKEN']
}
```

## API vs SMTP

| Feature | API | SMTP |
|---------|-----|------|
| **Performance** | Faster (direct HTTP) | Slower (SMTP handshake) |
| **Batch sending** | Up to 500 per call | One at a time |
| **Error handling** | Structured JSON errors | SMTP error codes |
| **Metadata** | Native support | Via custom headers |
| **Templates** | Native support | Not available |
| **Migration effort** | Requires code changes | Minimal (change host/credentials) |

**Recommendation:** Use the API for new integrations. Use SMTP when migrating existing infrastructure with minimal changes.

## Key Notes

- SMTP and API use the same Server API Token
- Always use TLS (STARTTLS on ports 25, 2525, or 587)
- Use `smtp.postmarkapp.com` for transactional, `smtp-broadcasts.postmarkapp.com` for broadcast
- Set `X-PM-Message-Stream` header via SMTP to control stream selection
- API provides better performance, structured errors, and batch support
- SMTP is ideal for legacy systems or frameworks with built-in SMTP support
references/template-examples.md Reference
# Template Email Examples

Examples for sending emails using Postmark server-side templates.

## Endpoints

| Endpoint | Description |
|----------|-------------|
| `POST /email/withTemplate` | Send single email with template |
| `POST /email/batchWithTemplates` | Send batch with templates (up to 500) |

## Node.js / TypeScript

### Send with Template ID

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const result = await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateId: 12345,
  TemplateModel: {
    name: 'Jane Doe',
    order_id: 'ORD-67890',
    items: [
      { name: 'Widget', quantity: 2, price: '$19.99' },
      { name: 'Gadget', quantity: 1, price: '$29.99' }
    ],
    total: '$69.97'
  },
  MessageStream: 'outbound'
});

console.log('Sent:', result.MessageID);
```

### Send with Template Alias (Recommended)

```javascript
const result = await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateAlias: 'order-confirmation',
  TemplateModel: {
    name: 'Jane Doe',
    order_id: 'ORD-67890'
  },
  MessageStream: 'outbound',
  Tag: 'order-confirmation',
  Metadata: {
    customer_id: '12345',
    order_id: 'ORD-67890'
  }
});
```

### Batch with Templates

```javascript
const results = await client.sendEmailBatchWithTemplates([
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: {
      name: 'User 1',
      action_url: 'https://app.yourdomain.com/start'
    },
    MessageStream: 'outbound'
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: {
      name: 'User 2',
      action_url: 'https://app.yourdomain.com/start'
    },
    MessageStream: 'outbound'
  }
]);

results.forEach((result, index) => {
  if (result.ErrorCode === 0) {
    console.log(`Email ${index + 1}: sent (${result.MessageID})`);
  } else {
    console.error(`Email ${index + 1}: failed (${result.Message})`);
  }
});
```

## Python

### Send with Template

```python
from postmarker.core import PostmarkClient
import os

postmark = PostmarkClient(server_token=os.environ['POSTMARK_SERVER_TOKEN'])

result = postmark.emails.send_with_template(
    From='[email protected]',
    To='[email protected]',
    TemplateAlias='order-confirmation',
    TemplateModel={
        'name': 'Jane Doe',
        'order_id': 'ORD-67890',
        'items': [
            {'name': 'Widget', 'quantity': 2, 'price': '$19.99'},
            {'name': 'Gadget', 'quantity': 1, 'price': '$29.99'}
        ],
        'total': '$69.97'
    },
    MessageStream='outbound'
)

print('Sent:', result['MessageID'])
```

## Ruby

### Send with Template

```ruby
require 'postmark'

client = Postmark::ApiClient.new(ENV['POSTMARK_SERVER_TOKEN'])

result = client.deliver_with_template(
  from: '[email protected]',
  to: '[email protected]',
  template_alias: 'order-confirmation',
  template_model: {
    name: 'Jane Doe',
    order_id: 'ORD-67890'
  },
  message_stream: 'outbound'
)

puts "Sent: #{result[:message_id]}"
```

## cURL

### Send with Template

```bash
curl "https://api.postmarkapp.com/email/withTemplate" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "From": "[email protected]",
    "To": "[email protected]",
    "TemplateAlias": "order-confirmation",
    "TemplateModel": {
      "name": "Jane Doe",
      "order_id": "ORD-67890",
      "items": [
        { "name": "Widget", "quantity": 2, "price": "$19.99" }
      ],
      "total": "$19.99"
    },
    "MessageStream": "outbound"
  }'
```

### Batch with Templates

```bash
curl "https://api.postmarkapp.com/email/batchWithTemplates" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "Messages": [
      {
        "From": "[email protected]",
        "To": "[email protected]",
        "TemplateAlias": "welcome-email",
        "TemplateModel": { "name": "User 1" },
        "MessageStream": "outbound"
      },
      {
        "From": "[email protected]",
        "To": "[email protected]",
        "TemplateAlias": "welcome-email",
        "TemplateModel": { "name": "User 2" },
        "MessageStream": "outbound"
      }
    ]
  }'
```

## Handlebars Syntax Quick Reference

### Variables

```handlebars
{{name}}              <!-- Escaped output -->
{{{html_content}}}    <!-- Unescaped HTML -->
```

### Conditionals

```handlebars
{{#if premium_member}}
  <p>Premium features included.</p>
{{else}}
  <p>Upgrade for more features.</p>
{{/if}}
```

### Iteration

```handlebars
{{#each items}}
  <tr>
    <td>{{this.name}}</td>
    <td>{{this.price}}</td>
  </tr>
{{/each}}
```

### Nested Objects

```handlebars
{{customer.name}}
{{customer.address.city}}
```

## Key Notes

- Use `TemplateAlias` (string) over `TemplateId` (integer) for portability across environments
- Provide either `TemplateId` or `TemplateAlias`, not both
- `TemplateModel` keys must match the Handlebars variables in your template
- Missing variables render as empty strings — validate your model data
- Templates support the same optional parameters as regular sends (Tag, Metadata, Tracking, etc.)
- Batch templates support up to 500 messages per call

Version History

v1.0.0 Imported from GitHub
1 week ago