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

# Switch from Postmark

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

## What to change

Point the library at https://api.openemail.uk/compat/postmark and put an OpenEmail API key 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 OpenEmail.

**Point the Postmark library at OpenEmail**

_Node.js_

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

const client = new ServerClient(process.env.OPENEMAIL_API_KEY, {
  requestHost: 'api.openemail.uk/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["OPENEMAIL_API_KEY"],
    base_url="https://api.openemail.uk/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['OPENEMAIL_API_KEY'],
  host: 'api.openemail.uk',
  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.openemail.uk/compat/postmark';

$client = new PostmarkClient(getenv('OPENEMAIL_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.openemail.uk/compat/postmark/email \
  -H "X-Postmark-Server-Token: $OPENEMAIL_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 OpenEmail |
| --- | --- |
| `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 5 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. |
| `TemplateAlias` | The slug or id (`tpl_...`) of an OpenEmail template, 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 OpenEmail and send its slug or id as `TemplateAlias`.
- The `broadcast` stream, with ErrorCode 1236. These endpoints send transactional mail, and newsletters go out as OpenEmail broadcasts.
- `Subject`, `HtmlBody` or `TextBody` on a templated message, with ErrorCode 1123, because the template supplies them. `TrackLinks` set to `TextOnly`, because OpenEmail 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 OpenEmail 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.
