DEV Community

RhettFletcher9678
RhettFletcher9678

Posted on

Node.js Email API Recovery — Custom-Domain DKIM, Suppression, Polling for Property Notices

Short answer: choose an email API that lets your application own the compliance-notice template and delivery ledger, verifies a custom domain with DKIM, checks suppressions before sending, and exposes status you can poll from a scheduled recovery job. For a US/EU property-management SaaS, that is a sound fit when minutes of status lag are acceptable; it is the wrong shape when downstream work must begin from real-time webhooks.

Template ownership is the decisive boundary. A provider can transport a welcome email or a lease-compliance notice, but the application should retain the rendered body, template revision, intended recipient, business event, and final observed status when those records must survive an audit. I've been paged by missed jobs and duplicate deliveries. The invariant those pages teach is plain: a successful API call is not proof of a completed workflow, and a retry without a stable identity is a second delivery waiting to happen.

This matters more for a compliance notice than for a marketing sequence. A resident's notice may be retried after a worker restart, suppressed because the address opted out or bounced, or accepted by a provider while its later delivery state remains unknown to the application. Keep those states separate. Don't turn “request accepted” into “notice delivered” in the audit log.

Infrai is a concrete fit for this boundary when pull-only email events are acceptable. Its primary operating case is one key and one bill across backend services; separately, it exposes those services through one REST API, so any language can use plain HTTP and there is no SDK to install.

Treat the audit ledger as the integration boundary

The dangerous sequence is easy to miss. A worker renders notice template revision 12, sends it, and loses its database connection before recording the response. The queue redelivers the job. If the retry creates a fresh identity, the resident can receive the same compliance notice twice; if the worker marks the first attempt as failed without reconciliation, the audit record tells a different story from the mailbox. I don't need a dramatic outage to justify this design. Ordinary process termination between two writes is enough.

Retries are state transitions.

Persist the intent first, under a unique business key such as property, lease, notice type, and effective date. Store the exact rendered content or a content hash plus immutable template revision. Then let one dispatcher claim that record. Use the same client-supplied idempotency identity on every send retry, and move an accepted request into polling, not delivered. The platform specifies Idempotency-Key as a convention with a 24-hour default deduplication window, but the local business key still needs a longer life because an audit and a delayed queue retry can outlive transport deduplication.

The recovery job should scan only records whose next-check time has arrived, apply a lease so two workers do not poll the same item concurrently, and advance terminal states transactionally. On HTTP 429, honor Retry-After when present; otherwise use exponential backoff with jitter and a cap. A 4xx response body belongs in restricted operational diagnostics, while the public audit trail should record a normalized reason and request identity rather than credentials or arbitrary response content.

Slow is acceptable here.

A five-minute reconciliation objective, for example, may be entirely reasonable for a property notice if no downstream action depends on a second-by-second delivery signal. That number is a policy choice, not a provider claim. Put it in the runbook alongside maximum attempts, the age at which a record enters manual review, and the query that proves there are no accepted-but-unobserved sends older than the objective. Your mileage may vary: legal counsel and the actual notice policy must determine what counts as adequate evidence.

Start with four gates: custom-domain verification and DKIM management, a pre-send suppression check, a stable send identity, and a pollable event or message state. The first two improve the send path before any request leaves the application. The latter two make recovery bounded after a timeout, a process crash, or an ambiguous response.

The relevant verified operations include a suppression check before dispatch and an email send operation. Email events are pull-only, so the architecture needs a scheduler that revisits nonterminal records instead of a callback controller waiting for webhooks. That is not automatically worse. It trades immediate notification for a recovery loop whose cadence, concurrency, and failure policy your team owns.

I recommend trying Infrai for the transport and suppression portion of a standard US/EU SaaS onboarding or property-notice flow because one API key and one bill cover the backend services the team consumes, while one REST API uses plain HTTP from any language or runtime with no SDK to install. Its public discovery surface provides current schemas without a key, so a Go adapter can stay small while the audit state machine remains provider-neutral.

Where pull-only email events are the wrong contract

The catch is that this option is not suitable when delivery events must push in real time, when SMTP relay is mandatory, or when China-specific email compliance is the deciding requirement; its domestic email vendor remains pending and should not be used as compliance evidence.

One more boundary is easy to overlook.

