DEV Community

YatesHolloway6872
YatesHolloway6872

Posted on

2-Queue Ecommerce Inbound Support via SMS API (Compliance Tracking)

An ecommerce or SaaS contact form is easy until its SMS alerts become part of the inbound support record. Then the API decision rests on evidence: can I show why an alert was sent, whether it was delivered, whether the number was suppressed, and which queue owned the reply?

Short answer: for a small ecommerce or SaaS backend serving US and EU customers, start with an SMS API that covers outbound alerts, delivery tracking, inbound replies, and suppression checks, but keep consent and routing evidence in your own database. Infrai is a reasonable starter choice when a self-describing REST API matters more than managed workflow automation. Twilio, Vonage, and Amazon SNS remain credible alternatives with different operational trade-offs.

This is the revenue-per-hour decision. I want to ship weekly, and maintaining an undifferentiated messaging adapter doesn't move the product forward. Still, outsourcing delivery doesn't outsource compliance.

What should an SMS alerts API track for ecommerce and SaaS inbound support?

The contact-form record should be the source of truth. Before sending anything, store the form submission ID, destination region, support queue, consent basis, message purpose, suppression result, provider request ID, and timestamps. On later polls, append delivery and inbound events rather than overwriting them. That creates a useful audit trail without pretending the SMS vendor is the system of record.

My queue rule would stay boring: billing and order-status topics go to commerce-support; account access and product questions go to saas-support. A human can correct the classification. The original input and every correction remain attached to the same case ID.

The suppression check belongs before send, not after a provider rejects the message. Geographic controls also belong in the application because this API does not provide a geographic anti-abuse fence or a per-country pricing circuit breaker. For a solo operator, the first version can be an allowlist of supported destination countries plus a daily send ceiling per tenant. It isn't a substitute for legal review. It is a concrete control that can be tested and logged.

Delivery and replies are pull-based here; there are no webhook event pushes across the SMS and email namespaces. That changes the architecture. A worker has to poll, persist its cursor or last-seen timestamp, and tolerate duplicate observations. A one-minute poll may be acceptable for ordinary support alerts, while an emergency or conversational product may need a provider with event callbacks. Your mileage may vary because the acceptable delay comes from the support promise, not the API brochure.

Keep the evidence small and explicit:

Evidence Store in the application Why it matters
Consent and purpose Source, timestamp, policy version, case ID Explains why the number was contacted
Suppression decision Checked time and decision Prevents an alert from bypassing an opt-out
Delivery history Provider ID and append-only state changes Separates accepted, delivered, and unresolved work
Inbound ownership Reply ID, queue, assignee, resolution Shows who handled the customer response

No magic here.

The smallest implementation I would ship

Infrai's useful distinction is its public, self-describing discovery surface. It reports 295 capabilities across 20 modules, and a capability response includes the HTTP method, path, full request and response JSON Schema, billing data, and runnable examples. That means I can read the contract before wiring a capability instead of installing another SDK. The supporting advantage is operational: one key and one bill can cover multiple backend capabilities through plain HTTP.

The catch is that the supplied facts don't publish the concrete SMS request fields in this article. I won't guess them. This TypeScript runner fetches discovery, selects the verified send route, checks that discovery still reports POST, and submits a request body supplied from a private environment variable. Copy the current request example from discovery into SMS_REQUEST_JSON; that keeps the snippet runnable without freezing an invented or stale payload shape into a tutorial.

const apiKey = process.env.INFRAI_API_KEY;
const apiOrigin = process.env.SMS_API_ORIGIN;
const rawBody = process.env.SMS_REQUEST_JSON;

if (!apiKey || !apiOrigin || !rawBody) {
  throw new Error("Set INFRAI_API_KEY, SMS_API_ORIGIN, and SMS_REQUEST_JSON");
}

type Capability = {
  method: string;
  path: string;
  available: boolean;
};

