# Migrate from Postmark

> Move from Postmark to Emailit. Map servers, message streams and tokens, convert API fields, swap SMTP settings, and move webhooks, suppressions and templates.

This guide maps Postmark concepts, API calls, webhooks, suppressions and templates to their Emailit equivalents. Read [Migrate to Emailit](/docs/migrate/) first for the overall order and how to run both providers in parallel.

## Concepts

| Postmark | Emailit |
| --- | --- |
| Account | Account |
| Server | [Workspace](/docs/workspaces/), or one workspace with several sending domains |
| Server API token | **Sending Only** [API key](/docs/developers/api-keys/), optionally restricted to one domain |
| Account API token | **Full Access** API key |
| Transactional message stream | The [Email API](/docs/email-api/) and [SMTP relay](/docs/smtp/) |
| Broadcast message stream | [Campaigns](/docs/campaigns/) to [audiences](/docs/audiences/), or the API with your own `List-Unsubscribe` header |
| Inbound message stream | [Inbound email](/docs/inbound/) on a subdomain such as `inbound.acme.com` |
| Sender signatures and domains | [Sending domains](/docs/domains/). Single-address sender signatures aren't available. |
| Templates and layouts | [Templates](/docs/templates/) with an alias and versions. There are no layouts. |
| Webhooks per stream | [Webhooks](/docs/webhooks/) per workspace |
| Suppressions per stream | One [suppression list](/docs/suppressions/) per workspace |
| Activity | **Email API → Emails** and **Email API → Logs** |
| `Tag` and `Metadata` | `meta` |

To keep transactional and marketing reputation apart, send them from different domains or subdomains, such as `mail.acme.com` for receipts and `news.acme.com` for newsletters.

## Update your API calls

Postmark's `POST /email` with an `X-Postmark-Server-Token` becomes `POST /v2/emails` with a bearer token. Field names change from PascalCase to snake_case:

```bash title="Before: Postmark"
curl https://api.postmarkapp.com/email \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "From": "Acme <hello@acme.com>",
    "To": "ada@example.com",
    "Subject": "Your receipt",
    "TextBody": "Thanks for your order.",
    "HtmlBody": "<p>Thanks for your order.</p>",
    "MessageStream": "outbound"
  }'
```

```bash title="After: Emailit"
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@acme.com>",
    "to": "ada@example.com",
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "html": "<p>Thanks for your order.</p>"
  }'
```

| Postmark | Emailit |
| --- | --- |
| `X-Postmark-Server-Token` header | `Authorization: Bearer secret_…` |
| `From` | `from` |
| `To`, `Cc`, `Bcc` (comma-separated strings) | `to`, `cc`, `bcc` as a string or an array of up to 50 each |
| `ReplyTo` | `reply_to` |
| `Subject` | `subject` |
| `HtmlBody`, `TextBody` | `html`, `text` |
| `Headers: [{ "Name": "…", "Value": "…" }]` | `headers: { "Name": "Value" }` |
| `Metadata`, `Tag` | `meta`, returned in webhook events |
| `TrackOpens`, `TrackLinks` | `tracking: { "loads": true, "clicks": true }` |
| `Attachments[]` with `Name`, `Content`, `ContentType`, `ContentID` | `attachments[]` with `filename`, `content`, `content_type`, `content_id` |
| `MessageStream` | Not needed |
| `POST /email/withTemplate` with `TemplateAlias` or `TemplateId` and `TemplateModel` | The same `POST /v2/emails` with `template` (an alias or ID) and `variables` |
| Response with `MessageID` and `ErrorCode: 0` | `200` with `id` (`em_…`), `status: "accepted"` and `ids` per recipient. Errors use HTTP status codes. |

Emailit has no batch endpoint. Send one request per message, each with up to 50 recipients, and add an `Idempotency-Key` header so retries are safe. See [Send an email](/docs/email-api/send-email/).

## Switch SMTP settings

| Setting | Postmark | Emailit |
| --- | --- | --- |
| Host | `smtp.postmarkapp.com` | `smtp.emailit.com` |
| Port | `587`, `2525` or `25` | `587` (STARTTLS), `465` (TLS), `2525`, `2587` or `25` |
| Username | Your server API token | `emailit` |
| Password | Your server API token | Your Emailit API key |

Emailit doesn't read `X-PM-*` headers such as `X-PM-Message-Stream` or `X-PM-Tag`. Remove them, and set tracking on the domain instead. See [SMTP settings](/docs/smtp/settings/).

