Short answer: for a B2B SaaS report attachment, investigate the evidence trail before resending an SMS event notification. Confirm the destination, sender registration, signature choice, consent decision, and downstream status first. Carrier filtering can make an immediate second attempt repeat the same failure, while a missing audit record makes a correct decision impossible to defend.
The email carries the report. SMS usually carries the time-sensitive pointer: the report is ready, an export failed, or an approval is waiting. Treat those as related notifications with separate lifecycles. An accepted send request is not delivery evidence.
Build the compliance record before the notification
The report job creates an artifact. The notification system tries to tell an authorized person about it. A single sent flag joins two different workflows and hides the details needed to investigate a complaint, prove consent, or explain why a resend was withheld.
Persist an event record that survives worker restarts. At minimum, connect the application event ID, report ID, destination country, normalized phone number, notification purpose, consent or lawful-basis decision, sender identity, sender-registration evidence, message ID, attempt number, and delivery deadline. Store an opaque report reference rather than the report body in the SMS log. The record should prove what happened without becoming a second copy of sensitive customer data.
Signature is an overloaded word in messaging. It may refer to a registered alphanumeric sender, a branded originator, a short-code or long-code setup, or a message-level signature required by a local program. Do not assume one sender identity is valid in every market. Save the resolved sender configuration and the rule that selected it.
This is where compliance evidence matters more than a throughput dashboard. A dashboard can show 10,000 accepted requests; an investigation needs to answer who was eligible, which sender was selected, what content class was sent, which market rule applied, and what downstream state was observed. Those are different measurements.
Keep the application-level taxonomy deliberately small:
-
queued: accepted at a downstream boundary, with no terminal delivery result yet. -
delivered: the downstream status says the message reached its destination. -
failed: a terminal failure that policy may classify as retryable or non-retryable. -
carrier-rejected: downstream evidence points to filtering or a carrier policy decision.
Keep the raw status and reason beside the normalized state. Four workflow states are enough for a decision; they are not a reason to discard diagnostic detail.
How should teams separate carrier filtering from SMS delivery failures?
Work from the outside in. Verify that the recipient and destination country are allowed for this notification class. Verify sender registration and the signature selected for that country. Inspect the original message ID and its current status. Only then evaluate a resend rule.
I don't treat a 2xx response as a delivery receipt. I treat 429 as a control-loop signal: slow down, honor Retry-After when supplied, and leave the message state unchanged. I'm not sure which carrier rule caused a rejection until status detail, carrier feedback, or an accountable delivery report provides evidence. Country alone isn't an explanation.
The sequence prevents a familiar mistake. An operator sees no SMS, clicks resend, and creates a second attempt with the same unverified sender. That can produce duplicates, obscure the original timeline, and make a later compliance review harder. A retry is a policy transition, not a button attached to an error screen.
The decision needs four explicit checks:
- Is the message still within its useful deadline?
- Is the observed outcome actually retryable?
- Has sender registration and destination configuration been validated for this market?
- Can the new attempt be linked to the same authorized application event?
If the third answer is no, fix configuration before resending. If the first answer is no, stop. An obsolete report-ready alert does not become responsible by arriving late.
Implement the bounded decision function
Polling and webhooks are delivery mechanisms, not business states. Whichever mechanism is available, persist each observation and make the next action deterministic. A polling worker should back off on rate limiting, stop polling terminal outcomes, and treat the event deadline as a hard upper bound.
This provider-neutral policy function leaves the status adapter outside the example. That adapter should map a documented service response into the four states above and preserve the raw reason.
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
class DeliveryState(str, Enum):
QUEUED = "queued"
DELIVERED = "delivered"
FAILED = "failed"
CARRIER_REJECTED = "carrier-rejected"
@dataclass(frozen=True)
class Notification:
event_id: str
message_id: str
state: DeliveryState
attempt: int
deadline: datetime
sender_verified: bool
destination_allowed: bool
retryable_failure: bool
def choose_action(notification: Notification, now: datetime) -> str:
if notification.state == DeliveryState.DELIVERED:
return "close"
if now >= notification.deadline:
return "stop-and-record-expired"
if not notification.sender_verified or not notification.destination_allowed:
return "hold-for-configuration-review"
if notification.state == DeliveryState.CARRIER_REJECTED:
return "hold-for-carrier-policy-review"
if notification.state == DeliveryState.FAILED and notification.retryable_failure:
return "resend-once-with-idempotency-key"
if notification.state == DeliveryState.QUEUED:
return "poll-after-backoff"
return "stop-and-record-nonretryable-failure"
def next_poll_time(now: datetime, attempt: int, retry_after: int | None) -> datetime:
seconds = retry_after if retry_after is not None else min(2 ** attempt, 60)
return now + timedelta(seconds=seconds)
The important branch is the explicit hold. A missing sender-registration record must not become an automatic resend. A rate-limited status lookup must not increment the SMS attempt count because no new message was created. Persist the observed status, selected rule, next poll time, and worker version that made the decision.
Idempotency belongs on the resend transition. A queue can deliver a job twice, a process can restart after a write, and an operator can submit the same action twice. Derive an idempotency key from the application event and intended retry number, then store the result of that write. The exact key format is an application choice; the invariant is that recovery cannot silently create another attempt.
Consider one edge case. The report is ready, the first SMS remains queued, the status request is rate-limited, and the report link expires soon. Launching a second SMS immediately risks a duplicate. Waiting forever risks an obsolete notification. Bound polling by the event deadline, then make an explicit expiry decision: cancel the original if the documented contract permits it, or record that it is no longer cancellable and suppress further sends. The email attachment still needs its own access and expiration policy. SMS status does not prove that the report was read.
Duplicates are failures too.
Test the sender and content paths by market
A staging test that receives an accepted response proves very little. Build a matrix around destination market, sender identity, traffic purpose, message length, link format, consent state, and expected downstream status. Run a controlled test for each sender configuration before enabling production traffic. Keep test recipients and report IDs separate from customer data.
For US traffic, retain evidence for the registered sender and the approved traffic category. For EU traffic, validate country-specific sender and consent requirements instead of treating “EU” as one carrier policy. Legal interpretation belongs with the responsible compliance team; engineering should still record the decision, source, timestamp, and rule version used.
The email half has a different failure surface. Verify that the generated report is attached from the expected object, that the recipient was authorized for this event, and that authentication and complaint signals are retained. Yahoo's sender guidance covers authentication, complaint rates, and unsubscribe handling for applicable mail. Those controls do not fix SMS carrier filtering, but they keep the email path from becoming an untracked fallback.
Do not call email an equivalent fallback until its security properties match the event. A report attachment can expose more data than an SMS body. Use a short-lived, access-controlled link or a protected attachment policy reviewed for the report's sensitivity. The notification log should identify the artifact version without storing its contents in a broad operational log.
Measure decisions, not just sends: accepted-to-delivered time, queued age, carrier-rejected rate by sender and destination, retry rate, duplicate rate, expired-event count, and the proportion of events with complete evidence. Alert on missing evidence as well as delivery decline. A perfect delivery rate with no sender or consent records is not a trustworthy system.
Roll out with evidence gates
Start with one notification class and one destination market. In observation mode, collect status transitions and validate the state mapper without automatic resends. Review the records with engineering and compliance owners. Look for missing sender decisions, ambiguous carrier reasons, duplicate event IDs, and report references that outlive their intended access window.
Then enable one retryable failure class, one retry maximum, and a deadline suited to the event. Add country allowlists, recipient throttles, and a circuit breaker for unusual volume before widening traffic. A resend must carry the original event ID and a new attempt number, and the audit record must connect both attempts.
The catch is ownership. This approach is not suitable when a team cannot maintain consent records, sender-registration evidence, status retention, or market-specific review. In that case, keep the workflow narrower, use an internal messaging service that already owns those controls, or make email the primary delivery path and SMS only a tightly scoped alert. Choose the system you can explain later.
Finally, sample completed events. Ask an operator to reconstruct the timeline from the event ID: report created, recipient authorized, sender selected, SMS accepted, status observed, resend decision made, and email artifact delivered or withheld. If that timeline requires searching five unrelated systems, the next feature is better evidence, not another retry button.
Top comments (0)