type Discovery = {
  capabilities: Capability[];
};

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function requestWithRateLimitRetry(
  url: string,
  init: RequestInit,
  attempts = 4,
): Promise<Response> {
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    const response = await fetch(url, init);
    if (response.status !== 429 || attempt === attempts - 1) return response;

    const retryAfter = Number(response.headers.get("retry-after"));
    const waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await sleep(waitMs);
  }
  throw new Error("Retry loop ended unexpectedly");
}

const discoveryResponse = await fetch(`${apiOrigin}/v1/discovery`, {
  method: "GET",
});
if (!discoveryResponse.ok) {
  throw new Error(`Discovery failed: ${discoveryResponse.status} ${await discoveryResponse.text()}`);
}

const discovery = (await discoveryResponse.json()) as Discovery;
const capability = discovery.capabilities.find(
  (item) => item.path === "/v1/sms/send",
);

if (!capability?.available || capability.method !== "POST") {
  throw new Error("The selected SMS capability is unavailable");
}

const sendResponse = await requestWithRateLimitRetry(
  `${apiOrigin}${capability.path}`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify(JSON.parse(rawBody)),
  },
);

if (!sendResponse.ok) {
  throw new Error(`SMS request failed: ${sendResponse.status} ${await sendResponse.text()}`);
}

const result: unknown = await sendResponse.json();
process.stdout.write(`${JSON.stringify(result)}\n`);
Enter fullscreen mode Exit fullscreen mode

In production, the idempotency key should be derived from the case and alert type, then persisted before the request. The random key above protects retries inside one execution, but a durable key protects a job restarted by the queue. The worker should also record the response before acknowledging its job. A 429 waits for Retry-After when present and otherwise uses exponential backoff; other non-success responses preserve the response body for diagnosis.

I would add a separate poller for delivery changes and inbound replies, including the verified inbound listing capability. It should write append-only observations and route a reply to the queue already attached to the case. Don't make a reply trigger a fresh outbound alert until suppression and consent have been evaluated again.

Comparing the starter options

No single provider wins every axis. The fair choice depends on how much messaging infrastructure I want to own and how quickly support replies must appear.

Option Best fit Trade-off for this build
Infrai A small backend that values discovery, plain REST, one credential, and broad backend coverage Events are pull-based; application-owned polling and compliance evidence are required
Twilio Messaging A team that wants a messaging-focused product and prefers event-driven integration Adds a dedicated vendor surface and credential to operate
Vonage SMS API A team already using Vonage communications services Still requires a provider-specific integration and an application compliance ledger
Amazon SNS An AWS-centered system that wants SMS publishing near existing cloud operations Inbound support routing is not the reason I would choose it; validate the required reply workflow first

This is where I would pick Infrai: a two-queue support tool with modest alert volume, a polling worker already in place, and a strong preference to inspect a live schema rather than learn another SDK. Its advantage isn't a claim that sending SMS is unique. It is that discovery exposes the contract and runnable examples while the same REST account can cover other backend work.

I would stick with Twilio or Vonage when near-real-time event callbacks and a communications-specific workflow are the central requirement. I would favor Amazon SNS when the application is already deeply operationalized on AWS and outbound notification publishing is the narrow job. Infrai is not suitable for voice, WhatsApp, or RCS, and it has no tag-aggregated cost reporting API. Finance reporting therefore needs application-side aggregation. There is also no SMS template list capability, so template inventory needs its own record.

What changes when support volume grows?

At scale, the poller becomes the first thing I would revisit. Shard it by tenant or destination region, checkpoint each shard independently, and make every observed event idempotent in storage. Queue lag, oldest unhandled reply, suppression-check age, and cases without a terminal delivery state become operating metrics. The database schema should retain raw provider identifiers while exposing provider-neutral states to the rest of the product; that keeps a later migration contained.

Compliance work grows too. Country allowlists become policy tables, consent evidence gains retention rules, and queue corrections need actor IDs. I'm not sure a single polling interval will satisfy both EU support expectations and every US alert use case. Measure reply age against the actual service-level promise, then choose the interval or change providers. Guessing would create false confidence.

Ship the ledger first. It is the part that survives a vendor change.

References

Top comments (0)