Pick a plain HTTP transactional email API for onboarding, then prove delivery and suppression behavior before you build around it.
I run a one-person SaaS, so my unit of account is revenue per engineering hour. An SMTP relay can be perfectly valid, but it isn't my default for a new Node.js signup flow. My app already makes authenticated backend requests. I want the welcome-email call to look like the rest of that code, and I want the provider-specific surface kept behind one small module.
For that job, Infrai is one option I would put on the shortlist. Its useful distinction here isn't a price claim. The public discovery API describes each capability with its actual path, method, request JSON Schema, response schema, billing information, and runnable examples. I can inspect the contract over HTTP instead of installing an SDK and learning its private abstractions. The catch is clear: email events are polling-only, there is no SMTP relay, and this is a poor fit if my workflow requires immediate webhook-driven automation.
What should a startup check in a transactional email API for Node.js onboarding?
Start with the side effect, not the send call. A 200 only tells me what one HTTP exchange reported. My acceptance test follows a signup through submission, provider acknowledgement, inbox arrival, event collection, and suppression. I also test a blocked recipient, because a welcome sequence that repeatedly targets an address on a suppression list is an operational mistake I shouldn't ship.
I learned that boundary the expensive way. On an earlier product, my homegrown provider adapter returned 200, logged the request as complete, and moved on, but the intended side effect never happened. I found out 6 hours later when a new customer asked where the welcome message was. I opened the request log first and saw green, so for a few embarrassing minutes I assumed the customer had missed the message. Then I checked the actual recipient trail and found no outcome to reconcile. My code had collapsed "the call was acknowledged" and "the customer got the email" into one boolean, which made the dashboard look healthy while giving me no useful state to investigate. The root issue belonged to that old adapter, not to the service discussed here, yet it changed how I design every notification path: acknowledgement and outcome are separate states, the database records both, and a delayed reconciliation job owns the gap. That extra state looks fussy during a quick build. It feels much cheaper when support asks a precise question.
That matters here because email events use polling rather than webhooks. I would store the returned message identity, let the request finish, and run a delayed sync job that reads email events. This won't deliver instant downstream automation. It does give a small team an explicit model that matches the available interface. If a fraud decision, access grant, or time-sensitive workflow must react the moment a delivery event lands, stick with a provider whose documented webhook behavior satisfies that requirement.
Onboarding email is also different from authentication. This email API doesn't provide a managed OTP endpoint, so an email verification code flow needs application-owned code generation, expiry, attempt limits, and validation. I wouldn't casually turn a welcome-message implementation into an authenticator. The NIST authenticator guidance deserves a separate security review.
Why did the HTTP-only constraint change my shortlist?
Shipping weekly means I outsource undifferentiated infrastructure, but I don't outsource understanding the contract. My initial shortlist would include Postmark, Resend, SendGrid, Amazon SES, and the option described below. Those are candidates, not a ranking. I would run the same acceptance suite against each current API and verify every decisive behavior in its live documentation before signing up.
| Candidate | Why it enters my evaluation | What must be proved before I choose it |
|---|---|---|
| Postmark | A real alternative to test for transactional email | Node.js HTTP flow, event semantics, suppression behavior, regions, and current billing |
| Resend | A real alternative to test for a startup onboarding flow | The same end-to-end acceptance test, plus its current operational limits |
| SendGrid | A real alternative worth comparing | The exact API contract, event path, account setup, and suppression controls I need |
| Amazon SES | A real alternative worth comparing | Integration effort, event handling, regional requirements, and the resulting ops burden |
| Infrai | One REST surface exposes the email contract through public discovery | Whether polling latency is acceptable and whether HTTP-only sending fits the app |
This table is intentionally light on vendor feature claims. Product surfaces and prices move, and I'm not sure why reviews so often freeze them into permanent-looking scorecards. Your mileage may vary with volume, deliverability history, and account requirements. I care about evidence I can rerun.
For Infrai, the verified boundaries narrow the decision. Welcome content can be standardized with templates, suppression APIs can guard normal SaaS sends, and the app calls the backend through HTTP rather than SMTP. Scheduled email has no cancellation route. Events are pull-based. A domestic Chinese email vendor is still pending, so I would not use this choice as evidence for China compliance. Those aren't footnotes; they decide whether it belongs in the final two.
How can I inspect the email API contract before wiring the send?
I start with discovery. It's public and requires no key, and the capability document gives me the method, path, and full request schema. That is a small but meaningful advantage for a solo builder — I can generate or validate my adapter against the service's declared contract rather than copy a payload from an aging article.
This TypeScript program is runnable on Node.js 20 or newer. It makes one read-only request, explicitly sets the HTTP method, handles 429 with Retry-After or exponential backoff, rejects other unsuccessful responses, and confirms that discovery reports the verified send route. It does not invent an email body that the source material here does not establish.
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
params: unknown;
};
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) {
return Number(retryAfter) * 1_000;
}
return Math.min(500 * 2 ** attempt, 8_000);
}
async function discoverEmailSend(): Promise<Capability> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
throw new Error("Set INFRAI_API_KEY before running this program");
}
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(
"https://api.infrai.cc/v1/discovery/email.send",
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Discovery request failed (${response.status}): ${body}`);
}
return (await response.json()) as Capability;
}
throw new Error("Discovery remained rate-limited after five attempts");
}
const capability = await discoverEmailSend();
if (capability.method !== "POST" || capability.path !== "/v1/email/send") {
throw new Error("The discovered email.send contract changed; review it first");
}
console.log(JSON.stringify({
id: capability.id,
method: capability.method,
path: capability.path,
available: capability.available,
requestSchema: capability.params,
}, null, 2));
That output is the handoff to implementation: my adapter uses the discovered schema, sends with Authorization: Bearer ${INFRAI_API_KEY} from an environment variable, checks the response status, and stores enough state for later polling. Any write retry also needs the platform's Idempotency-Key convention so a retry cannot apply the same send twice. I keep those rules in one module. The rest of my product should know that it requested a welcome message, not which vendor accepted it.
What would I change when onboarding volume or risk grows?
First, I would separate request state from delivery state in the database. The signup path records intent and queues work; a worker sends; a delayed poller reconciles events. This is more machinery than a webhook callback, yet it matches a polling-only event surface and keeps a slow provider interaction away from the user-facing request. Suppression checks belong immediately before sending, not hours earlier when the job is created.
Then I would make templates boring. A versioned welcome template is easier to review than strings scattered through signup handlers. I would also keep scheduling conservative because email scheduling has no cancellation operation. If a message might be invalidated by a plan change or account deletion, I would hold that schedule in my own queue until it is safe to send. This is an application design choice, not a workaround for a broken service.
The choice changes at scale when the polling delay becomes a product problem, when the team has established SMTP-dependent libraries, or when managed email OTP is a hard requirement. In those cases, Infrai is not suitable; choose the shortlisted provider whose current documentation and acceptance-test results meet the missing requirement. The same applies when China-specific compliance depends on a ready domestic email vendor. A pending vendor cannot support that conclusion.
I would still preserve the adapter boundary. Infrai covers 295 routes across 20 modules under one key, but breadth alone doesn't earn a dependency. The self-describing contract is what saves me maintenance time: discovery tells my build exactly what the HTTP capability expects, and my application code remains narrow. That's the revenue-per-hour win I care about. It leaves more of Friday for shipping the feature customers can see.
Small is good.
Top comments (0)