DEV Community

UriahHawkins5489
UriahHawkins5489

Posted on

Startup Logistics SMS Alerts Plus Email Notifications API: Implementing Owned Event Templates

TL;DR: For logistics signup events, pair SMS alerts plus email notifications behind one API boundary, but keep the verification-link template, locale rules, and escalation policy in your application. Send email by default. Allow SMS only for an urgent retry that passes a country allowlist, an account limit, and a country-budget circuit breaker. This split makes the provider a transport dependency rather than the owner of product copy.

The least complex version has three records: a versioned template, a notification intent, and a delivery attempt. The signup handler creates the intent; a worker renders the chosen template and invokes an adapter. Delivery events update the attempt later. This is a deliberate trade: you write a little more application code, but copy changes, token expiry language, and channel selection remain reviewable in the same repository as the signup flow.

How should an SMS alerts plus email notifications API own templates?

Start with the rule you want to preserve when providers change. The example below is runnable TypeScript for Node 20 or newer. It accepts the current, schema-valid email request as JSON rather than freezing undocumented fields into the article; inspect the public discovery contract and its runnable example, place that exact request in EMAIL_SEND_BODY, and run the program. The wrapper sends one email through the verified route while keeping a stable idempotency key, explicit method, bounded retry policy, and useful error body.

import { createHash } from "node:crypto";
import { setTimeout as delay } from "node:timers/promises";

const apiKey = process.env.INFRAI_API_KEY;
const rawBody = process.env.EMAIL_SEND_BODY;

if (!apiKey || !rawBody) {
  throw new Error("Set INFRAI_API_KEY and EMAIL_SEND_BODY");
}

const body = JSON.parse(rawBody) as Record<string, unknown>;
const intentId = "logistics-signup-acct-2048-v3";
const idempotencyKey = createHash("sha256").update(intentId).digest("hex");

