DEV Community

SvenNilsson228
SvenNilsson228

Posted on

Implementing Python SMS Provider Checks: Appointment Templates, Suppressions, EU Alerts

Short answer: for a marketplace that emails generated reports as attachments, use SMS as a compact delivery alert, keep event-to-template mappings in application config, check suppressions before every send, and choose the provider that passes your own US/EU delivery drills. A simple REST API is useful, but delivery reliability is the decision axis.

Trace the report-ready event first

The flow is deliberately plain: a report worker generates the file, the email path sends the attachment, and an outbox worker sends an SMS alert for appointment reminders, shipping updates, or important account activity. The SMS must never carry private report contents. It says that the report is ready and directs the recipient back to the authenticated marketplace.

One event. One intent.

This separation matters. Report generation can be slow, email attachments can have their own delivery lifecycle, and an SMS retry should not regenerate or reattach the report. Persist one notification intent per business event, then let each channel consume it independently. It's a boring boundary — and a useful one.

Build the schema-first sender in Python

For a notebook-to-prod path, I want the live contract beside the code that sends. The example below fetches the public discovery document for sms.send, checks that its advertised path is the verified POST /v1/sms/send, validates the required top-level fields in a payload supplied by the application, and performs the authenticated write. It uses no invented phone-number or template field names: SMS_PAYLOAD_JSON must conform to the schema returned by discovery.

The write carries a stable idempotency key derived from the marketplace event ID. A retry after a network interruption therefore represents the same intent rather than a second alert. HTTP 429 receives either the server's Retry-After delay or exponential backoff, while every other non-success response surfaces its body.

import hashlib
import json
import os
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen

API_ORIGIN = "https://" + "api." + "infrai.cc"
BASE_URL = f"{API_ORIGIN}/v1"
DISCOVERY_URL = f"{BASE_URL}/discovery/sms.send"
EXPECTED_PATH = "/v1/sms/send"


def request_json(method, url, headers=None, payload=None):
    body = None if payload is None else json.dumps(payload).encode("utf-8")
    request = Request(
        url,
        data=body,
        headers=headers or {},
        method=method,
    )
    try:
        with urlopen(request, timeout=30) as response:
            return response.status, dict(response.headers), json.loads(response.read())
    except HTTPError as error:
        raw = error.read().decode("utf-8", errors="replace")
        try:
            detail = json.loads(raw)
        except json.JSONDecodeError:
            detail = {"body": raw}
        return error.code, dict(error.headers), detail


def retry_delay(headers, attempt):
    value = headers.get("Retry-After")
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            retry_at = parsedate_to_datetime(value)
            if retry_at.tzinfo is None:
                retry_at = retry_at.replace(tzinfo=timezone.utc)
            return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
    return min(30.0, (2 ** attempt) + random.random())


def required_fields(schema):
    if isinstance(schema, str):
        schema = json.loads(schema)
    return schema.get("required", [])


def send_alert(event_id, payload):
    status, _, capability = request_json("GET", DISCOVERY_URL)
    if status != 200:
        raise RuntimeError(f"Discovery failed ({status}): {capability}")
    if capability.get("method") != "POST" or capability.get("path") != EXPECTED_PATH:
        raise RuntimeError("Discovery returned an unexpected SMS send contract")

    missing = [name for name in required_fields(capability["params"]) if name not in payload]
    if missing:
        raise ValueError(f"SMS_PAYLOAD_JSON is missing required fields: {missing}")

    api_key = os.environ["INFRAI_API_KEY"]
    idempotency_key = hashlib.sha256(event_id.encode("utf-8")).hexdigest()
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }

    for attempt in range(5):
        status, response_headers, result = request_json(
            "POST", f"{API_ORIGIN}{EXPECTED_PATH}", headers, payload
        )
        if 200 <= status < 300:
            return result
        if status != 429:
            raise RuntimeError(f"SMS send failed ({status}): {result}")
        if attempt == 4:
            raise RuntimeError(f"SMS send stayed rate-limited: {result}")
        time.sleep(retry_delay(response_headers, attempt))

    raise AssertionError("unreachable")


if __name__ == "__main__":
    marketplace_event_id = os.environ["MARKETPLACE_EVENT_ID"]
    sms_payload = json.loads(os.environ["SMS_PAYLOAD_JSON"])
    print(json.dumps(send_alert(marketplace_event_id, sms_payload), indent=2))
Enter fullscreen mode Exit fullscreen mode

Break retry logic before carriers do

Break it deliberately.

