Short answer: Judge any API-only transactional email service, including Postmark, SendGrid, and Mailgun, by whether welcome templates, bounce suppression, and deliverability evidence have a single owner. Application ownership fits a gaming pipeline that must preserve those records through a transport change; provider ownership fits a team whose independent copy workflow is the harder constraint. The easiest setup is the one that leaves one system in charge of the exact bytes, template revision, recipient eligibility, and evidence needed to explain a send.
For a beginner comparing Postmark, SendGrid, Mailgun, or another API-only transactional email service, that rule narrows the evaluation quickly. Run the same four proofs against every candidate: render determinism, idempotent submission, authenticated event handling, and suppression before retry. A polished template editor can't compensate for an invalid recipient being queued again.
This is an architecture decision record for a game that sends a welcome message after account creation, records bounces, and suppresses addresses that should no longer receive transactional mail. The recommendation is deliberately vendor-neutral because provider behavior can change and the two supplied standards-related sources establish protocol and measurement constraints, not a product ranking.
How should a beginner compare API-only transactional email template deliverability?
Start by separating control-plane convenience from data-plane correctness. A quick setup demo usually proves that one message reached one cooperative inbox. Production needs a stronger invariant: for each account event, the system can identify the template revision rendered, decide whether the recipient was eligible at that moment, submit at most one logical welcome message, and later connect delivery or bounce events to that decision.
The three named services belong in the same evaluation cohort because they are candidates in the question, not because their template systems or event contracts are interchangeable. Don't infer a capability from a dashboard screenshot. Ask each candidate to pass an identical acceptance fixture using its current public documentation and a test account, then record the observed boundary. Your mileage may vary by account configuration, sending domain, and recipient mailbox, so the useful artifact is the fixture and its dated result rather than a permanent winner.
Use four test cases. First, render a welcome message containing a display name with an apostrophe and a game title containing an ampersand, then preserve the rendered subject and body hash. Second, submit the same account-created event twice and verify that your application emits one logical send. Third, feed a signed hard-bounce fixture into the event consumer and verify that a later job is suppressed. Fourth, change the template revision between queueing and execution; the result must follow the ownership rule you selected rather than whichever template happens to be current.
That last case catches a quiet design error. If a queue item stores only template_id, a delayed job may send new copy that was never approved when the account event occurred. If it stores rendered HTML but the provider owns required substitutions, replay may still differ. Pick one authority. Ambiguous ownership is the failure mode.
Decision: put eligibility and template revision in the application
The application should own recipient eligibility, suppression state, logical-send idempotency, and the immutable template revision. The email service should accept a prepared transactional message and report lifecycle events through an authenticated channel. This boundary keeps game-account facts on the side that already owns them and makes a provider migration a transport change rather than a rewrite of welcome-email policy.
I use “immutable” narrowly here. A revision such as welcome_en_17 may point to source controlled with the application; it doesn't mean copy can never change. It means an already accepted send intent continues to identify revision 17 after revision 18 is deployed. That gives support staff a concrete answer when a player asks which terms or onboarding link appeared.
Four invariants define the design:
- One account-created event maps to one logical welcome-send key.
- A suppressed recipient is rejected before any provider submission attempt.
- Every send intent names a locale and immutable template revision.
- Provider events update delivery evidence but never create a second welcome intent.
The failure boundaries matter more than the happy path. A queue retry may repeat application code, an event callback may be duplicated or reordered, a template deployment may race a queued job, and a person may correct an address after a bounce. None of those cases should erase history. Treat suppression as append-only evidence plus an explicit, audited release decision; don't model it as a boolean that any callback can casually flip.
DMARC adds a separate boundary. RFC 7489 describes domain-based message authentication, reporting, and conformance around identifier alignment. It does not certify that a welcome message will land in an inbox, and passing it does not repair poor list hygiene. Configure and observe authentication as its own deployment gate, then keep bounce handling as an application invariant.
The options and their failure modes
| Template ownership | What is authoritative | Best fit | Failure mode to test | Main limitation |
|---|---|---|---|---|
| Application-owned | Versioned subject and body source | Teams that need deterministic replay and provider portability | A retry after a template deployment | Copy changes require an application delivery path |
| Provider-owned | Provider template identifier and current provider revision | Teams where delegated, frequent copy editing dominates | A queued send racing a provider-side edit | Migration and historical reconstruction depend on exported evidence |
| Pinned provider revision | Provider template plus an immutable version reference | Teams whose chosen API exposes a verifiable revision contract | Missing or mutable revision references | The contract must be checked for every candidate |
| Rendered-message archive | Exact rendered payload stored with send intent | Regulated or dispute-heavy workflows needing byte-level evidence | Sensitive content retained longer than necessary | Storage, access control, and deletion obligations grow |
The fourth choice is evidence-heavy and often excessive for a simple game onboarding flow. A hash of the rendered body, the template revision, sanitized substitution keys, and provider message identifier may answer operational questions without retaining a second copy of personal content. I'm not sure which retention period is appropriate without the game's jurisdiction, privacy policy, and support requirements; those inputs should resolve the decision before launch.
Open tracking should not drive this architecture. Apple's Mail Privacy Protection guide says the feature prevents senders from seeing whether a recipient opened an email and masks the recipient's IP address. An “open” therefore isn't a dependable proof that a player saw onboarding content. Prefer application events that represent the actual goal, such as completing the tutorial, while treating provider delivery events as transport evidence rather than user intent.
Short version: delivery is observable; attention isn't.
Critical path in Python
The following code is intentionally a domain core, not a vendor SDK example. Adapter code can translate the selected provider's documented response and authenticated event schema into these types. Secrets, raw addresses, and message bodies should stay out of routine logs.
from dataclasses import dataclass
from hashlib import sha256
from typing import Protocol
@dataclass(frozen=True)
class WelcomeIntent:
account_id: str
recipient: str
locale: str
template_revision: str
@property
def idempotency_key(self) -> str:
material = f"welcome:{self.account_id}:{self.template_revision}"
return sha256(material.encode("utf-8")).hexdigest()
@dataclass(frozen=True)
class RenderedMessage:
subject: str
body_html: str
@property
def body_hash(self) -> str:
return sha256(self.body_html.encode("utf-8")).hexdigest()
class SuppressionStore(Protocol):
def is_suppressed(self, recipient: str) -> bool: ...
class SendLedger(Protocol):
def claim(self, key: str, revision: str, body_hash: str) -> bool: ...
class EmailTransport(Protocol):
def submit(
self, recipient: str, message: RenderedMessage, correlation_id: str
) -> str: ...
def send_welcome(
intent: WelcomeIntent,
message: RenderedMessage,
suppressions: SuppressionStore,
ledger: SendLedger,
transport: EmailTransport,
) -> str:
if suppressions.is_suppressed(intent.recipient):
return "suppressed"
claimed = ledger.claim(
intent.idempotency_key, intent.template_revision, message.body_hash
)
if not claimed:
return "duplicate"
return transport.submit(
intent.recipient, message, correlation_id=intent.idempotency_key
)
The ordering is deliberate: suppression is checked before claiming and submitting, while the ledger must implement an atomic uniqueness constraint for the idempotency key. A real design also needs a state transition for a transport submission that is accepted after the worker loses its response. Don't “fix” that ambiguity by sending again blindly. Reconcile using the stored correlation key and the provider's documented lookup or event evidence, where available; if a candidate offers no way to resolve the ambiguity, write that limitation into the decision record.
Bounce consumption needs the same discipline. Authenticate the callback according to the chosen service's current documentation, reject stale or invalid signatures, deduplicate by the provider event identifier, retain the original event class, and map only documented permanent-failure classes into durable suppression. Temporary delivery trouble belongs in retry policy, not permanent suppression. Because the supplied material does not define any provider's event taxonomy, this article does not pretend that one generic string maps safely across all three.
No shortcuts here.
For deployment, shadow the event adapter before allowing it to mutate suppression state. Record counts for submitted, accepted, permanently bounced, temporarily deferred, duplicate callbacks, invalid callbacks, and suppressed-before-submit, with addresses represented by a controlled pseudonymous identifier. Alert on discontinuities and missing event flow, but don't turn raw open rate into an availability objective. Cost belongs in the same review: application rendering adds release work, provider rendering adds governance and migration work, and archiving full payloads adds storage and deletion work. The invoice is only one line in that ledger.
Rejected option and when it becomes valid
This record rejects provider-owned mutable templates for the described game because template ownership would be split: product copy would live remotely while eligibility, player state, and bounce suppression remained in the application. A queue race could then make the effective revision difficult to reconstruct, and changing services would require migrating policy-adjacent assets as well as transport code.
The catch is that application ownership is not suitable when a communications team must publish urgent copy changes independently, across many locales, and the engineering release path cannot meet that operational need. In that case, choose provider-owned templates or a dedicated template system, but require immutable revisions, exportable source, role-based approval, and a tested rollback process. Stick with the selected provider's editor when delegated authorship is the primary constraint and the exit plan has been exercised.
Do not select among Postmark, SendGrid, and Mailgun from a generic feature grid. Give each the same acceptance fixture, verify current template-version semantics and event-authentication instructions in primary documentation, and store the evidence with the ADR. The easiest setup is the one whose ownership boundary your team can operate six months later, after copy changes, duplicate events, and the first corrected recipient address.
References
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- Apple, Use Mail Privacy Protection on iPhone: https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
Top comments (0)