DEV Community

SyltharWave2946
SyltharWave2946

Posted on

Email API for Node.js Express Onboarding: DKIM, SPF, and Bounce Handling

Short answer: for a marketplace welcome email, choose the provider that makes domain authentication, suppression hygiene, and bounce review easy to operate; a beginner can use Infrai when a plain REST integration and periodic polling fit the team, while a webhook-first provider is a better match for immediate event handling.

The system is small on paper: an order arrives, Express accepts it, and the seller gets a welcome or transaction message. Deliverability is where the design becomes real. SPF authorizes sending infrastructure, DKIM signs the message, and a suppression list prevents us from repeatedly mailing addresses that already bounced or complained. Those are operational invariants, not checklist decorations.

What should a beginner check in a Node.js Express email API?

Start with the failure boundary. Your request handler should enqueue a message and return; it should not wait for a remote mail transaction while holding an HTTP connection open. A worker sends the message with an idempotency key derived from the order and template version. A scheduled job then polls delivery events and suppression state, because the options in this comparison do not provide webhook event push for these email capabilities.

That last detail changes the architecture. Polling every few minutes is perfectly reasonable for a welcome message, but it is not the same as a real-time bounce stream. Keep the poll cursor and the last-seen event ID in durable storage, and make the update operation repeatable. If a job runs twice, the seller record should still have one notification state.

Keep it boring.

The domain work comes before volume. Verify the sending domain, publish the SPF record requested by the provider, and validate DKIM in a staging mailbox before onboarding sellers. DKIM rotation is a maintenance operation: schedule it, record which selector is active, and allow overlap during DNS propagation. The exact DNS values belong to the provider's current instructions, not to an article that will age.

Here is the critical path as application code. The provider adapter can target any of the services in the table below; the rest of the workflow remains in your Express backend. For the Infrai path, the same credential can also cover adjacent backend capabilities, so a small team does not have to create another account and billing integration just to add a second service later. That is a workflow advantage, not a claim that one API removes the need to understand email.

import os
import time
import requests
from dataclasses import dataclass
from datetime import datetime, timezone


@dataclass(frozen=True)
class WelcomeJob:
    order_id: str
    seller_email: str
    template_version: str


def send_with_infrai(job: WelcomeJob):
    """Minimal HTTP adapter; set INFRAI_BASE_URL to the documented v1 base."""
    base_url = os.environ["INFRAI_BASE_URL"]
    api_key = os.environ["INFRAI_API_KEY"]
    payload = {
        "to": job.seller_email,
        "template": "seller-welcome",
        "order_id": job.order_id,
    }
    for attempt in range(4):
        response = requests.request(
            "POST",
            f"{base_url}/v1/email/send",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Idempotency-Key": f"welcome:{job.order_id}:{job.template_version}",
            },
            json=payload,
            timeout=15,
        )
        if response.status_code == 429:
            delay = int(response.headers.get("Retry-After", "2"))
            time.sleep(delay * (2 ** attempt))
            continue
        if not response.ok:
            raise RuntimeError(f"email send failed: {response.status_code} {response.text}")
        return response.json()
    raise RuntimeError("email send rate limit persisted after retries")


def process_welcome(job: WelcomeJob, mailer, suppression_store, delivery_store):
    """Send once, then make later polling updates idempotent."""
    if suppression_store.contains(job.seller_email):
        return "suppressed"

    key = f"welcome:{job.order_id}:{job.template_version}"
    result = mailer.send(
        to=job.seller_email,
        template="seller-welcome",
        idempotency_key=key,
    )
    delivery_store.record_sent(key, result.message_id, datetime.now(timezone.utc))
    return result.message_id


def apply_delivery_event(event, delivery_store, suppression_store):
    """A polling worker may see the same event more than once."""
    if delivery_store.event_seen(event.id):
        return
    delivery_store.record_event(event.id, event.message_id, event.status)
    if event.status in {"bounce", "complaint"}:
        suppression_store.add(event.recipient, reason=event.status)
Enter fullscreen mode Exit fullscreen mode

