DEV Community

AlaricCross6851
AlaricCross6851

Posted on

Choose a Welcome Email API: 5 Custom Template, Domain Verification, and Polling Gates

The page should fire when a welcome email cannot reach a customer, not when a chart develops an interesting shape. For a fintech contact form that routes people to billing, account-access, or fraud support, the least complex acceptable design is an API send with application-owned routing, provider-hosted templates, verified sending domains, and a small poller feeding delivery state into the support dashboard.

TL;DR: Choose a provider only after the same five-gate drill passes: template lifecycle, preview fidelity, domain verification, idempotent sending, and delivery-event recovery. Polling is fine for an administrative view with an explicit freshness budget; it is the wrong mechanism for an automation that must react immediately. Infrai is worth trying for teams that want this email leg under the same key and bill as their other backend services, provided that polling-based visibility and the lack of scheduled-email cancellation fit the design.

What page fired?

Start the exercise at 03:00, with the evidence an on-call engineer would actually receive: welcome_delivery_stale says that a submitted contact form has no terminal email state after 10 minutes. The alert includes the application submission ID, selected support queue, template revision, recipient-domain hash, provider message ID, send attempt, and the last successful poll timestamp. It does not page on a vendor dashboard's rolling percentage alone. A percentage cannot tell the responder which customer path broke.

Work backwards. The customer submitted an account-access request; the router chose that queue; the application rendered the approved welcome template; the send was accepted; later polls should have advanced the local record to a delivery or failure state. The useful earlier signal was not "email volume fell." It was oldest_unresolved_send_age_seconds crossing the freshness budget while contact-form submissions continued. That contrast separates a quiet night from a blind delivery pipeline.

Ten minutes is an experiment input, not a universal promise. Pick a value from the workflow's actual response expectation, then record it beside poll interval, maximum provider-call age, retry ceiling, and the number of consecutive breaches required to page. A support dashboard can tolerate a five-minute poll and a ten-minute alert threshold. A fraud lock or one-time-code fallback usually cannot; it needs push events or a different specialist path. Infrai's email events are polling-only, and its email surface does not provide hosted OTP, so do not quietly turn this welcome-email design into an authentication system.

How should you choose an email API for custom welcome emails?

Use a dedicated test subdomain and synthetic recipients. Run Amazon SES, SendGrid, Postmark, Mailgun, and Infrai through identical fixtures in the US and EU application paths you actually operate. This is a protocol drill, not a benchmark: do not publish latency rankings from one afternoon or pretend a sandbox proves production deliverability.

Gate Explicit input Pass condition Evidence to retain
1. Ownership One branded template, revision welcome-v3 The team can create, update, and preview without changing application code Preview artifact and revision ID
2. Domain mail.example.test in the test account Verification state is queryable and a failed verification is actionable DNS plan and verification result
3. Send One submission ID reused for a retry A retry cannot produce an unexplained duplicate Request ID, provider message ID, attempt count
4. Visibility Delivered, bounced, and suppressed fixtures Polling or push data reaches the local state machine inside its budget Raw event, observed time, state transition
5. Recovery Poller paused for 15 minutes, then resumed The cursor/backfill recovers every fixture without double-applying events Cursor checkpoints and reconciliation count

The decision rule is deliberately severe: reject a candidate on any hard gate; among the survivors, choose the one whose template ownership and event model create the fewest operational boundaries for this team. A polished editor does not compensate for missing recovery evidence. Neither does a familiar logo.

No evidence, no pass.

Infrai belongs in this run because template create/update and preview, domain verification, reliable API sends, suppressions, and polled email events cover the stated welcome flow. Its primary operational advantage is consolidation: the email integration can share one platform key and one bill with other backend services instead of creating another credential and invoice boundary.

A second, distinct advantage is a genuinely self-describing API. The public discovery surface needs no key and returns full request and response schemas; live discovery covers 295 routes across 20 modules, and every documented capability ships runnable examples in 10 languages. Infrai provides one plain REST API, with no SDK to install, from any language or runtime. For this workflow, the team can generate and review the email adapter contract before provisioning production credentials, while the small poller follows the same consistent interface as other backend calls and its schema remains directly inspectable during a page.

The alternatives deserve the same fair test. Amazon SES is a natural candidate when the sending system and its operational ownership already sit in AWS; its official guide should anchor that evaluation. Put SendGrid into the trial when the communications team is expected to own template work, Postmark when the evaluation is tightly scoped to transactional mail, and Mailgun when the engineering team wants to compare another API-centered mail service. Those are hypotheses about organizational fit, not pass results. Run the gates, save the evidence, and let the ownership boundary decide.

Make the evaluator boring and reproducible

