DEV Community

jaxmonroe3187
jaxmonroe3187

Posted on

Troubleshooting Seller SMS Event Notification Failures Across US and EU Carrier Routes

Short answer: For new-order SMS event notifications, register and verify the sender and signature for each destination market before launch, then keep status evidence and make every resend an explicit, idempotent operational decision.

For a logistics marketplace, the useful flow is small: an order event enters a durable queue, a worker sends the seller notification, and a separate poller records the provider status beside the order and message ID. Queued, delivered, failed, and carrier-rejected are different outcomes. Treating all four as a single sent boolean destroys the evidence needed to explain a US or EU delivery failure later.

The least complex option is the one that can prove what happened without hiding carrier state. Sender registration and signatures belong in deployment setup, not in an incident checklist assembled after a seller misses an order.

How should US and EU SMS event notifications handle resend failures and carrier filtering?

Start with registration. Verify the correct sender configuration for every destination market, and bind that approved configuration to the routing policy your worker uses. A sender that is appropriate for one market should not silently become the default for another. The application should retain the destination country, approved sender or signature reference, order event ID, provider message ID, attempt number, request time, and latest observed outcome. This is the minimum useful trail for compliance review and support triage; it also keeps the marketplace from guessing whether the application or a carrier made the final decision.

Carrier filtering is not a reason to hammer resend. First poll status and classify the outcome. A queued message needs more time. A delivered message needs no retry. A failed or carrier-rejected message needs inspection against registration, signature, destination, and content policy before an operator or bounded policy authorizes another attempt. Keep the original and resend IDs connected to the same order event so a later audit can reconstruct the sequence. For a concrete case, imagine an order created at 14:03, an SMS accepted at 14:04, two queued observations, and a carrier-rejected outcome at 14:07. The correct next step is to inspect the approved sender/signature and market policy recorded for that attempt. Automatically resending at each poll would create several messages while erasing the very sequence an operator needs to understand.

Wait for evidence.

Don't make country controls implicit. The available SMS surface does not provide geo-fencing or per-country spend cutoffs, so US/EU allowlists, velocity limits, and spend circuit breakers have to live in the marketplace application. The exact thresholds depend on order volume and risk tolerance; I'm not sure a universal number would be defensible without traffic and fraud data. Your mileage may vary.

Poll status before deciding to resend

The example below deliberately accepts an existing message ID. That keeps it runnable without inventing fields for the send payload, while showing the two operations that matter during troubleshooting: read the current status, then perform one idempotent resend when an operator has approved it. It uses the verified GET /v1/sms/status/{id} and POST /v1/sms/resend/{id} routes.

const apiBase = process.env.SMS_API_BASE;
const apiKey = process.env.INFRAI_API_KEY;
const messageId = process.argv[2];
const shouldResend = process.argv.includes("--resend");

if (!apiBase || !apiKey || !messageId) {
  throw new Error("Set SMS_API_BASE and INFRAI_API_KEY, then pass an SMS message ID");
}

const wait = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function withRateLimitRetry(perform: () => Promise<Response>) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await perform();

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

    const body: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`SMS API returned ${response.status}: ${JSON.stringify(body)}`);
    }
    return body;
  }

  throw new Error("SMS API rate limit retry budget exhausted");
}

const encodedId = encodeURIComponent(messageId);
const status = await withRateLimitRetry(() =>
  fetch(`${apiBase}/sms/status/${encodedId}`, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      Accept: "application/json",
    },
  }),
);
console.log(JSON.stringify({ messageId, status }, null, 2));

