infobip-sms-sdk 0.1.0

Async Rust SDK for the Infobip SMS API: send messages, manage scheduled bulks, query delivery reports and logs, fetch inbound SMS, and parse webhook payloads.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Models for `POST /sms/3/messages` — sending one or more SMS.
//!
//! The top-level request is [`SmsRequest`]. Each entry in
//! [`SmsRequest::messages`] is one [`SmsMessage`] with its own sender,
//! destinations, content (text or binary), and optional per-message
//! [options][`SmsMessageOptions`] / [webhooks][`Webhooks`]. Bulk-wide
//! options like scheduling and click-tracking go on
//! [`SmsRequest::options`].
//!
//! # Examples
//!
//! ## Sending a basic text message
//!
//! ```
//! use infobip_sms::models::send::{
//!     SmsMessage, SmsMessageContent, SmsRequest, SmsTextMessageContent, SmsToDestination,
//! };
//!
//! let request = SmsRequest {
//!     messages: vec![SmsMessage {
//!         sender: Some("InfoSMS".into()),
//!         destinations: vec![SmsToDestination {
//!             to: "41793026727".into(),
//!             ..Default::default()
//!         }],
//!         content: SmsMessageContent::Text(SmsTextMessageContent {
//!             text: "Hello!".into(),
//!             ..Default::default()
//!         }),
//!         ..Default::default()
//!     }],
//!     options: None,
//! };
//! # let _ = request;
//! ```
//!
//! ## Scheduling a send
//!
//! ```
//! use infobip_sms::models::send::{RequestSchedulingSettings, SmsRequest, SmsRequestOptions};
//!
//! let _ = SmsRequest {
//!     messages: vec![/* ... */],
//!     options: Some(SmsRequestOptions {
//!         schedule: Some(RequestSchedulingSettings {
//!             bulk_id: Some("BULK-ID-123-xyz".into()),
//!             send_at: Some("2026-12-24T09:00:00.000+0000".into()),
//!             sending_speed_limit: None,
//!         }),
//!         ..Default::default()
//!     }),
//! };
//! ```

use serde::{Deserialize, Serialize};

use crate::models::common::{
    DeliveryTimeWindow, Platform, RegionalOptions, SendingSpeedLimit, Status, ValidityPeriod,
};

/// Top-level request body for [`Client::send_messages`](crate::Client::send_messages).
///
/// Mirrors the API's `SmsRequestEnvelope` schema.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SmsRequest {
    /// Each entry is one logical message; one message can fan out to
    /// multiple destinations via [`SmsMessage::destinations`].
    pub messages: Vec<SmsMessage>,
    /// Bulk-wide options (scheduling, click tracking, conversion
    /// tracking, …). Optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub options: Option<SmsRequestOptions>,
}

/// Options applied to *every* message in a [`SmsRequest`].
///
/// For per-message options (validity period, delivery window, regional
/// options, …) see [`SmsMessageOptions`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsRequestOptions {
    /// Scheduling settings (bulk ID, `sendAt`, sending speed limit).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub schedule: Option<RequestSchedulingSettings>,
    /// URL shortening and click-tracking configuration.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tracking: Option<UrlOptions>,
    /// Set to `true` to receive `messageCount` in the response (the
    /// number of SMS parts each message split into).
    ///
    /// Not compatible with binary messages.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub include_sms_count_in_response: Option<bool>,
    /// Conversion tracking configuration. Pair with
    /// [`Client::end_conversion_log`](crate::Client::end_conversion_log)
    /// to mark a successful conversion.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversion_tracking: Option<ConversionTracking>,
}

/// Scheduling settings for a [`SmsRequest`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestSchedulingSettings {
    /// Optional client-supplied bulk ID. Auto-generated if omitted;
    /// either way, the resolved value comes back in
    /// [`crate::models::send::SmsResponse::bulk_id`]. Use this ID to
    /// later query/reschedule/cancel the bulk.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bulk_id: Option<String>,
    /// Date and time at which to send. Format
    /// `yyyy-MM-dd'T'HH:mm:ss.SSSZ`. Must be at most 180 days in the
    /// future.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send_at: Option<String>,
    /// Caps how fast the bulk dispatches.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sending_speed_limit: Option<SendingSpeedLimit>,
}

