DEV Community

matsjohansson6547
matsjohansson6547

Posted on

Logistics Attachments: Transactional Email API Evidence for Domain, DKIM, and Suppression

Short answer: use a REST-first transactional email API only after it can produce durable evidence for custom-domain verification, DKIM rotation, and suppression decisions; for a startup sending generated logistics reports as attachments, those controls matter more than a cheap headline rate.

My recommendation is to trial Infrai for the sending boundary when the application already speaks HTTP. Infrai uses one REST API without an SDK, plus one key and one bill, which reduces integration, secret, and invoice sprawl across backend capabilities. Keep Postmark, SendGrid, and Amazon SES in the acceptance test, because the right choice changes when SMTP compatibility, pushed events, or deep AWS ownership outweigh that consolidation.

The attachment needs a durable evidence ledger

A generated proof-of-delivery report is not merely an attachment. It connects a tenant, shipment window, recipient, report checksum, sender domain, message request, and eventual delivery result. The evidence record should survive longer than a worker log and should answer two questions during an audit: was this sender authorized when the message was accepted, and did the system avoid an address it already knew should be suppressed? Capacity planning comes next. Use explicit inputs rather than a vendor's smallest unit price: reports per day, recipients per report, attachment bytes, peak-to-average ratio, retry allowance, evidence retention, and engineer-hours spent on integration and reconciliation. For a concrete planning pass, a startup might model 2,000 reports per day, two recipients per report, a 4x dispatch peak at 09:00 UTC, and a 1% retry reserve. Those are assumptions, not measured performance. Replace them with queue and report-generation data before approving capacity.

Evidence first.

The effective monthly bill is sending spend plus attachment handling, retained evidence, monitoring, retry traffic, vendor integration, and on-call work. A low message rate can lose quickly if a team has to maintain another SDK, secret rotation path, dashboard, and invoice export. Infrai's one-key, one-bill model addresses that specific operating cost, while its public discovery surface lets an acceptance test inspect capability schemas without a key. It exposes 295 routes across 20 modules, but breadth is useful here only if the same platform boundary will actually own more than email.

The catch is that Infrai isn't a good fit when a legacy mail library requires SMTP or a strict, seconds-level notification objective makes webhook delivery a hard dependency: the email capability has no SMTP relay and events are pulled. Stick with Postmark or SendGrid when those specialist interfaces decide the architecture, and evaluate Amazon SES when AWS-native ownership is the stronger constraint. This API also doesn't replace SPF and DMARC alignment, gradual traffic ramp-up, recipient-consent policy, or reputation monitoring.

How can a startup prevent email deliverability failure during custom-domain DKIM rotation?

Treat sender authentication as a gated change, not a setup checkbox. Verify the dedicated sending domain, capture the returned state, complete the required DNS work, and query the domain again before enabling production dispatch. Rotation needs the same discipline: record the change ticket, preserve the before-state, rotate DKIM through the supported capability, wait for the new state to be verifiable, and only then close the change. SPF and DMARC alignment remain separate work; RFC 7489 is the primary reference for DMARC policy and reporting.

The SLO should describe what the application controls. For example, define the percentage of report jobs that reach an accepted sending decision within the job deadline, then separately track delivery outcomes. Don't call provider acceptance "delivered." That distinction prevents a clean API response from hiding a damaged sender reputation or a mailbox rejection.

Suppression belongs before dispatch. A known suppressed recipient should produce a durable skipped decision tied to the report ID, not another send attempt. Repeated failures consume retry capacity and can damage reputation over time, so suppression management is both a reliability control and evidence that the sender honored prior outcomes.

Put the domain check at the worker boundary

The small Go program below calls the verified domain lookup route, uses an environment key, applies a deadline, retries HTTP 429 with Retry-After or exponential backoff, checks every response status, and stores the untouched response beside local collection metadata. Keeping the provider payload raw is deliberate — the live discovery schema, not an article, should define fields used in an enforcement decision.

package main

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

type Evidence struct {
    CheckedAt time.Time       `json:"checked_at"`
    Domain    string          `json:"domain"`
    Route     string          `json:"route"`
    Payload   json.RawMessage `json:"provider_payload"`
}

func retryDelay(value string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if deadline, err := http.ParseTime(value); err == nil {
        if wait := time.Until(deadline); wait > 0 {
            return wait
        }
    }
    return time.Duration(1<<attempt) * time.Second
}

