Skip to content

sms

Utils to send SMS messages via the Mailgun→ClickSend email-to-SMS gateway.

Note

SMS sending depends on there being credits at ClickSend. If there are no credits, no SMS will be sent.

send_sms(phone_numbers, message, sender='alerts@axponordic.com', api_key=None)

Send an SMS to one or more phone numbers via the Mailgun→ClickSend email-to-SMS gateway.

Each phone number receives the message as a separate request to ensure independent delivery.

Parameters:

Name Type Description Default
phone_numbers List[str]

Phone numbers in E.164 format (e.g. "+46701234567").

required
message str

The SMS message body. Keep concise (SMS limit ~160 chars per segment).

required
sender str

The sender email address. Must end with @axponordic.com. Defaults to "alerts@axponordic.com".

'alerts@axponordic.com'
api_key str | None

Optional Mailgun API key. If not provided, fetched from Azure Key Vault.

None

Returns:

Type Description
List[Response]

List[requests.Response]: A list of Mailgun API responses, one per recipient.

Raises:

Type Description
ValueError

If phone numbers are invalid or sender is not from @axponordic.com.

HTTPError

If the Mailgun API returns an error status.

Example
from physical_operations_utils.notification_utils import send_sms

send_sms(
    phone_numbers=["+46701234567"],
    message="Task completed successfully.",
)
Source code in physical_operations_utils/notification_utils/sms.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def send_sms(
    phone_numbers: List[str],
    message: str,
    sender: str = "alerts@axponordic.com",
    api_key: str | None = None,
) -> List[requests.Response]:
    """
    Send an SMS to one or more phone numbers via the Mailgun→ClickSend email-to-SMS gateway.

    Each phone number receives the message as a separate request to ensure independent delivery.

    Args:
        phone_numbers (List[str]): Phone numbers in E.164 format (e.g. "+46701234567").
        message (str): The SMS message body. Keep concise (SMS limit ~160 chars per segment).
        sender (str): The sender email address. Must end with @axponordic.com.
            Defaults to "alerts@axponordic.com".
        api_key (str | None): Optional Mailgun API key. If not provided, fetched from Azure Key Vault.

    Returns:
        List[requests.Response]: A list of Mailgun API responses, one per recipient.

    Raises:
        ValueError: If phone numbers are invalid or sender is not from @axponordic.com.
        requests.HTTPError: If the Mailgun API returns an error status.

    Example:
        ```python
        from physical_operations_utils.notification_utils import send_sms

        send_sms(
            phone_numbers=["+46701234567"],
            message="Task completed successfully.",
        )
        ```
    """
    if api_key is None:
        api_key = get_secret("Mailgun-APIKey")

    if not sender.endswith("@axponordic.com"):
        raise ValueError(
            f"Sender email address must end with '@axponordic.com'. Got: '{sender}'"
        )

    validated_numbers = _validate_phone_numbers(phone_numbers)

    if os.getenv("ENVIRONMENT") != "prod":
        for number in validated_numbers:
            if number not in NONPROD_WHITELISTED_NUMBERS:
                raise ValueError(
                    f"Environment is {os.getenv('ENVIRONMENT')}. "
                    f"Phone number {number} is not whitelisted for non-prod SMS. "
                    f"Whitelisted: {NONPROD_WHITELISTED_NUMBERS}"
                )

    responses = []
    for number in validated_numbers:
        recipient = f"{number}@{CLICKSEND_SMS_GATEWAY}"
        response = requests.post(
            f"https://api.eu.mailgun.net/v3/{DOMAIN}/messages",
            auth=("api", api_key),
            data={
                "from": sender,
                "to": [recipient],
                "subject": "",
                "cc": "",
                "html": message,
            },
            timeout=(10, 30),
        )
        response.raise_for_status()
        responses.append(response)

    logging.info("SMS sent successfully to %s", validated_numbers)
    return responses