## Map webhook events

| Postmark webhook | Emailit event |
| --- | --- |
| Delivery | `email.delivered` |
| Bounce, hard bounce types | `email.bounced` |
| Bounce, soft or transient types | `email.attempted` while Emailit retries, then `email.bounced` if every retry fails |
| Spam complaint | `email.complained` |
| Open | `email.loaded` |
| Click | `email.clicked` |
| Subscription change | `email.unsubscribed` for campaign email, and `suppression.created` or `suppression.deleted` for suppressions you add or remove with the API |
| Inbound | `email.received`, then fetch the content with [`GET /emails/{id}`](/docs/api-reference/emails/get/) |

Emailit also sends `email.accepted` when the API accepts an email, which Postmark has no webhook for.

The request format changes:

- Postmark posts one record per request and names it in `RecordType`. Emailit posts a JSON array of up to 100 events, with the name in `type` and the email in `data.object`.
- Use `data.object.id`, the `em_` ID from the send response, instead of `MessageID`. Your `meta` values are in `data.object.meta`.
- Postmark webhooks are usually protected with basic authentication credentials in the URL. Emailit signs every request instead: verify `X-Emailit-Signature` against `X-Emailit-Timestamp` and your `whsec_` secret. See [Request signature](/docs/webhooks/request-signature/).

```javascript
for (const event of req.body) {
  const email = event.data.object;
  if (event.type === 'email.bounced') markBounced(email.to, email.id);
  if (event.type === 'email.complained') unsubscribe(email.to);
}
```

## Move suppressions

1. Export the suppressions of each Postmark message stream you send from, from the stream's suppressions page or with the suppressions dump API. Include hard bounces, spam complaints and manual suppressions.

2. Build one CSV with the columns `email,type,reason`:

```csv
email,type,reason
old-address@example.com,recipient,postmark hard bounce
angry@example.com,recipient,postmark spam complaint
```

   Use the type `recipient` for addresses that must never receive email. It blocks API, SMTP and campaign sends. For people who only opted out of your broadcasts, use the type `unsubscribe`, which stops campaigns but still lets transactional email through.

3. In **Email API → Suppressions**, select **Import** and upload the file. Each file can have up to 10,000 rows and can be at most 8 MB. Duplicates are skipped.

See [Manage suppressions](/docs/suppressions/manage/).

## Move templates

Copy each template's HTML from Postmark, including its layout, then import it in **Email Marketing → Templates** or create it with the [Templates API](/docs/api-reference/templates/create/). Emailit has no layouts, so merge the layout and the content into one template. Use the same alias you used in Postmark so your code changes stay small.

Postmark templates use Mustachio. Temple covers simple values and conditions:

| Postmark (Mustachio) | Emailit (Temple) |
| --- | --- |
| `{{name}}` | `{{name}}` |
| `{{company.name}}` | `{{company.name}}` |
| `{{#company}}…{{/company}}` sections | `{{#if company}}…{{/if}}`, with full paths such as `{{company.name}}` inside |
| `{{^name}}…{{/name}}` inverted sections | `{{#if name}}{{else}}…{{/if}}` |
| `{{#each items}}…{{/each}}` | Not supported. Render the list in your code and pass it as one variable. |
| `TemplateModel` | `variables` |

Temple never escapes HTML, so escape user input before you pass it. A missing value renders as an empty string unless you add a default, such as `{{name|"there"}}`. See [Temple](/docs/templates/temple/).

## Change DNS

Add your domain in **Email API → Domains** and publish the Emailit records. They use their own names (`emailit._domainkey`, `emailit.<domain>`, and optionally `go` and `inbound`), so they don't conflict with Postmark's DKIM record or its `pm-bounces` return-path CNAME. Keep your DMARC record. After the cutover, remove the Postmark records. See [DNS records](/docs/domains/dns-records/).

If you process inbound mail with Postmark, move it to an Emailit inbound subdomain and update the addresses your app gives out. See [Set up inbound](/docs/inbound/set-up/).

## Next steps

- [Go-live checklist](/docs/get-started/go-live/)
- [Set up webhooks](/docs/webhooks/set-up/)
- [Priority migration](/docs/programs/priority-migration/): let Emailit engineers do the move with you

---
Source: https://emailit.com/docs/migrate/postmark/
