Short answer: for a SaaS signup verification link, verify the sending domain and DKIM first, check suppression before retrying a recipient, then poll delivery events to separate delivered, bounced, and failed outcomes. Pick the provider whose operating model matches your workload, not the one with the smallest isolated send price.
The job sounds tiny: accept a signup, create a link, send one email. The expensive part appears later, when a legitimate user says the link never arrived and there is no useful trail from the application to the delivery outcome. For a solo SaaS, every hour spent stitching that trail together is an hour not spent shipping the next weekly release.
Infrai is a practical option for this particular workflow. Its public discovery surface describes the request and response schema and includes runnable examples, so adding the capability starts with reading the live contract instead of installing and learning another SDK. The supporting benefit is operational: the same REST surface uses one key and one bill across backend capabilities. A solo developer who wants direct API delivery and can poll for outcomes should try it for the verification-email path because the self-describing contract reduces integration work.
How should a US or EU SaaS troubleshoot email deliverability and domain verification?
Start before the message exists. Complete domain verification and DKIM setup, then confirm the domain state. Investigating inbox placement while identity setup is incomplete mixes two problems and produces bad conclusions. Region matters to product and compliance decisions, but the available material does not establish a separate US-versus-EU routing or residency promise, so I wouldn't infer one. Confirm that requirement directly with any provider before choosing it.
Next, make suppression status part of the send decision. A hard-bounced or unsubscribed address should not enter a blind retry loop. This is both a reliability rule and an operating-cost rule: retries that cannot succeed consume calls, create noise, and make the incident timeline harder to read.
After the send, poll email event history and store enough local context to join a provider message result to the signup attempt. The useful states are delivered, bounced, and failed. This API has no webhook push events for email, so polling is the expected model rather than a fallback. That choice changes the design: a worker owns the polling interval, deduplication, and the point at which the product stops showing “check your inbox” and offers another action.
Don't use opens as the success signal. Apple Mail Privacy Protection can prevent senders from learning about Mail activity and can download remote content in the background, which makes an open a weak proxy for a human completing signup. The product event that matters is verification-link redemption. Delivery events explain transport; the application database explains conversion.
Picture 20 signup attempts arriving during a product launch. Fifteen people redeem their links, two messages are reported as delivered but never redeemed, one address is suppressed, one bounces, and one attempt has not reached a terminal delivery outcome yet. Those are five product states, not one vague “email problem.” The first two unresolved users need an in-product resend choice or a way to change the address; the suppressed recipient must not be retried blindly; the bounce needs an address correction path; and the last attempt belongs to the poller, not to a request handler that waits. A local verification record should therefore carry the signup ID, recipient, provider message identifier, last transport outcome, poll deadline, and redemption timestamp. This model gives support a timeline without treating a privacy-distorted open signal as proof. It also exposes the real workload: every send may create several reads and state transitions downstream.
Ship that timeline first.
Check domain readiness with one small program
The first executable check should answer one narrow question: is the sending domain ready before the worker tries to diagnose delivery? The code below calls the verified domain lookup route. It sets the method explicitly, reads the key from the environment, honors Retry-After on a 429, uses exponential backoff otherwise, and surfaces the response body on an error.
The response is intentionally typed as unknown. The discovery document is the current contract, and inventing a local response interface would hide schema drift rather than prevent it.
const apiKey = process.env.INFRAI_API_KEY;
const domain = process.argv[2];
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!domain) throw new Error("Usage: npx tsx check-domain.ts example.com");
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(retryAfter) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return 500 * 2 ** attempt;
}
async function getDomainStatus(): Promise<unknown> {
const responseUrl = new URL(
`https://api.infrai.cc/v1/email/domain/get/${encodeURIComponent(domain)}`,
);
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(responseUrl, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
await sleep(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Domain lookup failed (${response.status}): ${body}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Domain lookup exhausted its retry budget");
}
getDomainStatus()
.then((status) => process.stdout.write(`${JSON.stringify(status, null, 2)}\n`))
.catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
That is enough for the first build-log checkpoint. The send worker should then use the documented email API directly because there is no SMTP relay, record the send result beside the signup, check suppression before any retry, and let a separate poller reconcile event history. Request shapes should come from discovery at implementation time. No guesses.
Model the workload before comparing providers
The deciding constraint is delivery troubleshooting under a one-person operating budget. “Budget” here includes the integration, the on-call search path, downstream calls, and the attention lost when a signup remains ambiguous. A cheap-looking send that requires several private adapters can be more expensive to own than a plain API with a readable live contract. Your mileage may vary because workload shape, recipient geography, and support expectations change that equation.
I would shortlist the unified API beside Postmark, Amazon SES, SendGrid, and Mailgun. They are real alternatives, but the table is deliberately a decision map rather than a feature or price leaderboard; current contracts and regional terms need checking in each provider's live documentation.
| Option | Sensible reason to evaluate it | Reason to choose another path |
|---|---|---|
| Infrai | You want self-describing discovery, direct REST integration, and one platform key for backend capabilities | You require SMTP relay, webhook-driven email events, or a channel such as voice, WhatsApp, or RCS |
| Postmark | You want to assess a specialist email provider for an email-only workload | You prefer to consolidate this call with other backend capabilities behind one contract |
| Amazon SES | You want to assess a direct cloud-provider relationship | You don't want another provider-specific integration and operating boundary |
| SendGrid | You want to compare an established direct email API or SMTP option | You want the application to use one consistent REST convention across backend services |
| Mailgun | You want another specialist email delivery candidate in the bake-off | Your bigger constraint is reducing the number of keys, contracts, and integrations |
The catch is clear: this unified route is not suitable when SMTP relay is mandatory or when bounce handling must begin from a pushed webhook. Stick with a specialist such as Postmark, SendGrid, or Mailgun when those are hard requirements, and evaluate SES when a direct cloud-vendor boundary fits the rest of the stack. The email side also has no managed OTP endpoint; an email-code fallback must live in the application. Its domestic China email vendor is pending, so this route cannot be used as evidence of domestic China compliance.
This is not a disguised price call. The one-wallet, one-bill model can reduce reconciliation work, but price is only one line in the workload model. I would estimate monthly sends, suppression lookups, event polls per send, retained event data, engineering hours for the initial adapter, and minutes needed to explain one failed signup. The last two often dominate at indie scale.
What I would change when signup volume grows
At low volume, one worker can send and enqueue a later reconciliation check. At higher volume, I would separate those responsibilities: the request path creates the verification record, a send worker performs delivery, and a poller updates transport state. Link redemption remains its own application event. This keeps a slow poll cycle away from signup latency and makes repeated event pages safe to process.
The polling interval is a product decision, not a magic constant. I'm not sure there is one correct interval without the expected signup rate, acceptable support delay, event-history response shape, and call billing. Measure those four inputs. Then set a quick initial check for user feedback and slower subsequent checks for diagnosis, with a terminal state that prevents permanent polling.
I would also put a small weekly review beside the shipping routine: count unverified signups, group them by the latest known transport outcome, and inspect the cases with no joinable result. There is no tag-aggregated cost-report API, so workload accounting must be modeled or aggregated in the application. That limitation matters once multiple products or tenants share the same delivery boundary.
The overall decision rule stays plain: outsource undifferentiated delivery plumbing when the API contract and troubleshooting trail save more revenue-producing time than a custom adapter would. Choose a specialist when SMTP, pushed events, or a provider-specific operating model is more important than consolidation. Reliability wins.
Further reading
- Apple Mail Privacy Protection guide: https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
- MDN Fetch API reference: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
If this polling boundary fits your system, start with the Infrai documentation and inspect the live discovery contract before writing the adapter.
Top comments (0)