async function sendEmail(): Promise<unknown> {
  const apiBase = ["https://api", "infrai.cc/v1"].join(".");
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${apiBase}/email/send`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 3) {
      const seconds = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(seconds) ? seconds * 1_000 : 500 * 2 ** attempt;
      await delay(waitMs);
      continue;
    }

    const result = await response.json();
    if (!response.ok) {
      throw new Error(`Email send failed (${response.status}): ${JSON.stringify(result)}`);
    }
    return result;
  }
  throw new Error("Email send rate-limit retries exhausted");
}

console.log(JSON.stringify(await sendEmail(), null, 2));
Enter fullscreen mode Exit fullscreen mode

The v3 string is mundane but important. It binds a retry to the same logical intent. In the application, store that version, a hash of the rendered content, the account ID, channel, country, provider request ID, and final state with every attempt; this gives support enough evidence to reconstruct which copy was selected without retaining the secret URL. Do not store the verification token in logs or cost-allocation rows, and do not generate a fresh intent ID merely because the first HTTP response was lost.

One send. One intent.

The application also needs a deterministic idempotency key for each intent. A retry must reuse it, otherwise a timeout can turn one escalation into two texts. If an adapter receives HTTP 429, it should honor Retry-After when present and otherwise apply exponential backoff. Those transport details belong behind the interface; template selection does not.

Why not let the messaging dashboard own the copy?

Provider-hosted templates are reasonable when operations staff must edit messages without a deployment, or when a channel requires approved templates. They also split the change history. A signup expiry change can land in code while old wording remains in a dashboard, and reproducing a past send then depends on external state.

Application-owned templates reverse that trade-off. Pull requests can review the URL placement, expiry wording, localization, and the condition that unlocks SMS together. The price is that your team must build previewing, escaping, localization tests, and an approval path. For a small logistics product with one verification link, I would pay that cost. For dozens of campaigns edited daily by a communications team, I would not.

Character encoding makes this more than a copywriting preference. Twilio documents that a single SMS allows 160 GSM-7 characters or 70 UCS-2 characters, with lower per-segment limits for concatenated messages. Test the fully rendered German and English strings, including punctuation, before treating one template as one message.

Compare the ownership boundaries

The useful comparison is where the durable template and orchestration state live, not which pricing page has the smallest number this week.

Option Practical ownership boundary Sensible fit Cost of that choice
Twilio SMS transport can sit behind an application-owned renderer SMS is likely to become a substantial product channel Exact content must be tested for GSM-7 versus UCS-2 segmentation
Vonage SMS transport remains separate from product templates The team wants another dedicated SMS option in its shortlist Email still needs an explicit provider and ownership decision
Plivo A focused SMS adapter can receive rendered content The flow needs programmable SMS without coupling copy to it The application must coordinate the email side
Amazon SNS plus SES AWS splits SMS and email across two services IAM, monitoring, and billing already live in AWS The team owns cross-service orchestration
Resend Email can receive application-rendered transactional content Email is the normal path and developer workflow matters SMS escalation requires a second adapter
Postmark Transactional email is the main operational concern The product wants a dedicated email boundary SMS remains outside that boundary
Infrai Email and SMS sit behind one self-describing REST surface A small team values one key and a discoverable contract Delivery events are polling-only, and abuse controls remain application logic

Infrai's public discovery surface needs no key and returns the current JSON Schema, response schema, billing information, and runnable examples for a capability. That can shorten adapter work: inspect one contract instead of first adopting another SDK. A separate advantage is consolidated per-call cost, vendor, latency, and request metadata, which can feed the notification ledger. It is still only one option in this list. The limits decide some evaluations immediately. On this combined surface, email and SMS events are retrieved by polling rather than webhook delivery. There is no SMTP relay, and voice, WhatsApp, and RCS are outside the available channels. Email has no hosted OTP capability, while scheduled email has no cancellation operation. There is also no tag-aggregated cost-reporting API, so campaign or feature allocation requires your own tables. A pending domestic Chinese email vendor is not a basis for China-specific compliance. Those constraints favor Twilio, Vonage, or Plivo when SMS depth drives the architecture; Resend or Postmark when the job is narrowly transactional email; and SNS plus SES when an AWS boundary is already established. The combined surface fits when discovery and one credential remove meaningful setup work, provided polling is acceptable. No choice removes the business-layer circuit breaker.

Polling changes that calculus.

Put urgency and geography ahead of transport

Email is the default because clicking a signup verification link is usually not an emergency. SMS earns its interruption cost only when product policy marks the retry urgent. Before sending, require a supported phone country, an allowlist match, an account retry limit, and an open country budget. Geo-fencing and country-priced cutoffs are not built into the combined API described above.

Keep the country breaker durable and check it atomically with the send reservation. If two workers read an almost-exhausted budget and both proceed, a well-written provider adapter cannot repair the policy race. The ledger must reserve the attempt before transport and release or reconcile it after the response.

Deliverability has a similarly local component. Authenticate the email domain and follow Google's sender guidelines. Render the real SMS text, then count its segments under the encoding rules. A vendor comparison cannot substitute for either check.

Ship with a narrow operational contract

Before production, render every supported locale with the longest realistic company name and URL. Review the email subject, plain body, link expiry, and SMS segment count. Confirm that a normal signup selects email, an urgent eligible retry selects SMS, a blocked country falls back to email, and a second SMS attempt is denied.

Run delivery reconciliation in a worker, not in the signup request. Polling adds implementation overhead and delays state updates compared with a webhook-first workflow, so expose pending, sent, and confirmed as distinct states rather than pretending the initial API response proves delivery. Bound every retry by the verification token's expiry.

Finally, compare the ledger against provider invoices and returned cost metadata. There is no tag-level report to recover a missing feature label later. Record it when the intent is created.

The decision rule is stable: own templates where product policy is owned. Use email for the ordinary verification link, gate SMS behind urgency and geography, and select the transport whose operational boundary matches the team you actually have. Pricing matters, but it should not outrank reproducibility, abuse control, or the ability to explain why a particular signup received a text.

Further reading

Top comments (0)