python-termii 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ """
2
+ RequestResponse class for handling API responses.
3
+ Classes:
4
+ RequestResponse
5
+ Methods:
6
+ handle_response: Processes an HTTP response and returns a RequestResponse instance.
7
+ """
8
+ from hmac import compare_digest
9
+
10
+
11
+ class RequestResponse:
12
+ """
13
+ Represents a standardized API response.
14
+
15
+ Attributes:
16
+ status_code (int): The HTTP status code of the response.
17
+ status (str): A string indicating the status of the response (e.g., "ok" or "error").
18
+ message (str | dict): The response message, which can be a string or a dictionary.
19
+ """
20
+
21
+ def __init__(self, status_code: int, status, message: str | dict):
22
+ """
23
+ Initializes a RequestResponse instance.
24
+
25
+ Args:
26
+ status_code (int): The HTTP status code of the response.
27
+ status (str): A string indicating the status of the response.
28
+ message (str | dict): The response message.
29
+ """
30
+ self.status_code = status_code
31
+ self.status = status
32
+ self.message = message
33
+
34
+ @staticmethod
35
+ def handle_response(response):
36
+ """
37
+ Processes an HTTP response and returns a RequestResponse instance.
38
+
39
+ Args:
40
+ response: The HTTP response object.
41
+
42
+ Returns:
43
+ RequestResponse: An instance representing the API response.
44
+
45
+ Notes:
46
+ This method checks the HTTP status code of the response and returns a RequestResponse instance with a
47
+ standardized status and message.
48
+ """
49
+
50
+ if compare_digest(str(response.status_code), "200") or compare_digest(str(response.status_code), "201"):
51
+ return RequestResponse(
52
+ status_code=response.status_code,
53
+ status="ok",
54
+ message=response.text
55
+ )
56
+
57
+ if not compare_digest(str(response.status_code), "200"):
58
+ return RequestResponse(
59
+ status_code=response.status_code,
60
+ status="error",
61
+ message=response.text
62
+ )
63
+
64
+ return None
@@ -0,0 +1,14 @@
1
+ """
2
+ This module contains the service classes for interacting with the Termii API. Each service class corresponds to a
3
+ specific aspect of the Termii platform, such as managing campaigns, contacts, messages, numbers, phonebooks, sender IDs,
4
+ and templates. These classes provide methods to perform various operations related to their respective domains,
5
+ allowing users to easily integrate Termii's functionality into their applications.
6
+ """
7
+
8
+ from .campaign import CampaignService
9
+ from .contact import ContactService
10
+ from .message import MessageService
11
+ from .number import NumberService
12
+ from .phonebook import PhonebookService
13
+ from .sender_id import SenderIDService
14
+ from .template import TemplateService
@@ -0,0 +1,175 @@
1
+ """
2
+ This module provides the CampaignService class, which allows you to send SMS campaigns, fetch campaign details, and
3
+ retry failed campaigns using the Termii API. The service includes methods to send campaigns with various options such
4
+ as scheduling, link tracking, and channel selection. It also provides functionality to retrieve campaign history and
5
+ retry campaigns that may have failed.
6
+
7
+ References:
8
+ - https://developer.termii.com/campaign
9
+
10
+ Classes:
11
+ CampaignService: A service class that provides methods to manage SMS campaigns via Termii's API
12
+ """
13
+ from termii_py.http import RequestHandler
14
+
15
+
16
+ class CampaignService:
17
+ """
18
+ Provides methods to manage SMS campaigns through Termii's API. The class includes methods to send SMS campaigns
19
+ with various options, fetch campaign details, and retry failed campaigns. Each method constructs the appropriate
20
+ API request and handles the response.
21
+
22
+ Attributes:
23
+ http (object): An HTTP client instance responsible for making API requests to Termii.
24
+ """
25
+
26
+ def __init__(self, http: RequestHandler):
27
+ """
28
+ Initializes the CampaignService instance. This constructor takes an HTTP client instance as an argument, which
29
+ is used to perform network requests to the Termii API.
30
+
31
+ Args:
32
+ http (object): An HTTP client instance (e.g., `requests.Session`) used to perform network requests to the Termii API.
33
+ """
34
+
35
+ self.http = http
36
+
37
+ def send_campaign(self, country_code: str, sender_id: str, message: str, message_type: str, phonebook_id: str,
38
+ enable_link_tracking: bool, campaign_type: str, schedule_sms_status: str,
39
+ schedule_time: str = None, channel: str = "dnd"):
40
+ """
41
+ Sends an SMS campaign using the Termii API. This method constructs a payload with the provided parameters
42
+ and sends a POST request to the `/api/sms/campaigns/send` endpoint. The method includes validation for the
43
+ input parameters to ensure they meet the required criteria before making the API call.
44
+
45
+ Args:
46
+ country_code (str): The country code for the recipients of the campaign (e.g., "234" for Nigeria).
47
+ sender_id (str): The sender ID to be displayed on the recipient's device (must be between 3 and 11 characters).
48
+ message (str): The content of the SMS message to be sent in the campaign.
49
+ message_type (str): The type of message, either "plain" for standard text or "unicode" for messages containing special characters.
50
+ phonebook_id (str): The unique identifier of the phonebook containing the recipients of the campaign.
51
+ enable_link_tracking (bool): A flag indicating whether link tracking should be enabled for the campaign.
52
+ campaign_type (str): The type of campaign, which can be "promotional" or "transactional".
53
+ schedule_sms_status (str): The scheduling status of the campaign, either "scheduled" for campaigns that should be sent at a later time or "regular" for immediate sending.
54
+ schedule_time (str, optional): The scheduled time for the campaign to be sent, required if `schedule_sms_status` is "scheduled". The format should be in ISO 8601 (e.g., "2024-12-31T23:59:00Z").
55
+ channel (str, optional): The channel through which the campaign should be sent, either "dnd" for Do Not Disturb compliant messages or "generic" for standard messages. Default is "dnd".
56
+
57
+ Raises:
58
+ ValueError: If any of the input parameters do not meet the required criteria (e.g., invalid country code,
59
+ sender ID length, message type, campaign type, scheduling status, or missing schedule time when required).
60
+
61
+ Returns:
62
+ A response object containing the result of the campaign send operation, including status and any relevant metadata
63
+
64
+ References:
65
+ - https://developer.termii.com/campaign#:~:text=a%20specified%20phonebook.-,Send%20a%20campaign,-This%20endpoint%20allows
66
+ """
67
+
68
+ if country_code.startswith("+"):
69
+ raise ValueError("country code should not start with '+' sign.")
70
+
71
+ if len(sender_id) < 3 or len(sender_id) > 11:
72
+ raise ValueError("sender id should be between 3 and 11 characters.")
73
+
74
+ if message_type not in ["plain", "unicode"]:
75
+ raise ValueError("message type should be either 'plain' or 'unicode'.")
76
+
77
+ if channel not in ["dnd", "generic"]:
78
+ raise ValueError("channel should be either 'dnd' or 'generic'.")
79
+
80
+ if schedule_sms_status not in ["scheduled", "regular"]:
81
+ raise ValueError("schedule sms status should be either 'scheduled' or 'regular'.")
82
+
83
+ if schedule_sms_status == "scheduled" and not schedule_time:
84
+ raise ValueError("schedule time is required when schedule sms status is 'scheduled'.")
85
+
86
+ payload = {
87
+ "country_code": country_code,
88
+ "sender_id": sender_id,
89
+ "message": message,
90
+ "message_type": message_type,
91
+ "phonebook_id": phonebook_id,
92
+ "enable_link_tracking": enable_link_tracking,
93
+ "campaign_type": campaign_type,
94
+ "schedule_sms_status": schedule_sms_status,
95
+ "schedule_time": schedule_time,
96
+ "channel": channel,
97
+ "remove_duplicate": "yes",
98
+ "delimiter": ","
99
+ }
100
+
101
+ return self.http.post("/api/sms/campaigns/send", payload)
102
+
103
+ def fetch_campaigns(self):
104
+ """
105
+ Fetches a list of all SMS campaigns that have been sent or are scheduled to be sent using the Termii
106
+ API. This method sends a GET request to the `/api/sms/campaigns` endpoint and retrieves the campaign
107
+ history, including details such as campaign ID, status, message content, recipient information, and
108
+ scheduling details. The response will contain an array of campaign objects, each representing a
109
+ specific campaign with its associated metadata.
110
+
111
+ Returns:
112
+ A response object containing a list of SMS campaigns, including details such as campaign ID, status,
113
+ message content, recipient information, and scheduling details.
114
+
115
+ References:
116
+ - https://developer.termii.com/campaign#fetch-campaigns:~:text=C714360330258%22%2C%0A%20%20%20%20%22status%22%3A%20%22success%22%0A%20%20%7D-,Fetch%20campaigns,-This%20endpoint%20retrieves
117
+ """
118
+
119
+ return self.http.fetch("/api/sms/campaigns")
120
+
121
+ def fetch_campaign_history(self, campaign_id: str):
122
+ """
123
+ Fetches the details of a specific SMS campaign using its unique identifier (campaign ID). This method sends
124
+ a GET request to the `/api/sms/campaigns/{campaign_id}` endpoint, where `{campaign_id}` is the unique
125
+ identifier of the campaign whose details you want to retrieve. The response will contain comprehensive
126
+ information about the specified campaign, including its status, message content, recipient
127
+ information, scheduling details, and any relevant metadata.
128
+
129
+ Args:
130
+ campaign_id (str): The unique identifier of the SMS campaign for which to fetch details. This ID is
131
+ typically returned when a campaign is created or can be obtained from the list of campaigns.
132
+
133
+ Raises:
134
+ ValueError: If the `campaign_id` is not provided or is invalid. The campaign ID is required to fetch
135
+ the details of a specific campaign, and it must be a valid identifier that exists in the system.
136
+
137
+ Returns:
138
+ A response object containing the details of the specified SMS campaign, including its status, message
139
+ content, recipient information, scheduling details, and any relevant metadata.
140
+ """
141
+
142
+ if not campaign_id:
143
+ raise ValueError("campaign id is required")
144
+
145
+ return self.http.fetch(f"/api/sms/campaigns/{campaign_id}")
146
+
147
+ def retry_campaign(self, campaign_id: str):
148
+ """
149
+ Retries a specific SMS campaign that may have failed or encountered issues during its initial sending attempt.
150
+ This method sends a PATCH request to the `/api/sms/campaigns/{campaign_id}` endpoint, where `{campaign_id}`
151
+ is the unique identifier of the campaign you wish to retry. The response will indicate whether the retry
152
+ attempt was successful and may include updated campaign details or status information. This functionality
153
+ is useful for campaigns that may have failed due to temporary issues such as network errors or recipient
154
+ device problems, allowing you to attempt sending the campaign again without having to create a new one.
155
+
156
+ Args:
157
+ campaign_id (str): The unique identifier of the SMS campaign that you want to retry. This ID is typically
158
+ returned when a campaign is created or can be obtained from the list of campaigns. It must be a valid
159
+ identifier corresponding to an existing campaign in the system.
160
+
161
+ Raises:
162
+ ValueError: If the `campaign_id` is not provided or is invalid. The campaign ID is required to identify
163
+ which campaign to retry, and it must be a valid identifier that exists in the system.
164
+
165
+ Returns:
166
+ A response object indicating the result of the retry attempt, including whether it was successful and
167
+ any relevant details about the campaign's updated status or information.
168
+ """
169
+
170
+ if not campaign_id:
171
+ raise ValueError("campaign id is required")
172
+
173
+ payload = {}
174
+
175
+ return self.http.patch(f"/api/sms/campaigns/{campaign_id}", json=payload)
@@ -0,0 +1,167 @@
1
+ """
2
+ This module provides the ContactService class, which allows you to manage contacts within a phonebook. You can fetch
3
+ existing contacts, create new contacts (individually or in bulk), and delete contacts from a specified phonebook.
4
+ The service interacts with the Termii API to perform these operations.
5
+
6
+ References:
7
+ - https://developer.termii.com/contacts
8
+
9
+ Classes:
10
+ ContactService: A service class that provides methods to manage contacts within a phonebook via Termii's API.
11
+ """
12
+ from termii_py.http import RequestHandler
13
+
14
+
15
+ class ContactService:
16
+ """
17
+ Provides methods to manage contacts within a phonebook through Termii's API.
18
+ The class includes methods to fetch contacts, create new contacts (individually or in bulk), and delete contacts from a specified phonebook. Each method constructs the appropriate API request and handles the response.
19
+
20
+ Attributes:
21
+ http (object): An HTTP client instance responsible for making API requests to Termii.
22
+ """
23
+
24
+ def __init__(self, http: RequestHandler):
25
+ """
26
+ Initializes the ContactService instance.
27
+
28
+ Args:
29
+ http (object): An HTTP client instance (e.g., `requests.Session`) used to perform network requests to the Termii API.
30
+ """
31
+
32
+ self.http = http
33
+
34
+ def fetch_contacts(self, phonebook_id: str):
35
+ """
36
+ Fetches all contacts associated with a specific phonebook. This method sends a GET request to
37
+ the `/api/phonebooks/{phonebook_id}/contacts` endpoint, where `{phonebook_id}` is the unique identifier of
38
+ the phonebook whose contacts you want to retrieve. The response will contain a list of contacts along with
39
+ their details.
40
+
41
+ Args:
42
+ phonebook_id (str): The unique identifier of the phonebook for which to fetch contacts.
43
+
44
+ Raises:
45
+ ValueError: If the `phonebook_id` is not provided or is invalid.
46
+
47
+ Returns:
48
+ A response object containing the list of contacts and associated metadata for the specified phonebook.
49
+
50
+ References:
51
+ - https://developer.termii.com/contacts#:~:text=within%20your%20phonebooks.-,Fetch%20contacts%20by%20phonebook%20ID,-This%20endpoint%20retrieves
52
+ """
53
+
54
+ if not phonebook_id:
55
+ raise ValueError("phonebook_id is required")
56
+
57
+ return self.http.fetch(f"/api/phonebooks/{phonebook_id}/contacts")
58
+
59
+ def create_contact(self, phonebook_id, phone_number: str, country_code: str, email_address: str = None,
60
+ first_name: str = None, last_name: str = None, company: str = None):
61
+ """
62
+ Creates a new contact within a specified phonebook. This method sends a POST request to
63
+ the `/api/phonebooks/{phonebook_id}/contacts` endpoint, where `{phonebook_id}` is the unique identifier of
64
+ the phonebook to which the contact will be added. The request body should include the contact's
65
+ phone number, country code, and optionally their email address, first name, last name, and company. The
66
+ response will contain the details of the newly created contact.
67
+
68
+ Args:
69
+ phonebook_id (str): The unique identifier of the phonebook to which the contact will be added.
70
+ phone_number (str): The contact's phone number (without the country code).
71
+ country_code (str): The country code for the contact's phone number (without the '+' sign).
72
+ email_address (str, optional): The contact's email address. This field is optional.
73
+ first_name (str, optional): The contact's first name. This field is optional.
74
+ last_name (str, optional): The contact's last name. This field is optional.
75
+ company (str, optional): The contact's company name. This field is optional.
76
+
77
+ Raises:
78
+ ValueError: If the `phonebook_id` is not provided or is invalid.
79
+ ValueError: If the `country_code` starts with a '+' sign. The country code should be provided without the '+' sign.
80
+
81
+ Returns:
82
+ A response object containing the details of the newly created contact, including its unique identifier and any associated metadata.
83
+
84
+ References:
85
+ - https://developer.termii.com/contacts#add-single-contacts-to-phonebook:~:text=12%2C%0A%20%20%20%20%22empty%22%3A%20false%0A%20%20%7D%0A%7D-,Add%20single%20contacts%20to%20phonebook,-This%20endpoint%20allows
86
+ """
87
+
88
+ if not phonebook_id:
89
+ raise ValueError("phonebook_id is required")
90
+
91
+ if country_code.startswith("+"):
92
+ raise ValueError("country code should not start with '+' sign.")
93
+
94
+ payload = {
95
+ "phone_number": phone_number,
96
+ "country_code": country_code,
97
+ "email_address": email_address,
98
+ "first_name": first_name,
99
+ "last_name": last_name,
100
+ "company": company,
101
+ }
102
+
103
+ return self.http.post(f"/api/phonebooks/{phonebook_id}/contacts", json=payload)
104
+
105
+ def create_multiple_contacts(self, phonebook_id, country_code: str, file_path: str, ):
106
+ """
107
+ Creates multiple contacts in bulk by uploading a CSV file containing the contact details. This method sends
108
+ a POST request to the `/api/phonebooks/contacts/upload` endpoint with the specified phonebook ID, country
109
+ code, and file path. The CSV file should be formatted according to Termii's requirements, with columns for
110
+ phone number, email address, first name, last name, and company. The response will indicate the success or
111
+ failure of the bulk upload operation.
112
+
113
+ Args:
114
+ phonebook_id (str): The unique identifier of the phonebook to which the contacts will be added.
115
+ country_code (str): The country code for the contacts' phone numbers (without the '+' sign).
116
+ file_path (str): The file path to the CSV file containing the contact details to be uploaded.
117
+
118
+ Raises:
119
+ ValueError: If the `phonebook_id` is not provided or is invalid.
120
+ ValueError: If the `country_code` starts with a '+' sign. The country code should be provided without the '+' sign.
121
+ ValueError: If the `file_path` is not provided or is invalid. The file path must point to a valid CSV file containing the contact details.
122
+
123
+ Returns:
124
+ A response object indicating the success or failure of the bulk contact upload operation, along with any relevant metadata or error messages.
125
+
126
+ References:
127
+ - https://developer.termii.com/contacts#add-single-contacts-to-phonebook:~:text=Promise%22%2C%0A%20%20%20%20%22last_name%22%3A%20%22John%22%0A%20%20%7D%0A%7D-,Add%20multiple%20contacts%20to%20phonebook,-This%20endpoint%20allows
128
+ """
129
+
130
+ if not phonebook_id:
131
+ raise ValueError("phonebook_id is required")
132
+
133
+ if country_code.startswith("+"):
134
+ raise ValueError("country code should not start with '+' sign.")
135
+
136
+ data = {
137
+ "phonebook_id": phonebook_id,
138
+ "country_code": country_code,
139
+ }
140
+
141
+ return self.http.post_file(f"/api/phonebooks/contacts/upload", data=data, file_path=file_path)
142
+
143
+ def delete_contact(self, phonebook_id):
144
+ """
145
+ Deletes all contacts associated with a specific phonebook. This method sends a DELETE request to the
146
+ `/api/phonebooks/{phonebook_id}/contacts` endpoint, where `{phonebook_id}` is the unique identifier of the
147
+ phonebook from which to delete contacts. The response will indicate the success or failure of the delete
148
+ operation. Note that this action will remove all contacts from the specified phonebook, so use it with
149
+ caution.
150
+
151
+ Args:
152
+ phonebook_id (str): The unique identifier of the phonebook from which to delete contacts.
153
+
154
+ Raises:
155
+ ValueError: If the `phonebook_id` is not provided or is invalid. The `phonebook_id` is required to identify which phonebook's contacts should be deleted.
156
+
157
+ Returns:
158
+ A response object indicating the success or failure of the delete operation, along with any relevant metadata or error messages. The response may include information about the number of contacts deleted or any issues encountered during the process.
159
+
160
+ References:
161
+ - https://developer.termii.com/contacts#add-single-contacts-to-phonebook:~:text=get%20it%20done.%22%0A%20%20%7D-,Delete%20Contact%20in%20a%20Phonebook,-This%20endpoint%20allows
162
+ """
163
+
164
+ if not phonebook_id:
165
+ raise ValueError("phonebook_id is required to delete contacts.")
166
+
167
+ return self.http.delete(f"/api/phonebooks/{phonebook_id}/contacts")
@@ -0,0 +1,191 @@
1
+ """
2
+ This module defines the `MessageService` class, which provides an interface for sending messages
3
+ through Termii's Messaging API.
4
+
5
+ It supports sending single SMS messages, WhatsApp messages, and bulk messages while ensuring
6
+ parameter validation and correct channel-type combinations before making HTTP requests to the API.
7
+
8
+ References:
9
+ - https://developer.termii.com/messaging-api
10
+
11
+ Classes:
12
+ MessageService: Handles message dispatch operations via Termii's Messaging API.
13
+ """
14
+
15
+ from hmac import compare_digest
16
+
17
+ from termii_py.http import RequestHandler
18
+ from termii_py.value_object import PhoneNumber
19
+
20
+
21
+ class MessageService:
22
+ """
23
+ Provides methods for sending SMS, WhatsApp, and bulk messages via Termii's Messaging API.
24
+
25
+ The class ensures data validation and enforces correct message channel and type usage.
26
+ Each method constructs the appropriate request payload and sends it using the injected
27
+ HTTP client.
28
+
29
+ Attributes:
30
+ http (object): An HTTP client instance responsible for making API requests.
31
+ """
32
+
33
+ def __init__(self, http: RequestHandler):
34
+ """
35
+ Initializes the MessageService instance.
36
+
37
+ Args:
38
+ http (object): An HTTP client instance (e.g., `requests.Session`) used to perform
39
+ network requests to the Termii API.
40
+ """
41
+
42
+ self.http = http
43
+
44
+ def send_message(self, sent_to: str, sent_from: str, message: str, channel: str, type: str):
45
+ """
46
+ Sends a single SMS message through Termii's Messaging API.
47
+
48
+ This method supports the following channels:
49
+ - "generic" (for standard messaging)
50
+ - "dnd" (for messages that bypass Do-Not-Disturb filters)
51
+ - "voice" (for voice call messages)
52
+
53
+ Notes:
54
+ - WhatsApp messages must be sent using `send_whatsapp_message()`.
55
+ - For voice messages, the `type` parameter must explicitly be set to "voice".
56
+
57
+ Args:
58
+ sent_to (str): Recipient’s phone number in international format.
59
+ sent_from (str): The sender ID registered on Termii.
60
+ message (str): The content of the message to be sent.
61
+ channel (str): The communication channel. Must be one of ["generic", "dnd", "voice"].
62
+ type (str): The message type (e.g., "plain", "voice").
63
+
64
+ Raises:
65
+ ValueError: If an invalid channel-type combination is provided.
66
+ ValueError: If attempting to send WhatsApp messages via this method.
67
+ ValueError: If `channel` or `type` parameters are invalid.
68
+
69
+ Returns:
70
+ dict: The JSON response returned by the Termii API.
71
+
72
+ References:
73
+ https://developer.termii.com/messaging-api#:~:text=Send%20message
74
+ """
75
+
76
+ PhoneNumber(sent_to)
77
+
78
+ if compare_digest("whatsapp", str(channel).strip().lower()):
79
+ raise ValueError("For WhatsApp messages, please use the 'send_whatsapp_message' method.")
80
+
81
+ if compare_digest("voice", str(channel).strip().lower()) and not compare_digest("voice",
82
+ str(type).strip().lower()):
83
+ raise ValueError("For voice channel, the 'type' parameter must be set to 'voice'.")
84
+
85
+ if not compare_digest("generic", str(channel).strip().lower()) and not compare_digest("dnd",
86
+ str(channel).strip().lower() or not compare_digest(
87
+ "voice",
88
+ str(channel).strip().lower())):
89
+ raise ValueError("The 'channel' parameter must be either 'generic' or 'dnd' or voice.")
90
+
91
+ payload = {
92
+ "to": sent_to,
93
+ "from": sent_from,
94
+ "sms": message,
95
+ "channel": channel,
96
+ "type": type
97
+ }
98
+
99
+ return self.http.post("/api/sms/send", json=payload)
100
+
101
+ def send_whatsapp_message(self, sent_to: str, sent_from: str, message: str, url: str = None, caption: str = None):
102
+ """
103
+ Sends a WhatsApp message through Termii's Messaging API.
104
+
105
+ This endpoint supports text-only and media messages (e.g., image or document links).
106
+
107
+ Args:
108
+ sent_to (str): Recipient’s phone number in international format.
109
+ sent_from (str): The sender ID or WhatsApp business number.
110
+ message (str): Text message to be sent.
111
+ url (str, optional): Media URL for attachments (image, document, etc.).
112
+ caption (str, optional): Caption for the media file (if applicable).
113
+
114
+ Raises:
115
+ ValueError: If the recipient phone number is invalid.
116
+
117
+ Returns:
118
+ dict: The JSON response from the Termii API.
119
+
120
+ References:
121
+ https://developer.termii.com/messaging-api#:~:text=Send%20WhatsApp%20Message%20(Conversational)
122
+ """
123
+
124
+ PhoneNumber(sent_to)
125
+
126
+ payload = {
127
+ "to": sent_to,
128
+ "from": sent_from,
129
+ "sms": message,
130
+ "channel": "whatsapp",
131
+ "type": "plain",
132
+ "media": {
133
+ "url": url,
134
+ "caption": caption,
135
+ }
136
+ }
137
+
138
+ return self.http.post("/api/sms/send", json=payload)
139
+
140
+ def send_bulk_message(self, sent_to: list, sent_from: str, message: str, channel: str, type: str):
141
+ """
142
+ Sends bulk SMS messages to multiple recipients using Termii's Messaging API.
143
+
144
+ This method supports sending to multiple phone numbers in one request. Only the "generic"
145
+ and "dnd" channels are supported for bulk operations.
146
+
147
+ Notes:
148
+ - Voice and WhatsApp messages cannot be sent in bulk.
149
+ - Each recipient number is validated before sending.
150
+
151
+ Args:
152
+ sent_to (list): A list of recipient phone numbers in international format.
153
+ sent_from (str): The sender ID registered on Termii.
154
+ message (str): The content of the message to be sent.
155
+ channel (str): The message channel. Must be either "generic" or "dnd".
156
+ type (str): The message type (e.g., "plain").
157
+
158
+ Raises:
159
+ ValueError: If any recipient number is invalid.
160
+ ValueError: If attempting to send WhatsApp or voice messages in bulk.
161
+ ValueError: If an invalid channel is specified.
162
+
163
+ Returns:
164
+ dict: The JSON response returned by the Termii API.
165
+
166
+ References:
167
+ https://developer.termii.com/messaging-api#:~:text=Send%20Bulk%20message
168
+ """
169
+
170
+ for x in sent_to:
171
+ PhoneNumber(x)
172
+
173
+ if compare_digest("whatsapp", str(channel).strip().lower()):
174
+ raise ValueError("For WhatsApp messages, please use the 'send_whatsapp_message' method.")
175
+
176
+ if compare_digest("voice", str(channel).strip().lower()) or compare_digest("voice", str(type).strip().lower()):
177
+ raise ValueError("Voice messages are not supported in bulk messaging.")
178
+
179
+ if not compare_digest("generic", str(channel).strip().lower()) and not compare_digest("dnd",
180
+ str(channel).strip().lower()):
181
+ raise ValueError("The 'channel' parameter must be either 'generic' or 'dnd' or voice.")
182
+
183
+ payload = {
184
+ "to": sent_to,
185
+ "from": sent_from,
186
+ "sms": message,
187
+ "channel": channel,
188
+ "type": type
189
+ }
190
+
191
+ return self.http.post("/api/sms/send/bulk", json=payload)