---
title: "Switch from Postmark"
description: "Keep the Postmark library and send through Nextmail. Change its host and its server token, and your sending code stays as it is."
url: "https://nextmail.sh/docs/knowledge/developers/switch-from-postmark"
area: "Knowledge base"
category: "Developers"
status: "live"
---

# Switch from Postmark

Keep the Postmark library and send through Nextmail. Change its host and its server token, and your sending code stays as it is.

## What to change

Point the library at https://api.nextmail.sh/compat/postmark and put an API key from Nextmail with the `emails:send` permission where the server token goes. It travels in the same `X-Postmark-Server-Token` header. Your calls that send mail stay as they are, and the From address decides whether a message may go out, as it does everywhere in Nextmail.

**Point the Postmark library at Nextmail**

_Node.js_

```nodejs
import { ServerClient } from 'postmark'

const client = new ServerClient(process.env.NEXTMAIL_API_KEY, {
  requestHost: 'api.nextmail.sh/compat/postmark',
})

await client.sendEmail({
  From: 'billing@acme.com',
  To: 'ada@example.com',
  Subject: 'Your invoice',
  HtmlBody: '<p>Your invoice is attached.</p>',
  MessageStream: 'outbound',
})
```

_Python_

```python
import os
import postmark.sync

with postmark.sync.ServerClient(
    os.environ["NEXTMAIL_API_KEY"],
    base_url="https://api.nextmail.sh/compat/postmark",
) as client:
    client.outbound.send({
        "sender": "billing@acme.com",
        "to": "ada@example.com",
        "subject": "Your invoice",
        "html_body": "<p>Your invoice is attached.</p>",
        "message_stream": "outbound",
    })
```

_Ruby_

```ruby
require 'postmark'

client = Postmark::ApiClient.new(
  ENV['NEXTMAIL_API_KEY'],
  host: 'api.nextmail.sh',
  path_prefix: '/compat/postmark/'
)
client.deliver(
  from: 'billing@acme.com',
  to: 'ada@example.com',
  subject: 'Your invoice',
  html_body: '<p>Your invoice is attached.</p>',
  message_stream: 'outbound'
)
```

_PHP_

```php
<?php

use Postmark\PostmarkClient;

PostmarkClient::$BASE_URL = 'https://api.nextmail.sh/compat/postmark';

$client = new PostmarkClient(getenv('NEXTMAIL_API_KEY'));
$client->sendEmail(
    'billing@acme.com',
    'ada@example.com',
    'Your invoice',
    '<p>Your invoice is attached.</p>',
    messageStream: 'outbound',
);
```

_curl_

```bash
curl -X POST https://api.nextmail.sh/compat/postmark/email \
  -H "X-Postmark-Server-Token: $NEXTMAIL_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "From": "billing@acme.com",
    "To": "ada@example.com",
    "Subject": "Your invoice",
    "HtmlBody": "<p>Your invoice is attached.</p>",
    "MessageStream": "outbound"
  }'
```

> In Node, `requestHost` is the host and the path together, with no scheme and no trailing slash. In Ruby, `path_prefix` needs a slash at both ends. In Python, use the official postmark-python package with `base_url`. The community postmarker package cannot reach a path below a host, so it does not work here. In PHP, `PostmarkClient::$BASE_URL` takes the scheme, the host and the path with no trailing slash. It is static, so it applies to every Postmark client in the process, `PostmarkAdminClient` included.

## What maps to what

`POST /email`, `/email/batch`, `/email/withTemplate` and `/email/batchWithTemplates` are the endpoints served. Field names match in any case, as they do at Postmark, and an empty string counts as left out.

| Postmark | In Nextmail |
| --- | --- |
| `From` | The sender, with its name. |
| `To` | Recipients separated by commas. With `Cc` and `Bcc`, up to 50 per message. |
| `ReplyTo` | One reply-to address. |
| `Subject` | The subject. |
| `HtmlBody` | The HTML body. `TextBody` becomes the text body, and one of the two is required. |
| `Headers` | Custom headers given as `Name` and `Value`: `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority and Feedback-ID. |
| `Attachments` | Files, 20 at most and 10 MB in all. An image whose `ContentID` the HTML uses as `cid:` is embedded where it appears. Any other file arrives as an ordinary attachment. |
| `Tag` | A tag named `tag`. |
| `Metadata` | Tags with the same names and values. With `Tag`, at most 10 per message. |
| `TrackOpens` | Turns open tracking on or off for the message. |
| `TrackLinks` | `HtmlAndText` and `HtmlOnly` turn click tracking on, and `None` turns it off. |
| `MessageStream` | `outbound`, or the id of any other transactional stream, sends the message as usual. `broadcast` sends it on the broadcast lane. |
| `TemplateAlias` | The slug or id (`tpl_...`) of a template in Nextmail, filled in from `TemplateModel`. `InlineCss` is accepted and changes nothing. |

## What is refused, and why

- `TemplateId`, with ErrorCode 1101. A Postmark template id means nothing here, so recreate the template in Nextmail and send its slug or id as `TemplateAlias`.
- The `broadcast` stream while the broadcast lane of the workspace is paused for too many bounces or complaints, with ErrorCode 1236. Otherwise a message on it travels on the broadcast lane, apart from app mail.
- `Subject`, `HtmlBody` or `TextBody` on a templated message, with ErrorCode 1123, because the template supplies them. `TrackLinks` set to `TextOnly`, because Nextmail tracks the links in the HTML part.
- More than one reply-to address, a header given twice or outside the list above, more than 10 tags, and a tag or `Metadata` name other than letters, digits, `_` and `-`.
- A batch of more than 100 messages, with ErrorCode 410. Postmark takes 500, so split larger batches.

## Responses and errors

- A send answers 200 with `To`, `SubmittedAt`, `MessageID`, `ErrorCode` 0 and `Message` OK. `MessageID` is the Nextmail message id, the one `GET /emails/{id}` and webhooks use. An `Idempotency-Key` header works as it does on the rest of the API.
- A batch answers 200 with one result per message, in order. A message that failed carries only its `ErrorCode` and `Message`, and the others still go out.
- Errors come back as `ErrorCode` and `Message`. A missing or unknown key, or one without `emails:send`, answers HTTP 401 with ErrorCode 10. The rest answer HTTP 422: ErrorCode 300 for the message itself, 400 for a From address the key may not use, 401 for a domain that cannot send yet, 402 for a body that is not JSON and 405 for a workspace that has used up its sending allowance. HTTP 413 means the body is over 10 MB, or 50 MB for a batch, or the attachments are over 5 MB.

## Bring your account across

Migrations → From a provider reads a Postmark account with a Server token and brings across what can leave it. Running it twice duplicates nothing, and the token is held encrypted only while the import runs.

- Suppressions arrive from every outbound stream with their reason: hard bounces as bounces, spam complaints as complaints and manual suppressions as manual.
- Standard templates arrive with their layout merged in. A template whose layout is missing arrives with a warning.
- Each webhook is registered with the same URL and the nearest matching events, switched off, because deliveries here are signed differently.
- Server tokens come back by name so you can create their replacements. Domains and sender signatures need an Account token, which the import does not take, so add those domains here yourself. Postmark keeps no contact lists, so there are none to bring.
