Migrate from Mailgun

Migrating from Mailgun to Sendpository

Everything that changes when you move your email from Mailgun, in the order you'll do it. An afternoon - mostly swapping form-data for JSON.

In short

Migrating from Mailgun to Sendpository replaces form-encoded requests with JSON and basic auth with a Bearer key. The sending domain moves out of the URL and into the from address, o:tag and v: variables become a tags object, and webhook verification switches from HMAC over timestamp and token to HMAC over the timestamp and raw body.

Or let your coding agent do the whole migration

Paste it in. It finds every Mailgun call, rewrites it, updates your webhook handler and switches over behind a flag you can flip back.

Show the prompt

Switch this project's email from Mailgun to Sendpository. First run `npx sendpository@latest init` - it opens my browser for me to approve, saves the API key and installs the Sendpository skill - then follow the skill's migration guide (references/migrate.md). If you can't run commands, read https://www.sendpository.com/agents/sendpository/references/migrate.md instead.

1. API differences

MailgunSendpository
EndpointPOST https://api.mailgun.net/v3/<domain>/messages (EU: api.eu.mailgun.net)POST https://api.sendpository.com/v1/emails - the domain comes from the from address
Encodingmultipart/form-dataapplication/json
Fieldsfrom, to, subject, text, htmlfrom, to (array), subject, text, html
Metadatao:tag (repeatable) and v:<name> variablestags: a string key/value object
Trackingo:tracking-opens, o:tracking-clickstrack_opens, track_clicks
Schedulingo:deliverytime (RFC 2822, up to 3 days ahead)scheduled_at (ISO 8601)
Custom headersh:<Header-Name>headers: { "Header-Name": "value" }

2. Authentication differences

MailgunSendpository
API keyHTTP basic auth, user api and your API key as the passwordAuthorization: Bearer sp_...

Create the key under API keys in the dashboard. It is shown once, so put it straight into SENDPOSITORY_API_KEY in your secrets manager. Keep the Mailgun key until the switch is finished, then revoke it.

3. Domain setup

Add your sending domain in Sendpository and publish the records it shows: a 2048-bit DKIM key signed as your own domain, plus SPF and a bounce MX on a sp. subdomain so your existing records are never edited. DMARC is recommended; if you already publish one, leave it as it is.

  • Mailgun domains are often a subdomain like mg.yourdomain.com. Add whichever domain your From address uses - Sendpository signs as that domain.
  • Mailgun's MX records only matter if you receive mail through Mailgun routes. Move receiving last, once sending is settled.

Not sure what’s published already? Run the domain through the email DNS checker.

4. Webhooks

MailgunSendpository
Verificationsignature object in the body: HMAC-SHA256 of timestamp + token with your webhook signing keySendpository-Signature header: HMAC-SHA256 of "timestamp.rawBody" with your endpoint secret

Event names

Mailgun eventSendpository event
deliveredemail.delivered
failed (temporary)email.delayed
failed (permanent)email.bounced
complainedemail.complained
openedemail.opened
clickedemail.clicked
unsubscribedemail.unsubscribed

Inbound routes become receiving addresses on your domain, delivered as email.received with an SPF, DKIM and DMARC verdict.

Full payloads and verification code are in the webhooks guide.

5. Error handling

MailgunSendpository
Error shapeA JSON message describing the problem, with the HTTP status{ "error": { "type": "domain_not_verified", "message": "..." } }

Sendpository returns { error: { type, message } } - branch on type. Only rate_limit_exceeded (wait for Retry-After) and internal_error are worth retrying automatically; the errors reference lists every type.

6. Code migration

Before - Mailgun (Node.js, fetch)
const form = new FormData();
form.append("from", "Acme <billing@mg.yourdomain.com>");
form.append("to", "customer@example.com");
form.append("subject", "Your receipt");
form.append("html", "<p>Thanks for your order.</p>");
form.append("o:tag", "receipt");

await fetch("https://api.mailgun.net/v3/mg.yourdomain.com/messages", {
  method: "POST",
  headers: {
    Authorization: "Basic " + btoa(`api:${process.env.MAILGUN_API_KEY}`),
  },
  body: form,
});
After - Sendpository (Node.js, fetch)
const res = await fetch("https://api.sendpository.com/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SENDPOSITORY_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "receipt-10428",
  },
  body: JSON.stringify({
    from: "Acme <billing@mail.yourdomain.com>",
    to: ["customer@example.com"],
    subject: "Your receipt",
    html: "<p>Thanks for your order.</p>",
    text: "Thanks for your order.",
    tags: { type: "receipt" },
  }),
});

const body = await res.json();
if (!res.ok) throw new Error(`${body.error.type}: ${body.error.message}`);
console.log(body.id);

Watch out for

7. Pricing

MailgunSendpository
Published pricingFree: 100 emails a day, one sending domain. Basic starts at $15 a month for 10,000 emails.6,000 emails a month (up to 200 a day), free forever. Current plans

Mailgun pricing from their own pages, checked 2026-09-23. Sendpository's free plan is larger: 6,000 emails a month (up to 200 a day), against Mailgun's 100 a day.

8. Things to test before switching

Specific to leaving Mailgun:

True of any migration:

Questions

Does Sendpository replace Mailgun routes?

For receiving on your domain, yes: add receiving addresses or a catch-all, and each message arrives as an email.received webhook and through the API, with an SPF, DKIM and DMARC verdict. Forwarding rules and route expressions are handled in your own code.

Does Sendpository validate email addresses like Mailgun?

Not as a separate API. Hard bounces and complaints are suppressed automatically, so a bad address is only ever tried once.

Mailgun API details are from their documentation, checked 2026-09-25: documentation.mailgun.com, documentation.mailgun.com.

Keep reading

Send your first email in minutes

6,000 emails a month (up to 200 a day), free forever. No card needed.