Skip to main content
Technology & EngineeringEmail Services277 lines

Sparkpost

Send transactional and marketing email with SparkPost (now MessageBird). Use this

Quick Summary31 lines
You are an expert in SparkPost (MessageBird Email API) for transactional and marketing
email. You understand the Transmissions API, stored templates, recipient lists,
subaccounts, webhook events, and how to maintain deliverability at enterprise scale.

## Key Points

- **Sending marketing email with `transactional: true`** -- This bypasses opt-out suppression handling, violates CAN-SPAM/GDPR, and risks account suspension. Set the flag accurately for every send.
- **Using the US API endpoint for an EU account** -- EU accounts must use `api.eu.sparkpost.com`. Hitting the US endpoint returns authentication errors with no indication that the region is wrong.
- **Mark transactional vs marketing explicitly.** Set `options.transactional: true`
- **Use stored templates.** Keep email content in SparkPost templates so marketing
- **Respect suppression lists.** SparkPost auto-suppresses hard bounces and spam
- **Use subaccounts for multi-tenant apps.** Each tenant gets isolated sending
- **Set up a custom bounce domain and tracking domain.** Aligns authentication and
- **Monitor bounce classifications.** SparkPost provides 100 bounce classes. Focus on
- **Ignoring the EU endpoint.** If your SparkPost account is in the EU data center,
- **Sending marketing mail with transactional flag.** This bypasses opt-out
- **Not handling 429 rate limits.** SparkPost returns 429 when you exceed sending
- **Webhook endpoint not idempotent.** SparkPost may redeliver webhook batches.

## Quick Example

```bash
npm install @sparkpost/node-sparkpost
```

```
SPARKPOST_API_KEY=your-api-key
SPARKPOST_WEBHOOK_AUTH_TOKEN=your-webhook-token
```
skilldb get email-services-skills/SparkpostFull skill: 277 lines
Paste into your CLAUDE.md or agent config

SparkPost — Email Service

You are an expert in SparkPost (MessageBird Email API) for transactional and marketing email. You understand the Transmissions API, stored templates, recipient lists, subaccounts, webhook events, and how to maintain deliverability at enterprise scale.

Core Philosophy

SparkPost is built for scale. Where other email services handle thousands of messages, SparkPost handles billions, with infrastructure designed for high-throughput sending, granular deliverability analytics, and multi-tenant isolation through subaccounts. Choosing SparkPost means you expect your email volume to grow significantly and you need a platform that will not become a bottleneck.

The transactional flag is a contract, not a label. Setting options.transactional: true tells SparkPost to treat the message differently for suppression handling, compliance, and deliverability reporting. Transactional messages bypass marketing unsubscribe suppression because they are expected by the recipient (receipts, alerts, password resets). Sending marketing content with the transactional flag violates this contract, bypasses consent mechanisms, and can lead to account suspension.

Suppression lists are sacred. SparkPost automatically suppresses hard bounces and spam complaints to protect your sender reputation. Removing an address from the suppression list should only happen when the recipient has explicitly re-opted in through a confirmed action. Programmatically clearing suppressions to "retry" sending to bad addresses is the fastest way to destroy your deliverability and get your account flagged.

Anti-Patterns

  • Sending marketing email with transactional: true -- This bypasses opt-out suppression handling, violates CAN-SPAM/GDPR, and risks account suspension. Set the flag accurately for every send.
  • Clearing suppression lists to retry bad addresses -- Suppressed addresses are suppressed for a reason. Removing them without genuine re-opt-in poisons your sender reputation and increases bounce rates.
  • Using the US API endpoint for an EU account -- EU accounts must use api.eu.sparkpost.com. Hitting the US endpoint returns authentication errors with no indication that the region is wrong.
  • Sending without a custom bounce domain and tracking domain -- Default SparkPost domains in email headers reduce brand trust and can fail strict DMARC policies. Configure custom domains for full alignment.
  • Not deduplicating webhook events -- SparkPost may redeliver webhook batches. Processing the same event twice can cause duplicate database writes, duplicate notifications, or incorrect metrics. Deduplicate by event_id.

Overview

SparkPost is a high-volume email delivery platform (now part of MessageBird) that powers email for major senders. It provides a REST API and SMTP relay for sending, stored templates with Handlebars-like substitution, granular engagement and deliverability analytics, suppression lists, and real-time event webhooks. SparkPost is particularly strong at scale — handling billions of messages — and offers subaccounts for multi-tenant isolation.

Setup & Configuration

Installation

npm install @sparkpost/node-sparkpost

Basic client setup

import SparkPost from "@sparkpost/node-sparkpost";

const client = new SparkPost(process.env.SPARKPOST_API_KEY, {
  origin: "https://api.sparkpost.com:443",
  // For EU data center: "https://api.eu.sparkpost.com:443"
});

Environment variables

SPARKPOST_API_KEY=your-api-key
SPARKPOST_WEBHOOK_AUTH_TOKEN=your-webhook-token

Domain authentication

Verify your sending domain by adding the DKIM TXT record SparkPost provides. SPF alignment happens automatically through SparkPost's return-path domain. Set up a custom bounce domain and custom tracking domain for full brand alignment:

const response = await client.sendingDomains.create({
  domain: "mail.example.com",
  generate_dkim: true,
});
// Add the returned DKIM record to DNS, then verify:
await client.sendingDomains.verify("mail.example.com", {
  dkim_verify: true,
});

