Migration

Migrate from Postmark

Keep the postmark SDK. Override its request host, use a Sendly key as the server token, and single and batch sends keep working unchanged.

Last updated 2026-09-04

What Sendly implements

Postmark's POST /email and POST /email/batch. Success comes back in Postmark's own shape — To, SubmittedAt, MessageID, ErrorCode: 0, Message — so code that reads MessageID off the response keeps reading it.

SettingValue
Request hostapi.sendly.now/api/compat/postmark
Auth headerX-Postmark-Server-Token: sk_…
HTTPSuseHttps: true

Postmark's Node client takes a request host rather than a base URL, and that value is host plus path with no scheme. The dialect prefix therefore hangs off the host, which looks odd the first time and is correct.

The change

Node
import * as postmark from "postmark";

const client = new postmark.ServerClient(process.env.SENDLY_API_KEY, {
  useHttps: true,
  requestHost: "api.sendly.now/api/compat/postmark",
});

// every call site below is untouched
await client.sendEmail({
  From: "you@yourdomain.com",
  To: "customer@example.com",
  Subject: "Hello",
  HtmlBody: "<p>Welcome aboard.</p>",
});

Postmark publishes no official Python client. A Python codebase has three options, and the third is usually the least work: call the compat endpoint over plain HTTP, run the Node SDK in a small service, or move straight to the Sendly Python SDK and skip the dialect entirely.

What carries over, and what is quietly accepted

FieldSupportNote
FromFullPlain address or the Name <you@domain.com> display form. Must be on a verified domain.
To / Cc / BccFullComma-separated lists, exactly as Postmark sends them.
HtmlBody / TextBodyFullHtmlBody wins when both are present; at least one is required.
ReplyTo, HeadersFullHeaders in Postmark's [{ Name, Value }] form.
AttachmentsFullName, Content, ContentType and ContentID, so inline images survive.
TagPartialA single tag, sanitized.
MessageStream, TrackOpens, TrackLinksAccepted and ignoredThe request succeeds; the setting has no effect. This is the one place the dialect does not tell you it could not do something.

That last row is the one to read twice. If your integration relies on TrackOpens or on separating streams, the send will succeed and the behaviour will not be there. Sendly has its own tracking and its own way of separating traffic; configure it on the Sendly side rather than expecting the Postmark field to carry across.

What does not come across

Template sends. sendEmailWithTemplate, and anything carrying TemplateId or TemplateModel, are not implemented in this phase and return a clean Postmark-shaped error rather than sending something unexpected.

Errors use Postmark's { ErrorCode, Message } body with the matching status — a bad token is a 401 with ErrorCode 10 — so the SDK's own error classes, InvalidAPIKeyError and ApiInputError among them, are thrown exactly as they are today.

Before you switch a production sender

Verify your domain and import your suppression list first, then move one slice at a time. The staged rollout plan is the seven steps we would follow, in the order that keeps each one reversible.

The full supported-field table lives in the Postmark migration guide in the docs.

Questions people ask

Why is the path part of the host?
Because the Postmark Node client takes a requestHost, which is host plus path without a scheme, rather than a base URL. So the dialect prefix is written as api.sendly.now/api/compat/postmark and useHttps: true supplies the scheme separately.
Does batch send work?
Yes. POST /email/batch returns an array of results with order preserved, so an index into that array still means what your code thinks it means.
What happens to MessageStream?
It is accepted and ignored: the send succeeds and the stream setting has no effect. If you use streams to separate transactional from broadcast traffic, set that up on the Sendly side before you move, rather than assuming the field carries across.
Can I use Postmark templates?
Not through the compat endpoint. Template sends are not implemented in this phase and return a clean Postmark error. Rebuild the templates in Sendly and send by Sendly template id, or move the markup into the request body.

Something on this page out of date, or missing the case you are in? Write to support@sendly.now — we answer our own email. The full technical detail lives in the documentation.