Skip to content
Browse the docs

Integrating · Published 2026-09-28 · Updated 2026-09-29 · AgentiSend

Migrating from Postmark

How a Postmark payload and webhook map onto this API, and what is refused on day one.

The request shape is the same

Postmark's send is POST /email with From, To, Subject, HtmlBody, TextBody and MessageStream. Ours is POST /emails with from, to, subject, html and text. The names differ; the job is the same single message. The full shape is on the POST /emails page.

Headers

headers takes a map of name to value. List-Unsubscribe and List-Unsubscribe-Post pass through when from is on a domain you have verified here. From, To, Subject, Message-ID and Date are refused in headers.

Error names resolve

Postmark's error names are not aliases here. Branch on our code. Every refusal carries message, fix and docs_url. The catalogue is at /docs/errors.

Payload map

Fetched from Postmark's send guide on 2026-09-28 (https://postmarkapp.com/developer/user-guide/send-email-with-api).

PostmarkAgentiSend
Fromfrom
Toto
Subjectsubject
HtmlBodyhtml
TextBodytext
MessageStreamNo equivalent field. Transactional and broadcast sends are different calls.
Idempotency keyNot on their API. Send Idempotency-Key here; it is remembered for 7 days.

Webhook events

Their overview, fetched 2026-09-28, names Delivery, Bounce, Spam complaint, Open, Click, Subscription change and Inbound. It also says they do not sign webhooks with HMAC. Ours are signed. A type with no row below was not on that page.

Our eventTheir event
email.queuedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.sentNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.deliveredDelivery
email.delivery_delayedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.bouncedBounce
email.complainedSpam complaint
email.openedOpen tracking
email.clickedClick
email.failedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.scheduledNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.canceledNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.suppressedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
domain.createdNot named on the pages fetched on 2026-09-28. This row does not guess a name.
domain.verifiedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
domain.failedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
domain.updatedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
domain.deletedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
suppression.addedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
suppression.removedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
limit.warningNot named on the pages fetched on 2026-09-28. This row does not guess a name.
limit.exceededNot named on the pages fetched on 2026-09-28. This row does not guess a name.
agent.approval_requestedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
agent.approvedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
agent.rejectedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
agent.killedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
contact.createdNot named on the pages fetched on 2026-09-28. This row does not guess a name.
contact.updatedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
contact.deletedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
segment.joinedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
segment.leftNot named on the pages fetched on 2026-09-28. This row does not guess a name.
segment.createdNot named on the pages fetched on 2026-09-28. This row does not guess a name.
segment.updatedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
segment.deletedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
broadcast.scheduledNot named on the pages fetched on 2026-09-28. This row does not guess a name.
broadcast.sentNot named on the pages fetched on 2026-09-28. This row does not guess a name.
broadcast.updatedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
automation.triggeredNot named on the pages fetched on 2026-09-28. This row does not guess a name.
email.receivedInbound webhook. Inbound mail is not offered here, so do not subscribe email.received expecting new mail.
topic.createdNot named on the pages fetched on 2026-09-28. This row does not guess a name.
topic.updatedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
topic.deletedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
contact.subscribedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
contact.unsubscribedSubscription change
trust.warningNot named on the pages fetched on 2026-09-28. This row does not guess a name.
trust.throttledNot named on the pages fetched on 2026-09-28. This row does not guess a name.
trust.pausedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
trust.appealedNot named on the pages fetched on 2026-09-28. This row does not guess a name.
trust.restoredNot named on the pages fetched on 2026-09-28. This row does not guess a name.

What will refuse on day one

  • Inbound mail is not offered. A migration that depends on receiving mail at your domain does not have that path here.
  • Keeping customer data in the EU is not offered. Data is processed in the United States.
  • There is no business associate agreement for health data.
  • A key is created with a send budget. A key that sends past the budget is refused with agent_budget_exceeded rather than invoiced.

The compatibility client, when the import line is all you want to change

That client mirrors one provider's SDK (agentisend/compat-resend). It does not mirror this provider. Call POST /emails with from, to, subject, html and text.

Contacts

POST /contacts accepts first_name, last_name, unsubscribed and properties. unsubscribed: true records that the person asked not to receive marketing mail. GET, PATCH and DELETE /contacts/{id} take the contact id or the email address. The Postmark pages fetched on 2026-09-28 cover sending and webhooks. They do not document a contact-import field map, so this guide does not invent one. Add each person with POST /contacts. An API key cannot lift an unsubscribe.

The bounce return path

A domain that already sends from somewhere else often has an MX record on send. Creating it here without a return-path label then uses bounce instead of refusing the domain. To choose the label, pass custom_return_path on POST /domains.

Two differences, both deliberate

Keys have budgets, and the budget is enforced. Set the budget to your real volume before you cut over.

Inbound mail and an EU data region are not part of the move. If the old account uses either, that part stays on the old provider.

What you gain in the move

  • Per-key budgets and one kill switch for the account, on Budgets and the kill switch.
  • An approval queue for a send the loop guard flags, or one an agent asks about with request_approval, on Approvals.
  • Signed webhooks, a replay call, and a dead-letter list, on Webhooks.

Order of operations

  1. Add and verify your domains here while the old provider still sends.
  2. Create keys with budgets, one per sender, and point webhooks at the new signing scheme.
  3. Move a low-volume stream first.
  4. Move the rest, and keep the old keys alive until you have a week of clean data.