Core Patterns

Send a single transactional email

async function sendTransactional(to: string, subject: string, html: string) {
  const result = await client.transmissions.send({
    content: {
      from: "app@mail.example.com",
      subject,
      html,
    },
    recipients: [{ address: { email: to } }],
    options: {
      transactional: true, // marks as transactional for suppression handling
      open_tracking: false,
      click_tracking: false,
    },
  });
  return result.results; // { total_rejected_recipients, total_accepted_recipients, id }
}

Send with a stored template and substitution data

async function sendFromTemplate(
  to: string,
  templateId: string,
  data: Record<string, unknown>
) {
  return client.transmissions.send({
    content: {
      template_id: templateId,
    },
    recipients: [
      {
        address: { email: to },
        substitution_data: data,
      },
    ],
    options: { transactional: true },
  });
}

// Usage
await sendFromTemplate("user@example.com", "welcome-email", {
  firstName: "Alice",
  actionUrl: "https://app.example.com/onboard",
});

Batch send with recipient list

async function sendBatch(
  recipients: Array<{ email: string; name: string; data: Record<string, unknown> }>,
  templateId: string
) {
  return client.transmissions.send({
    content: { template_id: templateId },
    recipients: recipients.map((r) => ({
      address: { email: r.email, name: r.name },
      substitution_data: r.data,
    })),
    options: { transactional: false },
  });
}

Process webhook events

import { Router } from "express";

const router = Router();

interface SparkPostEvent {
  msys: {
    message_event?: {
      type: string;
      rcpt_to: string;
      transmission_id: string;
      timestamp: string;
      bounce_class?: number;
      raw_reason?: string;
    };
    track_event?: {
      type: string;
      rcpt_to: string;
      target_link_url?: string;
    };
  };
}

router.post("/webhooks/sparkpost", (req, res) => {
  const authToken = req.headers["x-messagesystems-webhook-token"];
  if (authToken !== process.env.SPARKPOST_WEBHOOK_AUTH_TOKEN) {
    return res.status(401).end();
  }

  const events: SparkPostEvent[] = req.body;

  for (const event of events) {
    const msgEvent = event.msys.message_event;
    const trackEvent = event.msys.track_event;

    if (msgEvent) {
      switch (msgEvent.type) {
        case "delivery":
          // Email delivered
          break;
        case "bounce":
          // Handle bounce — check bounce_class for hard vs soft
          if (msgEvent.bounce_class && msgEvent.bounce_class <= 25) {
            // Hard bounce: suppress this address
          }
          break;
        case "spam_complaint":
          // Suppress sender immediately
          break;
      }
    }

    if (trackEvent) {
      switch (trackEvent.type) {
        case "open":
        case "click":
          // Log engagement
          break;
      }
    }
  }

  res.status(200).end();
});

Subaccounts for multi-tenant isolation

// Create a subaccount for a tenant
const subaccount = await client.subaccounts.create({
  name: "Tenant ABC",
  key_label: "tenant-abc-key",
  key_grants: ["smtp/inject", "transmissions/modify", "message_events/view"],
});
// subaccount.results.subaccount_id — use as x-msys-subaccount header

// Send on behalf of a subaccount
await client.transmissions.send(
  {
    content: { template_id: "tenant-welcome" },
    recipients: [{ address: { email: "user@example.com" } }],
  },
  { headers: { "x-msys-subaccount": subaccount.results.subaccount_id } }
);

Best Practices

  • Mark transactional vs marketing explicitly. Set options.transactional: true for transactional sends. SparkPost uses this flag for suppression list behavior and deliverability reporting.
  • Use stored templates. Keep email content in SparkPost templates so marketing teams can update copy without code deploys. Use substitution_data for dynamic values.
  • Respect suppression lists. SparkPost auto-suppresses hard bounces and spam complaints. Query the suppression list API before re-adding addresses. Never remove suppressions unless the recipient explicitly re-opts in.
  • Use subaccounts for multi-tenant apps. Each tenant gets isolated sending reputation, separate API keys, and independent suppression lists. This prevents one bad tenant from poisoning deliverability for all.
  • Set up a custom bounce domain and tracking domain. Aligns authentication and prevents brand confusion in email headers. Improves deliverability with strict DMARC policies.
  • Monitor bounce classifications. SparkPost provides 100 bounce classes. Focus on classes 10 (invalid recipient), 25 (admin failure), and 30 (generic bounce) for list hygiene decisions.

Common Pitfalls

  • Ignoring the EU endpoint. If your SparkPost account is in the EU data center, you must use api.eu.sparkpost.com. Using the US endpoint returns auth errors.
  • Sending marketing mail with transactional flag. This bypasses opt-out suppression handling and violates CAN-SPAM / GDPR. Always set the flag accurately.
  • Not handling 429 rate limits. SparkPost returns 429 when you exceed sending rate. Implement exponential backoff. For very high volume, request a rate limit increase via support.
  • Webhook endpoint not idempotent. SparkPost may redeliver webhook batches. Deduplicate using the event_id field to avoid processing the same event twice.
  • Overlooking inline CSS. SparkPost does not auto-inline CSS. Use a CSS inliner (like Juice) before uploading HTML templates, or emails render incorrectly in many clients.

Install this skill directly: skilldb add email-services-skills

Get CLI access →