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