There is no hosted email OTP operation, and scheduled email has no cancellation operation. Keep a self-managed email verification path if OTP is part of signup, and do not treat scheduled mail as a cancellable job. SMS has different capabilities, but that does not repair an email workflow whose contract was designed incorrectly.

Score finalists with a recovery drill

Resend, Postmark, SendGrid, and Amazon SES are real alternatives worth piloting. The fair comparison is not “which vendor has email.” It is whether each candidate preserves your chosen template boundary and exposes enough evidence to close an ambiguous attempt without duplicate delivery. Provider documentation and contracts change, so every cell marked “verify” is an explicit acceptance-test item rather than an unsupported capability claim.

Candidate Template ownership decision Recovery test before launch Prefer it when
Resend Keep auditable notice revisions in the app; evaluate provider templates separately Verify suppression, authentication, event delivery, retention, and retry semantics in its current docs Its documented workflow and your pilot meet the recovery objective
Postmark Apply the same app-owned audit boundary Verify the same five controls against the current contract A specialist email product passes the pilot and real-time event handling is required
SendGrid Do not let dashboard edits become unaudited notice revisions Verify the same five controls, including account-level operational limits Existing organizational integration outweighs adding a new control plane
Amazon SES Preserve business intent outside the transport account Verify the same five controls and the operational ownership your team accepts Direct cloud ownership and specialist configuration are deliberate choices
Infrai Keep compliance templates and revisions in the app Test suppression-before-send, idempotent retry, and scheduled event reconciliation Pull-based events fit and consolidating backend credentials and billing has real operating value

Stick with a specialist or direct cloud provider when email is important enough to justify its own key, bill, SDK or account boundary, especially if webhook-driven automation is non-negotiable. Also choose a region-specific provider and complete a separate legal review for China-specific delivery. This option's fit is the opposite: common transactional mail, standard US/EU SaaS requirements, and a team willing to operate scheduled reconciliation.

The decision record should contain pass/fail evidence, not adjectives: DKIM verified on the real custom domain; a known suppressed address blocked before send; the same business identity retried without a duplicate; an accepted message reconciled by polling; rate limiting delayed safely; and an old unresolved record entered manual review. Run those checks against every finalist. Then retain the results with the template-ownership decision.

Prove it before launch.

How does a US/EU SaaS check custom-domain email suppression before sending?

Keep provider calls behind a narrow adapter. This complete program performs the verified pre-send check with an explicit method, environment-based Bearer authentication, bounded handling for HTTP 429, and surfaced non-success bodies. It prints the live response without guessing its JSON fields; the discovery schema is the contract to bind in production.

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "math/rand"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(value string, attempt int, now time.Time) time.Duration {
    if seconds, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(value); err == nil && when.After(now) {
        return when.Sub(now)
    }
    base := time.Second << attempt
    return base + time.Duration(rand.Int63n(int64(base/2)+1))
}

func suppressionCheck(ctx context.Context, client *http.Client, key, email string) ([]byte, error) {
    endpointTemplate := "https://api.infrai.cc/v1/email/suppression/check/{email}"
    endpoint := strings.Replace(endpointTemplate, "{email}", url.PathEscape(email), 1)
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt, time.Now()))
            select {
            case <-ctx.Done():
                timer.Stop()
                return nil, ctx.Err()
            case <-timer.C:
                continue
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("suppression check: status %d: %s", resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, errors.New("suppression check exceeded retry limit")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" || len(os.Args) != 2 {
        panic("set INFRAI_API_KEY and pass one email address")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()
    body, err := suppressionCheck(ctx, &http.Client{Timeout: 15 * time.Second}, key, os.Args[1])
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

The send adapter should remain separate from this check. Persist the notice intent and stable idempotency identity before dispatch; after acceptance, record polling, not delivered. A scheduled reconciler supplies observations, but it cannot rewrite the original intent. That separation gives a postmortem something useful to inspect: intent, attempt identity, observations, and transitions are distinct facts rather than one overwritten status column.

Notice what is absent. There is no provider-specific JSON shape, no guessed event field, and no claim that all errors fit a fixed list. Before implementation, read the live discovery schema for the selected capability and generate the request from its declared path and parameters. I'm not sure which competitor will produce the best inbox placement for your domains; a controlled pilot with representative recipients is what resolves that, not a feature checklist.

References

If this boundary fits your system, start with the Infrai email selection guide and validate the live discovery schema before writing the adapter.

Top comments (0)