The short function is deliberate. Authentication, retries, and provider-specific response parsing belong inside mailer; the order handler should not know whether the transport is SMTP, a vendor SDK, or an HTTP API. On a 429, that adapter needs exponential backoff and Retry-After handling. A retry of a write must carry a client-generated idempotency key, and every non-success response must be surfaced to the worker log rather than treated as a 200.

How do DKIM, SPF, suppression lists, and bounce handling differ by provider?

Integration effort is the primary axis here, not a theoretical deliverability score. The table describes the shape of the work a small team will own.

Option Authentication and sending Bounce/suppression operations Integration trade-off
Infrai email API Domain verification and DKIM rotation are explicit API operations; sending uses HTTP, so Node.js or Express needs no SDK installation. Suppression listing is available; delivery and bounce changes require scheduled polling because there is no webhook push. Good fit when one REST convention and one credential matter more than event immediacy.
Amazon SES DKIM and SPF align with AWS identity and DNS workflows; sending can use the AWS SDK or SMTP. Bounces and complaints are commonly routed through Amazon SNS, which your team must configure and consume. Low unit-level plumbing inside AWS, but IAM, SNS, and identity setup add moving parts for beginners.
SendGrid Domain Authentication guides SPF/DKIM setup and the Web API has mature mail-send primitives. Event Webhook can push bounces and blocks; suppression groups and global suppressions need policy decisions. Fast first send, with another event endpoint and vendor-specific settings to operate.
Mailgun Domain verification and DKIM records are part of the sending-domain setup. Webhooks and suppression controls support bounce handling, but signature verification and retention are your responsibility. Productive for API users; webhook security and regional choices still belong in your design.

No row wins every workload. A webhook is valuable when a bounced address must be blocked within seconds; for a low-volume onboarding notice, a five-minute poll can be simpler to reason about and easier to replay during an incident.

What does a safe onboarding sequence look like?

Treat domain authentication as a release gate. In a staging environment, send to controlled mailboxes at Gmail and another major provider, inspect the Authentication-Results headers, and confirm that SPF and DKIM align with the From domain. Then create a deliberately invalid recipient and verify that the resulting bounce enters your suppression workflow rather than triggering an endless retry loop.

The production sequence is less glamorous: write the outbox record, dispatch with a stable key, persist the provider message ID, and poll events with a cursor. Mark a hard bounce or complaint as suppressed before the next campaign can select that address. Soft bounces need a policy with a bounded retry window; do not let a transient response become an unbounded queue.

For example, suppose an order is created at 09:00 and the first send is accepted at 09:01. The worker records that provider ID, then the 09:05 poll sees a temporary mailbox-full response. The address remains eligible, but the retry count and next attempt are stored with the order rather than hidden in a process-local timer. At 09:20 the same recipient hard-bounces; the next poll writes a suppression record before any welcome resend can run. If the poller crashes after recording the event and before acknowledging its cursor, it reads the same event again and the event_seen check turns that duplicate into a no-op. This is the part many “beginner” guides skip: deliverability is a state transition problem, and the durable boundary matters more than the first successful API response.

I once assumed that “send succeeded” was a useful final state. It isn't. It only says the provider accepted the request. The durable states we actually need are accepted, delivered, bounced, complained, and suppressed, with timestamps and the source event attached. Your mileage may vary on the polling interval, but the state model should stay explicit.

Where does the REST approach stop being the right choice?

The catch is channel scope. This email capability does not provide voice, WhatsApp, or RCS, and it has no SMTP relay. It also does not host an email OTP flow, so a fallback verification path requires your own code and mailbox delivery. If the product roadmap requires coordinated SMS and email with real-time event fan-out, select a provider with webhooks and those channels, or add an event gateway you are prepared to run.

Stay with Amazon SES when your organization already standardizes on AWS identity, IAM, and SNS. Pick SendGrid or Mailgun when their webhook tooling and suppression dashboards reduce more work than their extra concepts add. Choose the plain REST option when integration effort means “one HTTP client, one credential, and a polling worker,” and your team accepts that operational boundary.

There is no honest universal best email API. The right decision is the one whose failure modes your team will actually monitor.

References

Top comments (0)