/// URL shortening and click-tracking options.
///
/// See [Infobip's URL shortening docs](https://www.infobip.com/docs/url-shortening).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UrlOptions {
    /// Enable URL shortening. Must be `true` to use the other fields.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shorten_url: Option<bool>,
    /// Track clicks on shortened URLs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub track_clicks: Option<bool>,
    /// Webhook URL to receive click reports.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tracking_url: Option<String>,
    /// Strip `http://` / `https://` from shortened URLs (saves
    /// characters but some handsets won't recognize it as a link).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remove_protocol: Option<bool>,
    /// Use a custom domain for shortened URLs. Must be pre-configured
    /// on your account.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_domain: Option<String>,
}

/// Conversion tracking configuration on a [`SmsRequest`].
///
/// When enabled, the API tracks impressions; you call
/// [`Client::end_conversion_log`](crate::Client::end_conversion_log)
/// per message to record a successful conversion.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversionTracking {
    /// `true` to enable conversion tracking. Defaults to `false`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub use_conversion_tracking: Option<bool>,
    /// Friendly name for the conversion campaign,
    /// e.g. `"ONE_TIME_PIN"` or `"SOCIAL_INVITES"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversion_tracking_name: Option<String>,
}

/// One logical message inside a [`SmsRequest`].
///
/// One [`SmsMessage`] can be sent to many destinations at once; the
/// API generates a separate per-recipient `messageId` for each.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SmsMessage {
    /// Sender ID — alphanumeric (e.g. `"InfoSMS"`) or numeric
    /// (e.g. `"41793026700"`). Subject to length and registration
    /// constraints documented [here][1].
    ///
    /// [1]: https://www.infobip.com/docs/sms/get-started#sender-names
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender: Option<String>,
    /// One or more destination addresses (international format,
    /// e.g. `"41793026727"`).
    pub destinations: Vec<SmsToDestination>,
    /// Either textual or binary content. See [`SmsMessageContent`].
    pub content: SmsMessageContent,
    /// Per-message options (validity, delivery window, regional…).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub options: Option<SmsMessageOptions>,
    /// Per-message webhook configuration for delivery reports.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub webhooks: Option<Webhooks>,
}

/// One destination on an [`SmsMessage`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsToDestination {
    /// Destination address in international format
    /// (e.g. `"41793026727"`). Max 64 characters.
    pub to: String,
    /// Optional client-supplied per-recipient message ID. Auto-
    /// generated if omitted. Anything over 200 characters is clipped.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message_id: Option<String>,
    /// Network ID (US/CA only, contact Infobip support to enable).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub network_id: Option<i32>,
}

/// Body of an [`SmsMessage`] — either text or binary.
///
/// Serialized untagged so the JSON matches the API's `oneOf` schema
/// directly:
///
/// ```ignore
/// // text content:
/// { "text": "...", "language": {...}, "transliteration": "..." }
///
/// // binary content:
/// { "hex": "...", "dataCoding": 8, "esmClass": 0 }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SmsMessageContent {
    /// A standard text SMS.
    Text(SmsTextMessageContent),
    /// A binary SMS (e.g. WAP push, ringtone, vCard).
    Binary(SmsBinaryMessageContent),
}

impl Default for SmsMessageContent {
    fn default() -> Self {
        Self::Text(SmsTextMessageContent::default())
    }
}

/// Plain-text body for an [`SmsMessage`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SmsTextMessageContent {
    /// The message body.
    pub text: String,
    /// Convert characters between scripts before sending; useful when
    /// you want consistent rendering across handsets that don't
    /// support a given alphabet.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transliteration: Option<TransliterationCode>,
    /// National-language settings (Turkish, Spanish, Portuguese, or
    /// AUTODETECT).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub language: Option<SmsLanguage>,
}

/// Binary body for an [`SmsMessage`].
///
/// Used for WAP push, OTA configuration, ringtones, vCards, etc. When
/// in doubt, prefer [`SmsTextMessageContent`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsBinaryMessageContent {
    /// Hex-encoded payload. Pairs of digits represent bytes; spaces
    /// are accepted as separators (e.g. `"48 65 6c 6c 6f"`).
    pub hex: String,
    /// Data Coding Scheme value. `0` = GSM7 (default), `8` = Unicode.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data_coding: Option<i32>,
    /// SMPP `esm_class` value. Defaults to `0`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub esm_class: Option<i32>,
}

