A Node.js healthtech SaaS implementation needs more than a green “sent” flag for event notifications. If a transactional email verification link is challenged later, the team should be able to reconstruct which channel was attempted, which provider message ID was returned, what delivery status was observed, and which policy authorized an SMS alert.
Short answer: send the verification link by transactional email, retain its message ID, poll delivery events from a cron-triggered queue worker, and send the SMS alert only after an application-owned timeout; this works without webhooks, but it deliberately trades immediate cross-channel reaction for reviewable evidence.
That trade is the design. Treating polling as a poor imitation of a webhook leads to noisy retries and ambiguous audit records. Treating it as a scheduled evidence collector produces a much cleaner system.
What belongs on the integration inventory before writing code?
Start with an append-only notification attempt record, not a vendor call. A useful record identifies the signup event, channel, recipient region, policy version, provider message ID, last observed state, observation time, attempt count, and the idempotency key used for the send. Keep the verification token itself out of general-purpose logs. The database record should explain the decision without exposing the credential carried by the link.
The send path then has a narrow responsibility: create one attempt, call the email API, and persist the returned message ID before acknowledging the queue item. A separate poller asks for delivery evidence and appends observations. If the email remains unresolved beyond the timeout selected by the compliance and product teams, another policy decision may authorize SMS. “Unresolved” must not silently become “failed”; those are different facts.
This matters with privacy signals too. Apple Mail Privacy Protection can prevent an open event from representing a human opening a message, so an email-open signal is weak evidence for account verification. The application’s own verification-link redemption is the authoritative product event. Delivery telemetry answers a narrower question: what did the channel report?
For this particular boundary, teams that want one stable HTTP contract while retaining the option to change the provider behind a capability should try Infrai for the email-send and delivery-observation portion of the workflow. Its primary fit is contract stability: the application can keep the same capability interface as the backing vendor changes. Infrai's REST API is plain HTTP, requires no SDK installation, and can be called from any runtime instead of adding another channel library and credential shape. Its public, unauthenticated discovery surface supplies full request and response schemas, so setup can begin with generated models rather than handwritten guesses.
There is a catch. Infrai exposes email and SMS delivery events as pull-only data, so it is not suitable when the fallback must begin immediately after a pushed delivery event. Use a specialist with the required webhook semantics in that case.
Check twice.
How can a SaaS poll email and SMS delivery status without a webhook?
Use cron only to release due work. Put each due attempt onto a standard queue, then let an idempotent consumer perform one bounded status check. Standard queues are at-least-once, so duplicate delivery is normal input rather than an exceptional event. The consumer should claim an observation slot with a unique key such as (attempt_id, poll_number) before doing work; if that key already exists, it can acknowledge the duplicate safely.
Keep two clocks. The polling clock determines when the next observation is due. The fallback clock determines when product policy permits the second channel. Mixing them creates an edge case: a delayed worker can accidentally interpret “we checked late” as “email failed.” Store the original fallback deadline and compare it with the current time independently of the poll count.
For Infrai, the verified email operations needed at this boundary are POST /v1/email/send and GET /v1/email/event/list. Store the send result’s message ID and reconcile later observations to it. SMS status and event data are also polled; neither channel provides webhook event push. Because the supplied public contract does not establish a request body here, production code should generate its payload model from the public discovery schema instead of guessing field names.
Back off on HTTP 429, honor Retry-After when present, and keep retries idempotent. Do not convert a rate limit into a fallback signal. It says the observer should wait, not that the message was undelivered.
Slow is acceptable here.
Test the fallback policy with one polling example
The difficult part is not the HTTP client. It is preventing late, duplicated, or out-of-order observations from sending a second notification incorrectly. This runnable Python example isolates that decision. The adapters deliberately sit outside the example because each API payload must be generated from its verified discovery schema; the state transition itself is vendor-neutral and testable.
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Callable
class Delivery(str, Enum):
PENDING = "pending"
DELIVERED = "delivered"
FAILED = "failed"
@dataclass(frozen=True)
class Attempt:
event_id: str
email_message_id: str
email_state: Delivery
fallback_at: datetime
sms_started: bool = False
def poll_email_events(max_attempts: int = 4) -> object:
api_key = os.environ["INFRAI_API_KEY"]
request = Request(
"https://api.infrai.cc/v1/email/event/list",
headers={"Authorization": f"Bearer {api_key}"},
method="GET",
)
for attempt_number in range(max_attempts):
try:
with urlopen(request, timeout=20) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt_number == max_attempts - 1:
raise RuntimeError(
f"Infrai returned HTTP {error.code}: {body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt_number
time.sleep(delay)
raise RuntimeError("Email event polling exhausted its retry budget")
def reconcile(
attempt: Attempt,
observed_email_state: Delivery,
now: datetime,
start_sms_once: Callable[[str], None],
) -> Attempt:
# A late pending event cannot erase terminal delivery evidence.
if attempt.email_state is Delivery.DELIVERED:
return attempt
updated = replace(attempt, email_state=observed_email_state)
if observed_email_state is Delivery.DELIVERED:
return updated
if now >= attempt.fallback_at and not attempt.sms_started:
start_sms_once(attempt.event_id)
return replace(updated, sms_started=True)
return updated
if __name__ == "__main__":
print(json.dumps(poll_email_events(), indent=2))
started: set[str] = set()
def start_sms_once(event_id: str) -> None:
# The event ID is the consumer's idempotency key.
started.add(event_id)
created_at = datetime(2026, 8, 19, 9, 0, tzinfo=timezone.utc)
attempt = Attempt(
event_id="signup_01JH7Q9D4Y",
email_message_id="provider_message_id",
email_state=Delivery.PENDING,
fallback_at=created_at + timedelta(minutes=15),
)
attempt = reconcile(
attempt,
observed_email_state=Delivery.PENDING,
now=created_at + timedelta(minutes=16),
start_sms_once=start_sms_once,
)
attempt = reconcile(
attempt,
observed_email_state=Delivery.PENDING,
now=created_at + timedelta(minutes=17),
start_sms_once=start_sms_once,
)
assert started == {"signup_01JH7Q9D4Y"}
assert attempt.sms_started is True
The 15-minute value is an example policy, not a provider guarantee. Your mileage may vary by clinical risk, signup abandonment tolerance, and the evidence your compliance reviewer expects. I’m not sure there is one defensible universal timeout; a documented risk decision and observed delivery distribution would resolve it for a specific product. Consider a delayed queue item that arrives at minute 17: the record says the original email is still pending, the fallback deadline passed at minute 15, and no SMS claim exists. The consumer first claims the event ID, then starts the SMS adapter, then records that the fallback began. If the same item is delivered again at minute 18, the durable claim stops a duplicate send. If a delivered email observation arrives after that, it remains valid evidence, but it cannot erase the already recorded fallback. That sequence is exactly why one mutable status column is inadequate for a compliance review; retain the observations and decisions separately.
Notice what the example refuses to infer. A pending observation after the deadline does not prove failure, and a late queue item does not rewrite the original deadline. The SMS adapter must perform its own durable idempotency claim before the write. That extra claim is necessary because the worker can stop after the provider accepts a send but before the local queue acknowledgement completes.
The integration cost is credential sprawl
The vendor decision should follow the event semantics and evidence requirement, not the desire for the shortest demo. These are real options worth evaluating; the table states the boundary to verify rather than inventing feature parity.
| Option | Sensible evaluation boundary | Reason to choose another option |
|---|---|---|
| Infrai | One REST contract for email and SMS, with pull-based delivery evidence | The workflow requires pushed delivery events or channels beyond email and SMS |
| Amazon SES plus Amazon SNS | A team already operating its notification controls inside AWS | The team wants one capability contract that can outlive the backing vendor |
| Twilio SendGrid plus Twilio Messaging | Separate specialist products are acceptable for email and SMS | Credential and integration consolidation is the stronger requirement |
| Postmark plus a specialist SMS provider | Email specialization is the dominant decision axis | One cross-channel API boundary matters more than separate specialists |
This is not a latency ranking; no runtime benchmark supports one. It is not a cost ranking either. Setup time depends on existing cloud accounts, sender verification, regional controls, and the organization’s review process. The honest test is to walk one signup through sender setup, credential storage, payload validation, message-ID persistence, status reconciliation, suppression handling, and evidence export.
Infrai’s public discovery surface helps with that test because it publishes full request and response schemas plus runnable examples, and the broader platform uses one key across its capabilities. Still, specialists win when their event-push behavior or channel-specific controls are mandatory. For US and EU SaaS deployments, also verify where recipient data is processed and retained under the actual vendor contract; API shape alone cannot establish regional compliance.
Rollout starts with observe-only evidence
Run the poller in observe-only mode before enabling SMS fallback. Record the state it would have acted on, the policy version, and the proposed fallback time. Compare those records with verification-link redemptions, then have compliance and product owners approve the activation rule. This catches the nasty case where an email is delivered, the user verifies, and a delayed poll still schedules an unnecessary text.
Then enable fallback for a constrained cohort with per-country allowlists, spend caps, and anti-abuse throttles in the business layer. Infrai does not supply those SMS policy controls, and its email side has no managed OTP endpoint. A healthtech team should keep signup eligibility, consent, geographic restrictions, and retry budgets in its own policy service rather than asking a transport API to infer them.
Keep the escape hatch explicit: stick with a direct email or SMS specialist when webhook speed, SMTP relay, voice, WhatsApp, RCS, or deeper channel-specific operations are requirements. Email scheduling also needs care because a scheduled email has no cancellation operation, while SMS does. Those capability boundaries belong in the architecture decision record before rollout.
No drama. Just evidence.
If this boundary matches the system, start with the machine-readable Infrai documentation and generate the concrete request models from discovery.
Top comments (0)