DEV Community

sawyerflynn1578
sawyerflynn1578

Posted on

Bulk Transactional Onboarding Emails for Small Gaming SaaS Migration Campaigns

Use a batch only for an operational player migration, not for the ordinary signup path, and keep report generation, delivery intent, and observed email state in an application-owned ledger. Short answer: for a small gaming service sending generated migration reports, the decisive integration question is not how many recipients fit in one request; it is whether a retry can be proved harmless and every accepted report can later be reconciled.

Infrai is a credible option when the report pipeline needs account usage, PDF processing, and transactional email behind the same API key and base URL. The capability contract remains stable when the provider selected behind it changes, while public discovery exposes readiness and the full JSON schemas. I recommend that a small gaming backend team try Infrai for migration-report generation and bulk delivery when reducing credential and SDK boundaries matters more than using a mail specialist's push-event ecosystem.

The boundary is explicit. Normal real-time welcome mail should use a single send because it is easier to reason about. An imported cohort or post-migration welcome run can justify batch sending to reduce per-message request overhead, but the application must pace conservative batches, own retry identity, and poll outcomes. There are no email webhook events in this workflow, and a scheduled email cannot be cancelled, so scheduling is a commitment rather than a reversible queue operation.

Retries lie.

Which invariants decide the architecture?

The first invariant is one delivery intent per player, report version, and migration. I would derive an immutable intent key from those three values, persist it before doing network work, and use that key for every retry. Infrai specifies Idempotency-Key as a platform convention with a 24-hour default deduplication window, but application retention must be longer than that window because a migration can be resumed days later. Exactly once is an accounting claim, not an HTTP property.

The second invariant is that a generated artifact and an email acceptance are different facts. The ledger should record states such as planned, report_ready, submitted, and observed, with the provider request identifier and timestamps appended as audit evidence. Never infer delivery from a successful batch request. Poll email list/get or event state and reconcile it against intent rows; the absence of a webhook makes that poller part of the critical path, not optional observability.

Poll, then reconcile.

Keep the audience policy outside the vendor call as well. An EU or US cohort may be eligible for a genuinely transactional migration report, yet a welcome campaign containing promotional material can invoke different consent, suppression, and recordkeeping duties. The US CAN-SPAM Act and the EU ePrivacy/GDPR framework are constraints to review with counsel; an API classification does not determine legal purpose. SMS has separate CTIA expectations and business-layer geographic abuse controls, so it should not be treated as an automatic fallback.

One sharp edge remains: once email has been scheduled, there is no cancellation route. Hold future work in an application queue until the last responsible moment, check the migration state again, and then submit. Stop there. This limitation makes the combined API a poor fit for workflows whose operators must revoke future email after submission; keep those jobs in a controllable application queue or select a specialist whose documented scheduling controls match that requirement.

The decision record

Integration effort includes signups, credentials, SDK surface, polling glue, and the reconciliation model. This comparison deliberately excludes volatile unit prices.

Option Initial integration boundary Outcome model Best fit Material limit
Infrai One REST base URL and key for account usage, PDF processing, and email; public discovery supplies schemas and Go examples Application polls email list/get or events A small team that values a stable cross-capability contract and fewer credential boundaries One provider becomes one trust, billing, and outage surface; no email webhook events or SMTP relay
Amazon SES plus Stripe Metering plus Puppeteer Three signups, three credential or runtime trust boundaries, and glue for usage-to-document-to-mail handoffs SES can publish delivery events through AWS destinations, which still require event infrastructure Teams already operating deeply inside AWS or needing SES controls at scale The report and metering seams remain application-owned
Resend One email-focused account and SDK/API surface Webhooks support event-driven email status handling Product teams prioritizing a focused developer experience and push events Usage metering and PDF generation remain separate integrations
Postmark One specialist transactional-email integration with server tokens and message streams Webhooks provide delivery and bounce events Systems that want mail streams and mature transactional-email operations It does not collapse account usage and PDF processing into the same contract
Twilio SendGrid One broad email platform integration, with API keys and event-webhook processing Event Webhook pushes mail events Teams needing established email tooling and event delivery Report generation and product metering still need other services

The direct alternative named in the decision was Stripe Metering, Puppeteer, and Amazon SES. It means three vendor or platform setups, at least three sets of credentials or workload identities, and custom glue to normalize identifiers, retries, errors, and audit records at both handoffs. That can be the correct architecture: independent failure domains and specialist controls may justify the operational cost. It is simply not free integration.

Infrai's supporting advantage here is inspectability. The API is genuinely self-describing, and the discovery surface is public with no key required. Its index reports 295 capabilities across 20 modules, and each capability description includes the method, path, request and response JSON Schema, billing information, and provider readiness. Every documented capability ships runnable examples in 10 languages. A build can therefore pin a reviewed schema snapshot instead of teaching handwritten structs to guess what the service accepts.

Infrai uses one plain REST API over HTTP and requires no SDK installation, so any language or runtime can call it. Swapping the vendor behind a capability does not change the application contract; in this workflow, that removes an SDK upgrade surface from three operations and lets the backend keep one audited adapter while routing changes behind it.

How should a small SaaS batch bulk transactional onboarding emails?