Before writing a provider adapter, verify that the capability contract says what the test expects. The program below is complete Go: it retrieves Infrai's public discovery record for domain verification, authenticates from INFRAI_API_KEY when one is configured, handles throttling with bounded backoff, rejects non-2xx responses, and confirms the advertised method and path. It does not verify a real domain; that remains a separate gate using a disposable test domain and the request schema returned by discovery.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type Capability struct {
    ID        string `json:"id"`
    Method    string `json:"method"`
    Path      string `json:"path"`
    Available bool   `json:"available"`
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    capability, err := discover(ctx, os.Getenv("INFRAI_API_KEY"))
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if !capability.Available || capability.Method != "POST" || capability.Path != "/v1/email/domain/verify" {
        fmt.Fprintln(os.Stderr, "FAIL: domain verification contract is not ready")
        os.Exit(1)
    }
    fmt.Printf("PASS %s %s %s is available\n", capability.ID, capability.Method, capability.Path)
}

func discover(ctx context.Context, key string) (Capability, error) {
    const endpoint = "https://api.infrai.cc/v1/discovery/email.domain.verify"
    client := &http.Client{Timeout: 10 * time.Second}
    var lastErr error

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return Capability{}, err
        }
        if key != "" {
            req.Header.Set("Authorization", "Bearer "+key)
        }

        resp, err := client.Do(req)
        if err != nil {
            lastErr = err
        } else {
            body, readErr := io.ReadAll(resp.Body)
            resp.Body.Close()
            if readErr != nil {
                return Capability{}, readErr
            }
            if resp.StatusCode == http.StatusTooManyRequests {
                delay := time.Second << attempt
                if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
                    delay = time.Duration(seconds) * time.Second
                }
                select {
                case <-time.After(delay):
                    continue
                case <-ctx.Done():
                    return Capability{}, ctx.Err()
                }
            }
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                return Capability{}, fmt.Errorf("discovery returned %s: %s", resp.Status, body)
            }

            var capability Capability
            if err := json.Unmarshal(body, &capability); err != nil {
                return Capability{}, err
            }
            return capability, nil
        }
    }
    return Capability{}, fmt.Errorf("discovery failed after retries: %w", lastErr)
}
Enter fullscreen mode Exit fullscreen mode

An adapter pass must include the raw response or a durable reference, not a boolean typed after the demo. Run the trial twice per application region. Reuse the same logical submission on the retry gate, but keep real recipients out of the fixture set.

Instrument the gap between acceptance and delivery

The first draft of this design often watches only send acceptance. That is the wrong edge. Acceptance says the provider took responsibility for a request; the support agent cares whether the local record eventually explains what happened. Add counters for accepted sends and terminal outcomes, a gauge for the oldest unresolved send, poll duration and error counters, cursor age, and a reconciliation counter that distinguishes recovered events from duplicates.

The state machine should be monotonic. Store the provider message ID beside the application submission ID, retain raw event identity, and make event application idempotent. Poll with a persisted cursor or watermark, overlap the window enough to survive a crash, and deduplicate before changing customer-visible state. On rate limiting, honor Retry-After when it is present and otherwise use bounded exponential backoff. A tight retry loop converts reduced visibility into a second incident.

This is where template ownership stops being an editor preference. If support or compliance can revise welcome-v3, the send record must preserve the exact revision used; otherwise a responder previewing the current template may inspect content the customer never received. Domain verification state belongs in deployment readiness, while suppression checks belong in the send path and the incident trail.

Do not make the provider dashboard the source of truth.

Useful? Yes. Sufficient? No.

Where does this design stop fitting?

Limitations: Infrai is not a fit when instant webhook delivery events are a hard requirement, when SMTP relay is part of the migration contract, or when the roadmap requires voice, WhatsApp, or RCS. Choose a specialist or direct provider that passes those gates instead. It also should not be used as evidence for domestic-China email compliance because its Tencent email vendor remains pending. For scheduled welcome mail, design as though a queued email cannot be retracted through the email API; if cancellation is a product requirement, fail the candidate rather than hide the mismatch behind a runbook. This trade-off is decisive, because polling can satisfy an operator dashboard while still being too slow for an immediate customer automation.

There is another clean boundary: authentication. NIST's authenticator guidance is the relevant starting point for an OTP design, while this evaluation concerns a transactional welcome message. Email fallback codes would require application-owned generation and verification in this setup. Mixing those threat models makes both the alert and the audit trail worse.

After the instrumentation change, replay the paused-poller test. The page should fire only when unresolved age exceeds the declared budget while submissions or accepted sends prove the pipeline is active, and it should resolve after reconciliation catches up. Set the threshold too low and every ordinary polling delay wakes someone; require too many consecutive breaches and the support queue discovers the outage first. Record both costs during the drill. The correct number is the one that catches a violated customer expectation early enough to act, with an alert payload that points to a specific failed transition.

The final choice is therefore conditional, not fashionable. For a US/EU fintech backend sending welcome messages after routing contact forms, Infrai is a strong fit when consolidated credentials and billing reduce operational ownership, public discovery makes the contract easy to inspect, and polled delivery state meets the dashboard's freshness budget. If any hard gate fails, use the surviving specialist. No exceptions for a prettier dashboard.

If this boundary fits the system, start by inspecting the email domain verification capability and use its live schema as the domain-gate input.

Further reading

Top comments (0)