Migrate/SparkPost (Bird)
Auth + structure change

Switch from SparkPost (Bird) to Anypost

SparkPost's API uses a non-standard auth header and a deeply nested JSON body. Anypost flattens both. Budget about 45 minutes to update a typical integration.

~45 mintypical migration time
2structural changes (auth + body)
8official SDK languages

SparkPost is now Bird Email. MessageBird acquired SparkPost in 2021 and rebranded to Bird in 2024, folding SparkPost into its platform as Bird Email. The API, the api.sparkpost.com endpoints, and the SDKs below still operate under that product, so this guide applies whether your dashboard says SparkPost or Bird.

What's the same, what changed

SparkPost's API has two quirks Anypost drops: a raw API key in the auth header (no Bearer prefix) and a deeply nested request body. Both flatten out on the Anypost side.

ConceptSparkPost (Bird Email)Anypost
Base URLapi.sparkpost.com/api/v1api.anypost.com/v1
EU base URLapi.eu.sparkpost.com/api/v1api.anypost.com/v1 (unified)
Auth headerAuthorization: <raw_api_key>Authorization: Bearer ap_…
Key prefixNone (arbitrary string)ap_
Send endpointPOST /api/v1/transmissionsPOST /v1/email
Recipient formatrecipients: [{address: {email, name}}]to: ["email"] or ["Name <email>"]
Content wrappercontent: {from, subject, html, text}Top-level from, subject, html, text
From addresscontent.from.email + content.from.namefrom: "Name <email>"
Response shape{results: {id, total_accepted_recipients}}202 Accepted, {id, created_at}
Tags / metadatatags: [], metadata: {}tags: ["string"]
Substitution datasubstitution_data: {}Template variables (server-side)
Webhooks{msys: {message_event: {…}}} arrayemail.* names, batched array
SMTP hostsmtp.sparkpostmail.comsmtp.anypost.com
SMTP usernameSMTP_Injection (literal)anypost
SMTP passwordYour API keyYour ap_… API key

The biggest gotcha: SparkPost omits Bearer from the Authorization header and sends the raw key. Anypost uses standard Bearer token auth, so add Bearer (with the trailing space) in front of your key.

Four steps to switch

SparkPost and Anypost can both be active during cutover; your existing sending domain works with both. Migrate one email type at a time and verify delivery before cutting over completely.

Create an Anypost account and verify your domain

Sign up at anypost.com and add your sending domain (the same one you use with SparkPost). You'll publish DNS records for SPF, DKIM, and tracking, then click verify. Propagation takes a few minutes to a few hours. See Domains.

Create an API key

In the Anypost dashboard, create a key (it starts with ap_). Unlike SparkPost's arbitrary-string keys, Anypost keys are always prefixed, so you can recognize them at a glance. Set it as ANYPOST_API_KEY. See Authentication.

Flatten the request body and fix the auth header

This is the main code change. Move fields out of content and recipients[].address to the top level, change to to a plain array of strings, and add Bearer in front of your key in the Authorization header.

Before (SparkPost)
POST /api/v1/transmissions
Authorization: YOUR_API_KEY
 
{
  "recipients": [
    { "address": { "email": "[email protected]" } }
  ],
  "content": {
    "from": { "email": "[email protected]" },
    "subject": "Welcome",
    "html": "<p>Glad you're here.</p>"
  }
}
After (Anypost)
POST /v1/email
Authorization: Bearer ap_…
 
{
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Welcome",
  "html": "<p>Glad you're here.</p>"
}

Update your webhook endpoint

Register your webhook URL in the Anypost dashboard. SparkPost wraps each event in a {msys: {message_event: {…}}} envelope; Anypost uses a flat email.* event with a data object. Update your handler to read the new structure and remap the event names. See the Webhooks section below.

The same change in every SDK

The changes are the same everywhere: standard Bearer auth, a flat JSON body, and a simpler recipient and from format. Pick your language.

SparkPost
curl https://api.sparkpost.com/api/v1/transmissions \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [{"address": {"email": "[email protected]"}}],
    "content": {
      "from": {"email": "[email protected]"},
      "subject": "Welcome",
      "html": "<p>Hi.</p>"
    }
  }'