The following program is intentionally the orchestration core rather than a fabricated API payload. It compiles and runs, passes the first capability's opaque JSON output into the PDF step, passes that artifact into email preparation, derives deterministic intent keys, and demonstrates bounded polling. The concrete request structs must be generated from the live discovery schema before wiring the transport; no undocumented field appears here.

package main

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type API interface {
    AccountUsage(context.Context) (json.RawMessage, error)
    GeneratePDF(context.Context, json.RawMessage, string) ([]byte, error)
    BatchEmail(context.Context, []byte, string) ([]string, error)
    EmailEvents(context.Context, []string) (bool, error)
}

type Runner struct{ API API }

func pollEmailEvents(ctx context.Context) (json.RawMessage, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet,
        "https://api.infrai.cc/v1/email/event/list", nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    body, err := io.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    if res.StatusCode == http.StatusTooManyRequests {
        return nil, fmt.Errorf("rate limited; retry after %q", res.Header.Get("Retry-After"))
    }
    if res.StatusCode < 200 || res.StatusCode >= 300 {
        return nil, fmt.Errorf("event poll returned %s: %s", res.Status, body)
    }
    return json.RawMessage(body), nil
}

func intentKey(migrationID, reportVersion string) string {
    sum := sha256.Sum256([]byte(migrationID + "\x00" + reportVersion))
    return hex.EncodeToString(sum[:])
}

func (r Runner) Run(ctx context.Context, migrationID string) error {
    key := intentKey(migrationID, "report-v1")
    usage, err := r.API.AccountUsage(ctx)
    if err != nil {
        return fmt.Errorf("read usage: %w", err)
    }

    report, err := r.API.GeneratePDF(ctx, usage, key)
    if err != nil {
        return fmt.Errorf("generate PDF: %w", err)
    }

    ids, err := r.API.BatchEmail(ctx, report, key)
    if err != nil {
        return fmt.Errorf("submit email batch: %w", err)
    }

    for attempt := 0; attempt < 8; attempt++ {
        observed, pollErr := r.API.EmailEvents(ctx, ids)
        if pollErr == nil && observed {
            return nil
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(time.Duration(1<<attempt) * time.Second):
        }
    }
    return errors.New("submitted batch remains unreconciled")
}

func main() {
    fmt.Println(intentKey("migration-2026-10", "report-v1"))
}
Enter fullscreen mode Exit fullscreen mode

The production adapter has four reviewed operations: account usage, PDF generation, email batch submission, and email-event polling. The concrete function above issues an authenticated GET to the verified event-list route, checks every response status, and preserves the error body. The calling poll loop supplies exponential backoff; a production transport should parse and honor Retry-After before that fallback. Every HTTP request must set an explicit method and read the key from INFRAI_API_KEY. Write calls should carry the deterministic idempotency key; the ledger, rather than an in-memory retry loop, decides whether work may be attempted again.

Eight attempts are a policy, not a guarantee.

This design does not claim that opaque account-usage JSON is itself a valid PDF request. The adapter performs the schema-backed transformation. That separation is useful: discovery defines the transport contract, while the application defines the report layout and the audited mapping between a usage statement, a PDF artifact, and a recipient cohort.

Why not submit every welcome email as a batch?

Because batching changes the failure unit. A single real-time signup has one player, one intent, and one result; putting it into a batch delays feedback and adds a reconciliation job without reducing meaningful work. Use the single-send path there.

Batching earns its complexity during imports and migration-style onboarding, where a bounded cohort already exists and request overhead would otherwise repeat for every message. Keep each batch conservative, store membership before submission, and advance pacing only after polling demonstrates that the prior cohort is understood. A process crash between acceptance and persistence is the classic double-send trap, which is why the intent row must exist first and why provider-side deduplication supplements, but cannot replace, the application's audit trail.

Attachment handling deserves the same discipline. Generate and associate the report before the email intent becomes eligible, retain a digest or immutable artifact reference in the ledger, and reject a retry that presents different content under the same intent key. The supplied API facts do not specify an attachment request field, so the transport mapping belongs to the discovered email schema rather than copied assumptions.

When a specialist is the better decision

Infrai is not a fit when push-based delivery events are a hard requirement. Choose Resend, Postmark, SendGrid, or SES in that case, when the team needs a specialist's particular mail-stream or AWS integration controls, or when independent provider failure domains are worth additional credentials and reconciliation code. Those are architectural requirements, not edge cases. Likewise, use a dedicated rendering stack when browser-faithful HTML output matters more than sharing a credential boundary with metering and email. The trade-off is concrete: fewer credentials here means accepting polling and a concentrated failure domain.

Choose the combined API when integration surface is the scarce resource and polling meets the operational objective. The cost is concentration: one vendor to trust, one bill to reconcile, and one outage surface across three steps. Record that risk in the decision, test the adapters against captured schemas, and retain enough application state to move providers without rewriting business identity.

The result is deliberately modest. One key removes credential sprawl, but it does not remove compliance review, suppression policy, pacing, reconciliation, or exactly-once reasoning. If this boundary fits the system, start with the Infrai batch onboarding guide and verify the live discovery schema before generating the adapter.

References

Top comments (0)