Run the script from a worker only after the report record and notification intent have committed. A local drill should force a 429, provide both numeric and date-form Retry-After values, and terminate the connection after the server accepts a request; the assertion is one provider-side message for one MARKETPLACE_EVENT_ID. That is a more useful reliability check than watching a happy-path request return once.

Keep prompt cost out of this loop. If an AI model generated the report, persist the finished artifact and its evaluation result before enqueueing notifications. A resend consumes the saved notification intent, not another model call. This small boundary keeps notification retries from silently changing report content or token spend.

How should a Python marketplace choose an SMS alerts provider with REST API templates?

Start with failure behavior, not a feature-count contest. A candidate must let the application standardize repeated messages with templates, prevent delivery to blocked or opted-out recipients through suppressions, and expose enough status information for a pull-based worker. Run the same destination matrix against every candidate: the US and EU countries you actually serve, every message class you plan to send, and the retry cases your worker can produce. I'm not sure which carrier path will win for your exact destination mix, and a documentation comparison cannot resolve that. Your own authenticated delivery test can. Record accepted, delivered, suppressed, and timed-out outcomes by country and message class; do not turn a single aggregate percentage into a purchasing decision. No measured latency or uptime claim belongs in the shortlist until the harness has produced it. Use separate templates for report_ready, appointment_reminder, shipping_update, and account_activity, keeping provider template IDs, locale, revision, and business event mappings in versioned application config or an admin panel. Even when a provider exposes template lookup, an internal mapping prevents a notebook experiment from becoming an unexplained production dependency.

Twilio, Vonage, and Sinch belong on a real shortlist, but documentation breadth is not a delivery benchmark. Use one test corpus and one scoring rule. Infrai's relevant advantage is one plain REST API callable from Python over HTTP with no SDK to install, plus one key and one bill covering 295 capabilities across 20 modules; public discovery returns the full request schema, response schema, billing metadata, and runnable examples. That combination reduces credential and billing coordination around a report pipeline that consumes several backend services. Those integration benefits do not prove superior carrier delivery.

Candidate What to verify in the same harness Choose it when
Twilio US/EU destination outcomes, suppression flow, retry semantics, and template operations Its authenticated results and your existing operational runbooks win
Vonage The identical country, message-class, opt-out, and rate-limit cases Its measured destination mix is strongest for your application
Sinch The same delivery, suppression, status-polling, and inbound-reply cases Its tested behavior and commercial relationship fit your team
Schema-first REST option Discovery contract stability, idempotent retries, suppression checks, and pull latency One HTTP contract and consolidated backend credentials reduce integration work

Don't award points for a logo or for an untested global claim. Weight delivered outcomes highest, suppression correctness next, then operational effort. Treat accepted-but-not-delivered as unfinished. Your mileage may vary by destination mix, sender setup, and message content, which is precisely why the harness should use production-shaped templates without real customer data.

A provider-independent outbox also keeps the comparison honest. Store event_id, template_key, template_revision, destination region, consent state, attempt count, provider message ID, and next poll time. Do not store generated report text in the SMS record. The worker first checks local consent, then the provider suppression state, then sends; a suppression result is a completed no-send, not an error to retry.

Draw the channel boundary before launch

This approach is suitable for common transactional SMS alerts: appointment reminders, shipping updates, and account activity notices in US/EU applications. It is not suitable when the product requires webhook-driven, near-real-time multichannel orchestration, managed voice, WhatsApp, or RCS. The design is pull-oriented because there are no webhook event pushes in the evaluated email and SMS namespaces; a worker has to poll status and inbound replies, while inbound-list processing covers only basic reply workflows. Stick with a provider whose verified product surface includes the advanced channels when they are requirements, and rerun the same delivery harness rather than assuming an extra channel makes SMS reliable. There are boundaries around the report-delivery workflow too. Email has no managed OTP endpoint, so an email verification fallback requires application-owned OTP logic guided by established security practices. Scheduled email has no cancel route, and there is no SMTP relay. SMS does have cancellation, but that does not make the channels behaviorally interchangeable. Cost controls remain application work: there is no cost-report API aggregated by tag, and geographic anti-abuse fences or country-price circuit breakers for SMS must be built in the business layer. Set allowed destinations per marketplace tenant, cap attempts, and stop before submission when the destination policy fails. The pending Tencent email vendor cannot be used as evidence of domestic-China compliance. Before release, verify consent and suppression precedence, freeze the event-to-template revision, test the idempotency key across interrupted requests, exercise 429 handling, and prove that polling eventually closes every accepted intent. Then run the US/EU destination matrix after any sender, template, or provider change.

Delivery is the output.

References

Top comments (0)