Migrate from Postmark to Loops
This guide moves your transactional email from Postmark to Loops. You will swap the SDK, move template content into published Loops templates, keep your send function signatures close to what they are today, and cut over without hurting deliverability. The transactional model maps almost one to one. The main structural change is that template content moves out of your code and into published Loops templates.
Migration guides
Should you do this?
Postmark is transactional-first and well regarded for that job. It is organized around Servers and Message Streams, with a transactional stream and broadcast streams. If your only requirement is a low-level transactional stream and you do not want a marketing side, staying on a transactional-focused provider is a reasonable choice, and this guide will not pretend otherwise.
Loops fits when you want transactional email and marketing email in one place: transactional templates for account mail, plus campaigns, contacts, mailing lists, and event-triggered automations for lifecycle and marketing. If you are consolidating both jobs onto one platform, the migration is straightforward.
The one structural change to plan for: in Postmark you can send raw HTML from code, or send a template with a TemplateModel. In Loops, the subject, body, and design live in a published template, and your code passes only dynamic values as dataVariables. Copy edits stop needing a code deploy. The tradeoff is that you keep the template variable names and your code in sync, because dataVariable names are case-sensitive and must match the template exactly.
Swap the packages
Remove the Postmark SDK and install the Loops SDK. The package is named loops, not loops-so or loops-js.
API mapping
Most of the migration is a direct swap.
Template send. Postmark POST /email/withTemplate (or client.sendEmailWithTemplate) becomes Loops POST /v1/transactional (or loops.sendTransactionalEmail). The Postmark TemplateId or TemplateAlias becomes the Loops transactionalId. The TemplateModel merge fields become Loops dataVariables. There is no HtmlBody at send time, because the content lives in the published Loops template.
Auth. Postmark authenticates each request with the X-Postmark-Server-Token header, a per-server token. Loops uses a single team API key sent as Authorization: Bearer YOUR_API_KEY. Keep it server-side, because browser calls are blocked by CORS by design.
Templates. Postmark templates use Mustachio {{ }} merge fields against the TemplateModel. Recreate each template in Loops, publish it, and map the model keys to dataVariables of the same name.
Streams. Postmark separates a transactional stream from broadcast streams. Keep that separation in Loops: transactional templates for the transactional stream, and campaigns for broadcast streams.
Contacts. Postmark does not hold a marketing contact database the way an audience does. For broadcast streams it tracks subscriptions and suppressions. In Loops, recipient lists become mailing lists (GET /v1/lists), and contacts are created or updated with POST /v1/contacts/create and PUT /v1/contacts/update.
Events. Delivery, bounce, and open webhooks in Postmark map to Loops webhooks, which expose email.delivered, email.softBounced, email.hardBounced, and email.spamReported events (alongside email.opened, email.clicked, and email.unsubscribed) for monitoring.
Before and after
Before, a Postmark template send:
After, the same send with Loops:
The From address and the subject now live in the published Loops template, so they leave your code. The variable names (firstName, resetUrl) must match the names you define in the template, exactly, including case.
Environment
Replace the per-server token with the Loops API key.
Each transactionalId comes from the Loops dashboard Transactional section (the Publish page shows the id and the expected payload), or from GET /v1/transactional-emails. Store the ids you use, or read them at startup.
Dashboard setup, the part code can’t do
Some steps happen in the Loops dashboard, not in code.
Create and test the API key. Generate a key under Settings -> API. Confirm it works with GET /v1/api-key, which returns { success: true, teamName }.
Verify your sending domain. Postmark authentication (DKIM, a Return-Path CNAME, and SPF) does not carry over. In Loops, add the SPF, DKIM, MX, and default DMARC records Loops shows during sending-domain setup, then verify from the domain settings page. DNS changes can take up to an hour to propagate, so do this first. Sends only work from a verified domain.
Recreate each template and publish it. Rebuild each Postmark template in Loops, map every {{ }} merge field to a dataVariable of the same case-sensitive name, and publish. A draft cannot send: POST /v1/transactional returns 400 if the transactional email is not published.
Create mailing lists and copy their ids. If you send broadcast streams, create the matching mailing lists in Loops and copy each list id from GET /v1/lists for use in mailingLists objects.
For contacts, there is no public bulk-contact-import API. Bulk import is done in the dashboard under Audience -> import CSV. For a programmatic move, loop over POST /v1/contacts/create or PUT /v1/contacts/update per contact, staying within the rate limit.
Cutover checklist
Carry suppressions over from day one. Export the Postmark suppression list for each stream (the suppressions dump), and honor those addresses in Loops. Loops also auto-suppresses hard bounces and complaints, and you can inspect or remove a suppressed contact with GET /v1/contacts/suppression and DELETE /v1/contacts/suppression.
Run both providers in parallel, then cut over per email type. Keep Postmark live, move one email type at a time (password reset first, then receipts, and so on), and watch delivery events for each before moving the next. Do not switch everything at once.
Mind the marketing audience flag. POST /v1/transactional accepts addToAudience. Leave it off for account mail like password resets. Set it to true only when you intend to add the recipient to your marketing audience.
Respect the rate limit. Loops allows 10 requests per second per team and returns 429 when you exceed it, so back off and retry. For sends and events you can pass an Idempotency-Key header (up to 100 characters); a key reused within 24 hours returns 409, which protects against duplicate sends on retry.
Keep template variables and code in sync. Because dataVariable names are case-sensitive and must match the published template, a rename in one place needs the same rename in the other. A missing required variable fails with 400.
Common questions
Can I keep sending raw HTML from code like Postmark's HtmlBody?
How long does the migration take?
Will my Postmark suppressions carry over?