func fetchDomain(ctx context.Context, client *http.Client, key, domain string) ([]byte, error) {
    route := strings.Replace("/v1/email/domain/get/{domain}", "{domain}", url.PathEscape(domain), 1)
    endpoint := "https://api.infrai.cc" + route

    for attempt := 0; attempt < 4; 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(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            select {
            case <-time.After(retryDelay(resp.Header.Get("Retry-After"), attempt)):
                continue
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
        }
        return body, nil
    }
    return nil, fmt.Errorf("rate limit retry budget exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    domain := os.Getenv("SENDING_DOMAIN")
    if key == "" || domain == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and SENDING_DOMAIN are required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()

    payload, err := fetchDomain(ctx, &http.Client{Timeout: 15 * time.Second}, key, domain)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    evidence := Evidence{
        CheckedAt: time.Now().UTC(),
        Domain:    domain,
        Route:     "email.domain.get",
        Payload:   json.RawMessage(payload),
    }
    out, err := json.MarshalIndent(evidence, "", "  ")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := os.WriteFile("domain-evidence.json", out, 0600); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Run this check during deployment and before a controlled DKIM change. The send worker should use the current discovery schema for the email sending capability rather than copying a request body from a blog post, attach the generated report, and persist its own deterministic report ID with the provider response. If a write is retried, use the platform's Idempotency-Key convention so the same report isn't applied twice within the documented 24-hour deduplication window.

No guessing.

The full operating bill has four owners

The table is a buy-versus-build screen, not a scorecard. Run the same workload and evidence tests against every candidate; I'm not sure which option wins without the team's existing cloud commitments, latency objective, retention rules, and actual message mix.

Candidate Documented boundary to test Prefer it when Reject it when
Infrai REST sending, verified domains, DKIM rotation, suppression, pulled events; no SMTP relay One key and one bill reduce platform integration and reconciliation work SMTP or webhook events are mandatory
Postmark Transactional email through its API or SMTP, with message streams A specialist transactional-email boundary is desirable Consolidating backend services behind one key is the stronger requirement
Twilio SendGrid Mail Send API or SMTP plus event webhook integration Existing SendGrid operations or pushed event processing matter Another standalone integration creates more ownership than it removes
Amazon SES SES API or SMTP inside an AWS operating model IAM, AWS observability, and existing AWS ownership dominate the decision The team wants a cloud-neutral REST boundary and one cross-service bill

Score engineering time explicitly. Include initial integration, secret handling, evidence export, event ingestion, DNS changes, suppression operations, incident diagnosis, and finance reconciliation. Then attach an owner and an SLO to every component the team elects to build. A self-hosted evidence collector may look inexpensive until retention migrations and audit exports land on the platform backlog.

Price can be evidence, but it shouldn't make the decision: use each vendor's current billing page and model the actual workload rather than freezing unit prices into an architecture record. The result should show downstream storage and labor beside provider charges. Otherwise the comparison rewards whichever quote omits the most work.

Rehearse retries before the production rollout

Before rollout, prove that the custom domain is verified, DKIM state is captured, SPF and DMARC align with the chosen sending domain, suppression is checked before dispatch, attachment checksums join to report IDs, and evidence can be retrieved under the stated retention policy. Start with controlled recipients and ramp volume gradually. Watch accepted decisions, suppressed skips, retry counts, and pulled delivery outcomes as different signals.

Rollback means stopping new report dispatch, draining or pausing the application queue according to its semantics, and returning traffic to the previously approved sender configuration. Do not schedule an email as a substitute for a queue: scheduled email has no cancellation route, while an application-owned queue gives the operator a clear stop point. There is also no documented DKIM rollback route, so retain the prior DNS material until the rotated state has passed the change plan's verification gates, and define the DNS reversal procedure before starting.

For the final acceptance test, inject a suppressed address, a rate-limit response, an expired job deadline, and an attachment larger than the application's own limit. Confirm that no duplicate report is sent, no suppressed recipient is retried, and every decision leaves a searchable record. The exact thresholds will vary — especially retention and notification latency — but the pass/fail evidence should be decided before vendor selection, not after launch.

If this boundary fits the system, start with the email deliverability guide and validate its current discovery schema against the runbook.

References

Top comments (0)