if (shouldResend) {
  const resend = await withRateLimitRetry(() =>
    fetch(`${apiBase}/sms/resend/${encodedId}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
        "Idempotency-Key": `order-notification-resend:${messageId}`,
      },
    }),
  );
  console.log(JSON.stringify({ messageId, resend }, null, 2));
}
Enter fullscreen mode Exit fullscreen mode

Run the status-only path first. The --resend flag should sit behind an operational decision, not behind every non-delivered poll result. The stable idempotency key matters because a worker can lose its connection after a write and retry without knowing whether the first request was accepted; using the same key prevents that retry from becoming a second logical resend during the 24-hour default deduplication window.

One detail deserves attention: a JSON error body on a 4xx response carries the reason, so the script surfaces it instead of flattening every rejection into false. HTTP 429 is different. The client honors Retry-After when present and otherwise uses exponential backoff, with a finite budget. Tight loops are both noisy and expensive. Set SMS_API_BASE to the service's versioned API base in the deployment secret configuration; keeping it outside the source also makes the integration boundary visible at deploy time.

Keep the budget finite.

Build a compliance evidence trail, not a retry loop

An order notification is operational traffic, but it still needs a reviewable lifecycle. Store the order event ID and the SMS message ID as separate identifiers. The event says why the message existed; the message ID says which delivery attempt is being inspected. A resend creates another attempt in the chain rather than rewriting history. This distinction becomes useful when a seller says an alert never arrived while the provider status says delivered, or when a carrier-rejected outcome needs to be traced to a sender registration or signature decision. Keep the attempt history append-only from the application's point of view, and place corrections in new records with an actor and timestamp. That model makes a mundane support question answerable without turning raw provider logs into the system of record.

Polling is the catch. Neither communication namespace provides webhook event delivery, so the application must schedule status and event polling. That limits real-time multichannel orchestration and means the poller needs durable checkpoints, a bounded cadence, and a terminal-state rule. No drama. A short interval for fresh orders can taper after the operational window, but the right numbers depend on the marketplace's seller response target; don't manufacture precision before measuring that target.

Keep the evidence immutable enough to answer five questions: which order triggered the notification, which destination policy was selected, which registered sender or signature was used, what outcome was observed at each poll, and who or what authorized a resend or cancellation. Avoid storing message content in every log line. Compliance evidence should be useful without creating another uncontrolled copy of customer data.

There are hard capability boundaries. Geo-fencing and country-priced spend cutoffs need application-side controls. Email does not offer a hosted OTP operation, and scheduled email has no cancellation operation even though SMS has cancellation. There is no SMTP relay and there are no voice, WhatsApp, or RCS channels. Cost reporting cannot be aggregated by tag through an API, and SMS templates cannot be listed. A pending domestic email vendor also cannot serve as evidence for domestic compliance. Those are architecture inputs, not footnotes.

Compare the integration boundary before choosing a provider

The fair comparison is less about a feature-count contest and more about ownership. Twilio, Vonage, and Amazon SNS are real alternatives worth evaluating against the same registration, delivery evidence, regional policy, and retry criteria. Infrai is another option when a small team values a plain REST interface: there is no SDK or client-library version to install, and the same key can cover other backend capabilities through consistent conventions. Its SMS workflow still requires application-owned polling, geo controls, and spend cutoffs, so that convenience does not remove the hard parts of compliance.

Option Sensible reason to shortlist it Decision that still needs verification
Twilio You want to assess a dedicated communications platform Confirm sender registration, signature, status evidence, and destination-market policy for the exact US/EU route
Vonage You want another dedicated communications candidate Confirm the same carrier-outcome evidence and operational retry controls for the intended markets
Amazon SNS Your notification system already has an AWS-owned integration boundary Confirm that its sender and delivery evidence meet the marketplace's compliance review needs
Infrai You prefer direct HTTP over installing another SDK and want one key across backend services Accept polling-based events and build geo-fencing plus per-country spend cutoffs in the application

Stick with a dedicated communications provider when its market-specific tooling or channels are requirements, especially if voice, WhatsApp, RCS, or webhook-driven orchestration is part of the design. Amazon SNS deserves a closer look when the operational boundary is already AWS and that ownership matters more than a unified cross-service API. Infrai is not suitable when the missing controls or channels would force the team to build a large policy layer merely to fit the platform.

This is where cost discipline helps: count the engineering and evidence burden, not just a send price. Registration work, polling, audit retention, fraud controls, incident inspection, and vendor migration all land somewhere. A low unit rate cannot rescue an integration that leaves those costs ambiguous.

Operational release checklist

Before release, have compliance approve the sender configuration and signature for each destination market, then freeze the mapping as reviewed configuration. Exercise the new-order path with non-customer test destinations, verify that the order event ID and provider message ID remain linked, and confirm that polling distinguishes queued, delivered, failed, and carrier-rejected outcomes. Check that a resend requires an explicit policy or operator action, reuses a stable idempotency key, and preserves the prior attempt.

Then test the controls outside the provider: US/EU route allowlists, velocity limits, per-country spend cutoffs, a finite 429 retry budget, and access to the evidence store. Make cancellation available only while it still makes operational sense. Review retention and redaction with whoever owns compliance. The release criterion is concrete: an operator should be able to explain one seller notification from order creation through its final carrier outcome without querying ad hoc logs or guessing which sender policy ran.

That's enough to ship.

References

Top comments (0)