Short answer: for a startup sending marketplace outage alerts, pick the workflow that makes consent, suppression, regional routing, and delivery evidence testable before a batch leaves your queue. Reliability belongs to the pipeline, not to an API label.
A marketplace may generate a daily seller report, email it as an attachment, and send SMS when the report job or checkout service is unavailable. Those channels share customer data, but they should not share delivery policy. Email carries the artifact; SMS carries a short signal and a link. That separation prevents a broken template from becoming a broad incident.
What should a startup test before an SMS outage alert batch?
Use four gates: identity, policy, payload, and evidence. Each needs a deterministic test and an audit record.
Identity records who owns a destination, why it is opted in, and when that consent was captured. A phone number copied from a seller spreadsheet is not consent. Policy puts suppression lists in data: an opt-out, carrier complaint, or quiet-hour rule must be checked just before enqueueing and again before the provider call. The list can change while a large batch drains.
Payload means a versioned template and escaped variables. Enforce a length limit in the target character set. GSM-7 and UCS-2 produce different segment counts; a message that looks short can be split by a carrier. The Twilio SMS character-limit reference documents those rules.
Evidence is an event trail containing request id, template version, destination region, provider response, and later delivery status. HTTP success is acceptance, not handset delivery. Keep explicit states such as pending, accepted, delivered, expired, and suppressed.
A runnable preflight for a marketplace report alert
This provider-neutral Python check is the decision boundary a Node.js service can call before its HTTP client. The order matters: render, classify, suppress, then enqueue.
from dataclasses import dataclass
from hashlib import sha256
@dataclass(frozen=True)
class Recipient:
phone: str
region: str
consent: bool
@dataclass(frozen=True)
class Alert:
template: str
variables: dict
incident_id: str
def preflight(recipient: Recipient, alert: Alert, suppressed: set[str]) -> dict:
if not recipient.consent:
return {"decision": "suppressed", "reason": "missing_consent"}
if recipient.phone in suppressed:
return {"decision": "suppressed", "reason": "suppression_list"}
if recipient.region not in {"US", "EU"}:
return {"decision": "held", "reason": "unknown_region"}
text = alert.template.format_map(alert.variables)
if len(text) > 320:
return {"decision": "held", "reason": "template_too_long"}
fingerprint = sha256(text.encode("utf-8")).hexdigest()[:12]
return {"decision": "enqueue", "region": recipient.region,
"incident_id": alert.incident_id, "fingerprint": fingerprint}
The 320-character check is a guardrail, not a universal carrier rule. Add a GSM-7/UCS-2 segment calculator to tests when message length affects operations. I'm not sure which encoding every downstream route will choose, so the rendered text and encoding decision belong in the audit record.
The email attachment should use a separate job: store the generated file, record its checksum, and pass a bounded download link to the email sender. The SMS event references a report id rather than embedding the file. An operator can then revoke a report link without editing an approved SMS template.
How do US and EU paths change the operating model?
Region is more than a phone prefix. It drives consent wording, sender identity, quiet hours, retention, and escalation. Keep a routing table with an effective date and test it with synthetic numbers. A missing route should fail closed instead of silently choosing a default country.
For batches, generate a manifest with intended count, regions, template version, and suppression snapshot hash. Compare that manifest with the outbox after each page. Imagine a 2,400-seller marketplace run: page one accepts 500 records, a suppression import arrives, and page two is about to start. The worker must load the new list, recompute the expected count, and hold any newly suppressed numbers; blindly replaying page one would create duplicates while silently skipping the policy change. If counts disagree, pause the next page and leave a reason in the run record. During an outage, pressing resend repeatedly is a tempting way to amplify noise.
Derive an idempotency key from incident id, recipient, and template version. Retries then target ambiguous requests only. Exponential backoff with a cap protects both worker pools; a dead-letter queue gives support a concrete inspection list.
Which delivery approach fits the failure you can operate?
| Approach | Strength | Main trade-off | Suitable when |
|---|---|---|---|
| Managed messaging API | Less carrier integration code | Regional policy and event retention follow its contract | Small team with limited telecom operations |
| Self-hosted gateway | More control over logs and routes | Team owns carrier relationships and throughput tuning | Dedicated operations capacity exists |
| Email-first with SMS fallback | Lower SMS volume | Fails when email is the incident component | Email path is independently monitored |
A single HTTP interface for email and SMS can reduce credential sprawl and simplify an eval harness. The catch is concentration risk: one account, quota, or regional policy change can affect both channels. A split-provider design reduces that dependency but increases reconciliation work. Stick with the simpler path when your team cannot operate the extra failure modes.
Do not make price the deciding metric. Count engineering hours, on-call load, audit requirements, and duplicate-alert cost. Fixtures should include an opted-out seller, an EU recipient during quiet hours, a UCS-2 message, a provider timeout, and a replayed incident id.
Run the preflight against a fixed fixture and assert that suppressed recipients never reach the HTTP client. Record the template fingerprint, not the full phone list, in normal logs; keep the mapping in restricted storage. Authenticate delivery callbacks and make state transitions idempotent.
Keep the release check boring.
During an incident, publish the batch manifest, watch acceptance and delivery separately, and stop on a policy or count mismatch. Afterwards, reconcile provider events with the outbox, expire report links, and add each new failure mode to the eval harness. The least complex system that passes those checks is a sensible starting point; revisit it as geography and obligations change.
Top comments (0)