MailStack Dart SDK

The official Dart SDK for MailStack — email that lands. Send transactional email, manage sending domains and templates, stream delivery events, and verify webhooks.

Server-side SDK. This package is for backend Dart and Dart-based servers. The MailStack send API uses secret keys (ms_live_…) that must never ship inside a Flutter app bundle. In a mobile or web Flutter client, call your own backend, and have that backend use this SDK.

Install

dependencies:
  mailstack: ^0.1.0
dart pub add mailstack

Requires Dart 3.4+.

Quickstart

import 'package:mailstack/mailstack.dart';

Future<void> main() async {
  final ms = MailStackClient(apiKey: 'ms_live_...');

  final res = await ms.emails.send(const SendEmailRequest(
    from: 'hello@yourdomain.com',
    to: 'user@example.com',
    subject: 'Welcome to MailStack',
    html: '<h1>Hi there 👋</h1>',
  ));

  print('queued: ${res.id} (${res.status})');
  ms.close();
}

Authentication

Every request sends your API key as a Bearer token. The send API (ms.emails) is authorized with the API key alone.

The dashboard-scoped resources — domains, apiKeys, templates, messages, webhooks — are authorized with a user JWT and require your organization id, sent as the X-MailStack-Org header:

final ms = MailStackClient(apiKey: '<jwt>', organizationId: 'org_...');

Other options: baseUrl (defaults to https://api.mailstack.voostack.com) and httpClient (inject your own package:http Client for timeouts, retries, or tests).

Sending email

to, cc, and bcc accept either a single address or a List<String>:

await ms.emails.send(SendEmailRequest(
  from: 'hello@yourdomain.com',
  to: ['a@example.com', 'b@example.com'],
  subject: 'Hello',
  text: 'Plain-text body',
  tags: 'welcome,onboarding',
  variables: {'name': 'Sam'},
));

Batch (up to 100 fully-specified emails) and bulk (one base message + per-recipient overrides):

await ms.emails.sendBatch([ /* SendEmailRequest, ... */ ]);

await ms.emails.bulk(BulkSendRequest(
  from: 'hello@yourdomain.com',
  subject: 'Hi {{name}}',
  html: '<p>Hello {{name}}</p>',
  recipients: const [
    BulkRecipient(to: 'a@example.com', variables: {'name': 'Ada'}),
    BulkRecipient(to: 'b@example.com', variables: {'name': 'Bo'}),
  ],
));

Scheduled sends and cancellation:

final res = await ms.emails.send(SendEmailRequest(
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Reminder',
  text: 'See you soon',
  sendAt: DateTime.now().add(const Duration(hours: 2)),
));
await ms.emails.cancel(res.id); // before it dispatches

Lint is a read-only deliverability + content check (nothing is queued or billed):

final result = await ms.emails.lint(const SendEmailRequest(
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Check me',
  html: '<p>Hi</p>',
));
print('${result.score} (${result.rating})');

Observability

final messages = await ms.emails.list(limit: 50);
final events = await ms.messages.events(messages.first.id);
for (final e in events) {
  print('${e.type.name} at ${e.occurredAt}'); // sent, delivered, open, click, ...
}

Domains, templates, API keys

final domain = await ms.domains.create(const CreateDomainRequest(domain: 'yourdomain.com'));
for (final r in domain.dnsRecords) {
  print('${r.type} ${r.name} ${r.value}'); // publish these
}
await ms.domains.verify(domain.id);
await ms.domains.blocklist(domain.id);

await ms.templates.list();
await ms.apiKeys.create(const CreateApiKeyRequest(name: 'ci', scopes: ['emails:send']));

Webhooks

Create a webhook (the signing secret is returned exactly once), then verify incoming deliveries against the raw request body:

final res = await ms.webhooks.create(const CreateWebhookRequest(
  url: 'https://example.com/hooks/mailstack',
  eventTypes: ['Delivered', 'Bounce', 'Complaint'],
));
final secret = res.signingSecret; // store securely — shown once
// In your webhook handler — use the RAW request body, not a re-encoded map.
final ok = verifyWebhookSignature(
  secret: secret,
  payload: rawBody,
  signature: request.headers.value(webhookSignatureHeader) ?? '',
);
if (!ok) {
  // reject — return 400
}

The signature is an HMAC-SHA256 over "{timestamp}.{body}" carried in the MailStack-Signature: t=…,v1=… header, compared in constant time.

Errors

Any non-2xx response throws a MailStackException with the status code and raw body:

try {
  await ms.emails.send(req);
} on MailStackException catch (e) {
  print('${e.statusCode}: ${e.body}');
}

Enums

Status fields arrive as integers and are exposed as Dart enums: MessageStatus, MessageEventType, SendingDomainStatus, TemplateStatus. Each exposes its raw wire .value and a .fromInt(int) decoder.

License

MIT

Libraries

mailstack
The official Dart SDK for the MailStack email API — send transactional email, manage sending domains and templates, stream delivery events, and verify webhooks.