Anypost
curl https://api.anypost.com/v1/email \
  -H "Authorization: Bearer ap_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome",
    "html": "<p>Hi.</p>"
  }'

Webhooks

SparkPost wraps every event in a {msys: {message_event: {…}}} envelope and posts an array of them. Anypost posts its events in a batched events array, each a flat email.* object with a data payload.

SparkPost eventAnypost eventNotes
injectionemail.sentrenameMessage accepted by the platform
deliveryemail.deliveredrenameAccepted by the recipient's server
delayemail.delayedrenameTemporary failure; will be retried
bounceemail.bouncedrenamePermanent failure
out_of_bandemail.bouncedsplitAsynchronous bounce; same Anypost event
spam_complaintemail.complainedrenameFeedback-loop complaint
openemail.openedrenameTracked open
clickemail.clickedrenameTracked click
policy_rejectionemail.suppressedsplitSuppression-list drops; other policy blocks are synchronous API errors
generation_failure(none)no equiv.Template errors are synchronous API errors, not events

Two SparkPost events are easy to overlook: delay has a direct equivalent in email.delayed (Anypost emits it during transient retries), and out_of_band folds into email.bounced. The only events without an analog are generation/template failures, which Anypost returns synchronously at send time instead of as a webhook.

SparkPost webhook payload
[
  {
    "msys": {
      "message_event": {
        "type": "delivery",
        "rcpt_to": "[email protected]",
        "timestamp": "2026-01-15T10:30:00Z"
      }
    }
  }
]
Anypost webhook payload
{
  "batch_id": "9c1f2a7b…",
  "events": [
    {
      "type": "email.delivered",
      "occurred_at": "2026-01-15T10:30:00Z",
      "data": {
        "email_id": "email_018f4f3e-7b2c-7c80-8e21-1a3a4f5b6c7d",
        "recipient": "[email protected]"
      }
    }
  ]
}
Before (SparkPost)
// SparkPost: iterate the array, unwrap the msys envelope
app.post("/webhook/email", (req, res) => {
  for (const batch of req.body) {
    const event = batch.msys.message_event;
    if (event.type === "delivery") markDelivered(event.rcpt_to);
    if (event.type === "bounce")   suppressAddress(event.rcpt_to);
  }
  res.sendStatus(200);
});
After (Anypost)
// Anypost: iterate events, flat names, recipient is data.recipient
app.post("/webhook/email", (req, res) => {
  for (const { type, data } of req.body.events) {
    switch (type) {
      case "email.delivered":  markDelivered(data.email_id); break;
      case "email.bounced":    suppressAddress(data.recipient); break;
      case "email.delayed":    scheduleRetryNotice(data.email_id); break;
      case "email.complained": unsubscribe(data.recipient); break;
      case "email.opened":
      case "email.clicked":    recordEngagement(data); break;
    }
  }
  res.sendStatus(200);
});

Signature verification differs. SparkPost signs its webhooks with a Basic-auth or token check you configure; Anypost signs with an Anypost-Signature header (t=<unix>,v1=<hmac>) over the timestamp and raw body. Swap your verification step before trusting a delivery. See Webhooks.

Using SMTP instead of the API?

SparkPost's SMTP requires the literal username SMTP_Injection, one of the more unusual conventions around. Anypost uses anypost with your ap_ key as the password. Update four values and you're done.

SparkPost SMTP
SMTP_HOST=smtp.sparkpostmail.com
SMTP_PORT=587
SMTP_USER=SMTP_Injection
SMTP_PASS=YOUR_API_KEY
SMTP_TLS=true
Anypost SMTP
SMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
SMTP_PASS=ap_…
SMTP_TLS=true

SparkPost EU customers use smtp.eu.sparkpostmail.com; Anypost needs no separate EU endpoint, since every region connects to smtp.anypost.com. Supported ports are 587 (recommended), 2587, and 25, with STARTTLS required on every port. See Sending over SMTP.

Ready to switch?

Create your account, verify your domain, and send your first email in minutes.