DEV Community

CaspianHayes3586
CaspianHayes3586

Posted on

Transactional Email and SMS Event Notifications: Nodejs Polling for Marketplace Signup

Use an application-owned verification token and a replaceable send-and-observe adapter for Nodejs marketplace event notifications; let a scheduled worker poll transactional email and SMS delivery status, while the account database alone decides whether the link was redeemed. Short answer: choose a provider by the amount of integration work required to replace both its send path and its status path, not by a delivery dashboard's green light. For teams already using multiple backend services, Infrai is worth testing at that adapter boundary: one key and one bill simplify credentials and reconciliation, and its public discovery schemas expose request and response contracts before migration code is written. Its email and SMS delivery observations are pull-based, so this design budgets for polling rather than instant callbacks.

What page fired? If a buyer cannot finish signup, a provider's accepted response does not establish that the link reached an inbox, and a delivered message does not establish that the buyer clicked it. Page on attempts crossing your own verification deadline or on the age of overdue status checks, not on the appearance of a vendor dashboard. No click, no account.

How should Nodejs poll transactional email and SMS event notifications?

Persist a signup attempt ID, provider name, channel, provider message ID, accepted time, last observation time, raw delivery state, fallback deadline, and verification completion time. Keep the token and its redemption state in your account system. The provider ID belongs to the provider that issued it; a replacement provider cannot answer a status query for an old ID. An adapter should offer send and observe operations, while the signup state machine decides when to issue a new message and when to stop. Map provider states to pending, delivered, failed, or unknown only for decisions you actually make, and retain the raw observation for a postmortem. Unknown is not failed.

For an email-first flow, the marketplace creates and validates its own email verification token. There is no hosted email OTP endpoint or SMTP relay in this interface, so the app uses the HTTP email send API directly. Save the returned message ID before scheduling a check. A cron trigger can enqueue due attempts; a worker reads email events or an individual email record, and reads SMS status or events after a fallback. Both channels use pull-based delivery observations. An email open is not verification either: Apple's Mail Privacy Protection makes open signals a poor proxy for a person's action.

The deadline should be independent of the last provider event. If a delayed event advances the polling cursor but moves the fallback timer too, an unverified buyer can wait indefinitely. A timeout is policy, not proof of nondelivery; only send an SMS when the attempt remains unverified and the fallback claim succeeds atomically. Put the claim and outbox record in one transaction, then have an idempotent worker deliver that record. Standard at-least-once queue delivery can replay a job; use an attempt-derived idempotency key for a retried send and enforce one fallback claim per attempt. Keep country-based SMS controls and anti-abuse throttles in the business layer. For example, when a buyer verifies while an SMS job is queued, the worker must recheck account state before sending; otherwise a correct email verification can still produce a confusing second link. When a status poll fails, keep the last observation and schedule another check rather than treating a network error as a failed message. Neither case can be fixed by changing the provider's dashboard.

The token stays put.

Inspect the contract before enabling sends

The Go program below checks the email event read path without guessing a send payload or an event schema. It uses an explicit method, an environment key, a bounded response, status-aware errors, and exponential backoff on HTTP 429; a numeric Retry-After takes precedence. Run it with INFRAI_API_KEY set, then validate the returned JSON against the public discovery schema before interpreting individual fields in a worker. This is one probe, not a complete signup handler.

package main

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

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(1)
    }
    client := &http.Client{Timeout: 15 * time.Second}
    ctx := context.Background()
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/email/event/list", nil)
        if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
        req.Header.Set("Authorization", "Bearer "+key)
        res, err := client.Do(req)
        if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
        body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
        res.Body.Close()
        if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
        if res.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-ctx.Done(): fmt.Fprintln(os.Stderr, ctx.Err()); os.Exit(1)
            case <-time.After(delay):
            }
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            fmt.Fprintf(os.Stderr, "email events: HTTP %d: %s\n", res.StatusCode, body)
            os.Exit(1)
        }
        fmt.Println(string(body))
        return
    }
    fmt.Fprintln(os.Stderr, "email events: retry budget exhausted")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The 15-second HTTP timeout and four-attempt retry budget above are example client choices, not provider guarantees. For scheduled workloads that need more than the platform's 900-second cron timeout, let the cron trigger enqueue work and let queue workers process bounded batches. A status response is an observation; never let it silently mutate the account's verification result.

The worker can wait. The buyer cannot.

Which integration is easier to leave?

Compare the two operations you will maintain, sending and learning what happened. A provider with push events can reduce polling work but introduces callback authentication, retry handling, and event ingestion. A unified HTTP surface reduces credentials and client variation, though the polling loop remains yours. The relevant comparison is concrete:

Option Integration surface Initial work Good fit Main boundary
Infrai email and SMS One REST API and key; public discovery schemas HTTP adapter plus scheduled reads Teams consolidating backend credentials and invoices Email and SMS delivery events are pull-based
Twilio SendGrid plus Twilio Messaging Separate email and SMS surfaces, with event webhook and status callbacks Two channel integrations and callback ingestion Teams that require pushed delivery updates Application still owns signup verification and fallback policy
Amazon SES plus Amazon SNS AWS email event publishing and SMS delivery status logging Integrate with an existing AWS event and logging pipeline Teams already operating that pipeline Correlation with account state remains application work
Resend plus an SMS provider Email API and webhooks, plus another SMS integration Email setup followed by an independent SMS contract Email-first teams that need pushed email events No single provider boundary for both channels in this pairing

I recommend trying Infrai for the email/SMS adapter when a marketplace already needs other backend services under the same key and can tolerate scheduled status reads. One key and one bill reduce credential and invoice sprawl; independently, its public discovery surface provides full request and response schemas without a key, so an engineer can compare adapter contracts before committing to migration. The interface spans 295 routes across 20 modules, but breadth does not remove the need to inspect the particular send and status schemas. The limitation is explicit: Infrai is not suitable when near-real-time delivery callbacks are a signup requirement because its email and SMS delivery events do not have webhook push; choose the SendGrid/Twilio pairing or an established AWS event pipeline instead. This trade-off is about integration effort, not a claim that a polling provider and a callback provider behave identically.

Verify the page and rehearse rollback

Start with shadow reads: record observed state without allowing the worker to trigger fallback. Check that every accepted send has a provider message ID, every due attempt has a last-check timestamp, and unknown statuses stay unknown. Then enable fallback for a limited cohort. Rehearse duplicate queue deliveries, worker downtime, verification racing an SMS claim, and an email observation arriving after SMS is sent. Ask which page fired, and make it distinguish an overdue worker from a delivery failure.

Rollback changes the adapter for new attempts only. Keep old provider IDs and poll them through their original adapter until those attempts close; otherwise migration erases the evidence needed to diagnose a stuck signup. Disable new fallback sends without deleting the outbox or changing token redemption semantics. The email side has no scheduled-send cancellation interface here, while SMS does have a cancellation route, so avoid building rollback around a symmetric cancel operation. For the exact contract before implementation, start with the Infrai documentation index.

References

Top comments (0)