/// Script the message body should be transliterated into before
/// sending.
///
/// See [transliteration documentation][1].
///
/// [1]: https://www.infobip.com/docs/sms/language#sms-transliteration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[allow(missing_docs)]
pub enum TransliterationCode {
    None,
    Turkish,
    Greek,
    Cyrillic,
    SerbianCyrillic,
    CentralEuropean,
    Baltic,
    NonUnicode,
    Portuguese,
    Colombian,
    BulgarianCyrillic,
    /// Auto-recognize all supported languages.
    All,
}

/// National-language settings on an [`SmsTextMessageContent`].
///
/// See Infobip's [national language identifier docs][1].
///
/// [1]: https://www.infobip.com/docs/sms/language#national-language-identifier
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsLanguage {
    /// Target language for character-set selection.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub language_code: Option<LanguageCode>,
    /// Use the GSM single-shift table for selective extension
    /// characters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub single_shift: Option<bool>,
    /// Use a GSM locking-shift table for full national-language
    /// support.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub locking_shift: Option<bool>,
}

/// Language tag understood by the API for character-set selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum LanguageCode {
    /// No specific language.
    None,
    /// Turkish.
    #[serde(rename = "TR")]
    Tr,
    /// Spanish.
    #[serde(rename = "ES")]
    Es,
    /// Portuguese.
    #[serde(rename = "PT")]
    Pt,
    /// Let the platform guess.
    Autodetect,
}

/// Per-message options.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsMessageOptions {
    /// Routing entity / application IDs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub platform: Option<Platform>,
    /// How long to keep retrying delivery.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub validity_period: Option<ValidityPeriod>,
    /// Restrict delivery to a daily window.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delivery_time_window: Option<DeliveryTimeWindow>,
    /// Marketing campaign reference, returned later in delivery
    /// reports / logs / clicks. Max 255 characters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub campaign_reference_id: Option<String>,
    /// Region-specific options (India DLT, Turkey IYS, South Korea).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub regional: Option<RegionalOptions>,
    /// Send as a [flash SMS][1] (pops up on the recipient's device
    /// without interaction).
    ///
    /// [1]: https://www.infobip.com/docs/sms/message-types#flash-sms
    #[serde(skip_serializing_if = "Option::is_none")]
    pub flash: Option<bool>,
}

/// Per-message delivery-report webhook configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Webhooks {
    /// URL & flags for delivery reports.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delivery: Option<MessageDeliveryReporting>,
    /// Preferred response content type
    /// (`"application/json"` or `"application/xml"`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_type: Option<String>,
    /// Opaque value echoed back on every delivery report and click
    /// for this message. Max 4000 characters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub callback_data: Option<String>,
}

/// Delivery-report webhook URL and flags.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageDeliveryReporting {
    /// HTTPS URL to receive delivery reports. Retry cycle on failure
    /// is `1min + (1min × retryNumber²)`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Also send intermediate reports (operator-level updates) in
    /// addition to the final delivery report.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub intermediate_report: Option<bool>,
    /// Send delivery reports at all. Defaults to your account-level
    /// subscription setting.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notify: Option<bool>,
}

/// Top-level response body from
/// [`Client::send_messages`](crate::Client::send_messages).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsResponse {
    /// Bulk ID assigned by the API. Use this to fetch delivery
    /// reports, query bulk status, or reschedule/cancel.
    pub bulk_id: Option<String>,
    /// Per-recipient response details.
    pub messages: Vec<SmsMessageResponse>,
}

/// One per-recipient entry inside an [`SmsResponse`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsMessageResponse {
    /// Per-recipient message ID. Use this to query the delivery
    /// report for a single message.
    pub message_id: Option<String>,
    /// Initial status (typically `PENDING_ACCEPTED` or
    /// `PENDING_WAITING_DELIVERY`).
    pub status: Option<Status>,
    /// Destination address that this entry corresponds to.
    pub destination: Option<String>,
    /// Extra details (e.g. SMS-part count).
    pub details: Option<SmsMessageResponseDetails>,
}

/// Optional details on an [`SmsMessageResponse`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SmsMessageResponseDetails {
    /// Number of SMS parts the message was split into. Only populated
    /// when
    /// [`SmsRequestOptions::include_sms_count_in_response`] is `true`.
    pub message_count: Option<i32>,
}