Skip to content
Browse the docs

Integrating · Published 2026-11-07 · Updated 2026-11-07 · AgentiSend

Migrating from Mailgun, Postmark, SendGrid or Amazon SES

How a Mailgun, Postmark, SendGrid or Amazon SES send and its webhooks map onto POST /emails, field by field, and what is refused on day one.

To move from Mailgun, Postmark, SendGrid or Amazon SES, send the same message to POST /emails with from, to, subject, html and text, branch on our error code, and point your webhooks at our signed events. The four moves share every step except the field names, so the steps are written once and each provider's field map has its own section. AgentiSend does not send through Amazon SES; that section is the field map for a move off that API. Moving from Resend has its own guide, because our API accepts Resend's requests as they are.

The request shape is the same

Each of the four takes a sender, recipients, a subject and a body, and so does ours. The full shape is on the POST /emails page.

  • Mailgun. Mailgun's HTTP send takes form fields from, to, subject, text and html.
  • Postmark. Postmark's send is POST /email with From, To, Subject, HtmlBody, TextBody and MessageStream. The names differ; the job is the same single message.
  • SendGrid. SendGrid's mail send takes a from object whose email is required, a subject, and personalizations.
  • Amazon SES. Amazon SES SendEmail takes FromEmailAddress, Destination and Content (with Subject, HTML and text).

Headers

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

Error names resolve

Error names from Mailgun, Postmark, SendGrid and Amazon SES are not aliases here. Branch on our code. Every refusal carries message, fix and docs_url. The catalogue is at /docs/errors.

Payload map

Each table was read from that provider's own send reference on 2026-09-28; the URL is under each heading.

Mailgun

Fetched from Mailgun's HTTP send page on 2026-09-28 (https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/send-http).

MailgunAgentiSend
fromfrom
toto
subjectsubject
htmlhtml
texttext

Postmark

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.

SendGrid

Fetched from the mail-send reference on 2026-09-28 (https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send). That page does not document an idempotency key.

SendGridAgentiSend
from.emailfrom
personalizationsto, and one message per personalization if you were batching inside one call
subjectsubject
Idempotency keyNot on that page. Send Idempotency-Key here; it is remembered for 7 days.

Amazon SES

Fetched from the SendEmail reference on 2026-09-28 (https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html). That page does not document an idempotency key.

Amazon SESAgentiSend
FromEmailAddressfrom
Destinationto
Content subjectsubject
Content HTMLhtml
Content texttext
Account pausePUT /v2/email/account/sending with SendingEnabled false. Here that job is POST /limits/kill-all.

Webhook events

Our webhooks are signed; see Webhooks for the scheme. Of the four, only Postmark's overview, fetched 2026-09-28, names its event types: Delivery, Bounce, Spam complaint, Open, Click, Subscription change and Inbound. It also says Postmark does not sign webhooks with HMAC. Mailgun's securing-webhooks page describes an HMAC-SHA256 signature over timestamp and token and lists no event names. The SendGrid and Amazon SES pages fetched that day name no webhook event types either; Amazon SES publishes delivery events to a destination you configure. So the table maps only what a provider named, and does not guess a name for the rest.

Our eventPostmark's name
email.deliveredDelivery
email.bouncedBounce
email.complainedSpam complaint
email.openedOpen tracking
email.clickedClick
contact.unsubscribedSubscription change
email.receivedInbound webhook. Receiving is open here: turn it on for a verified domain and publish the MX record its page shows.
email.received.heldNo Postmark name. Ours: mail labeled review on a domain that holds suspect mail; the arrival webhook waits until a person releases it.
inbound.refused_by_ruleNo Postmark name. Ours: mail for a domain was not stored because a sender rule refused it.

Every event type on our webhooks, 51 in all: email.queued, email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.failed, email.scheduled, email.canceled, email.suppressed, domain.created, domain.verified, domain.failed, domain.updated, domain.deleted, domain.blocklisted, suppression.added, suppression.removed, limit.warning, limit.exceeded, agent.approval_requested, agent.approved, agent.rejected, agent.killed, contact.created, contact.updated, contact.deleted, segment.joined, segment.left, segment.created, segment.updated, segment.deleted, broadcast.scheduled, broadcast.sent, broadcast.updated, automation.triggered, email.received, email.received.held, inbound.refused_by_rule, topic.created, topic.updated, topic.deleted, contact.subscribed, contact.unsubscribed, trust.warning, trust.throttled, trust.paused, trust.appealed, trust.restored. Each one's payload is on the events page.

What will refuse on day one

  • Receiving is open: turn it on for a verified domain, publish the MX record its page shows, and mail to that domain arrives on your account. A migration that depends on receiving mail at your domain waits on that.
  • 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 Mailgun, Postmark, SendGrid or Amazon SES. 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. None of the provider pages fetched on 2026-09-28 documents a contact-import field map: Mailgun (pricing, webhook signing and the HTTP send); Postmark (sending and webhooks); SendGrid (pricing, the mail-send reference and the docs MCP server); Amazon SES (pricing, SendEmail and the account-sending call). 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.

What differs

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

An EU data region is not part of the move. If the old account uses one, 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.