Skip to main content

clicksend_rs/
types.rs

1//! Request and response data types.
2//!
3//! ClickSend wraps every response in an [`ApiEnvelope`]. Successful calls
4//! deserialize into `ApiEnvelope<T>` where `T` is the typed payload.
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8/// Generic ClickSend response envelope. Every endpoint returns this shape.
9///
10/// The client only returns `Ok(env)` when `response_code == "SUCCESS"` —
11/// you do not need to check it manually.
12#[derive(Debug, Clone, Deserialize)]
13pub struct ApiEnvelope<T> {
14    /// HTTP status reflected in the body (usually matches transport status).
15    pub http_code: u16,
16    /// ClickSend status code (`"SUCCESS"`, `"ERROR"`, `"INVALID_FROM"`, …).
17    pub response_code: String,
18    /// Human-readable status message.
19    #[serde(default)]
20    pub response_msg: Option<String>,
21    /// Typed payload — `None` only for endpoints that return no data (cancel, etc).
22    #[serde(default = "Option::default")]
23    pub data: Option<T>,
24}
25
26/// Paginated list payload — history, receipts, inbound, etc.
27#[derive(Debug, Clone, Deserialize)]
28pub struct Paginated<T> {
29    /// Total items across all pages.
30    #[serde(default)]
31    pub total: Option<u64>,
32    /// Items per page.
33    #[serde(default)]
34    pub per_page: Option<u32>,
35    /// Current page (1-indexed).
36    #[serde(default)]
37    pub current_page: Option<u32>,
38    /// Last page number.
39    #[serde(default)]
40    pub last_page: Option<u32>,
41    /// URL for the next page, if any.
42    #[serde(default)]
43    pub next_page_url: Option<String>,
44    /// URL for the previous page, if any.
45    #[serde(default)]
46    pub prev_page_url: Option<String>,
47    /// Index of the first item in this page.
48    #[serde(default)]
49    pub from: Option<u64>,
50    /// Index of the last item in this page.
51    #[serde(default)]
52    pub to: Option<u64>,
53    /// Items in the current page.
54    #[serde(default = "Vec::new")]
55    pub data: Vec<T>,
56}
57
58// ───────────────────── helpers ─────────────────────
59
60/// ClickSend returns `schedule` as either a unix timestamp (number) or `""`.
61/// Map both into `Option<i64>`: empty string → None, number → Some.
62pub(crate) fn deser_schedule<'de, D>(d: D) -> Result<Option<i64>, D::Error>
63where
64    D: Deserializer<'de>,
65{
66    use serde::de::Error;
67    let v = serde_json::Value::deserialize(d)?;
68    match v {
69        serde_json::Value::Null => Ok(None),
70        serde_json::Value::String(s) if s.is_empty() => Ok(None),
71        serde_json::Value::String(s) => s.parse::<i64>().map(Some).map_err(Error::custom),
72        serde_json::Value::Number(n) => Ok(n.as_i64()),
73        other => Err(Error::custom(format!("invalid schedule field: {other}"))),
74    }
75}
76
77/// ClickSend frequently returns numeric fields as strings (e.g. `"0.0670"`).
78pub(crate) fn deser_str_or_f64<'de, D>(d: D) -> Result<Option<f64>, D::Error>
79where
80    D: Deserializer<'de>,
81{
82    use serde::de::Error;
83    let v = serde_json::Value::deserialize(d)?;
84    match v {
85        serde_json::Value::Null => Ok(None),
86        serde_json::Value::String(s) if s.is_empty() => Ok(None),
87        serde_json::Value::String(s) => s.parse::<f64>().map(Some).map_err(Error::custom),
88        serde_json::Value::Number(n) => Ok(n.as_f64()),
89        other => Err(Error::custom(format!("expected float-ish, got: {other}"))),
90    }
91}
92
93// ───────────────────── account ─────────────────────
94
95/// Account info returned by `GET /account`.
96///
97/// Field names match ClickSend's response verbatim — they prefix things with
98/// `user_` (so `user_email`, not `email`). Use [`AccountData::email`] for
99/// brevity.
100#[derive(Debug, Clone, Default, Deserialize)]
101pub struct AccountData {
102    /// Numeric user id.
103    #[serde(default)]
104    pub user_id: Option<u64>,
105    /// Username (also your basic-auth username).
106    #[serde(default)]
107    pub username: Option<String>,
108    /// Email on file. See also [`AccountData::email`].
109    #[serde(default)]
110    pub user_email: Option<String>,
111    /// Phone in E.164.
112    #[serde(default)]
113    pub user_phone: Option<String>,
114    /// First name.
115    #[serde(default)]
116    pub user_first_name: Option<String>,
117    /// Last name.
118    #[serde(default)]
119    pub user_last_name: Option<String>,
120    /// `1` if active, `0` if not.
121    #[serde(default)]
122    pub active: Option<u8>,
123    /// `1` if account is banned.
124    #[serde(default)]
125    pub banned: Option<u8>,
126    /// Unix timestamp of sign-up.
127    #[serde(default)]
128    pub date_sign_up: Option<i64>,
129    /// Account balance in [`Currency`] (returned as a string by the API,
130    /// parsed into `f64` here).
131    #[serde(default, deserialize_with = "deser_str_or_f64")]
132    pub balance: Option<f64>,
133    /// Display name.
134    #[serde(default)]
135    pub account_name: Option<String>,
136    /// Where invoices go.
137    #[serde(default)]
138    pub account_billing_email: Option<String>,
139    /// Account country (ISO).
140    #[serde(default)]
141    pub country: Option<String>,
142    /// Default country code for SMS without explicit country.
143    #[serde(default)]
144    pub default_country_sms: Option<String>,
145    /// IANA timezone string.
146    #[serde(default)]
147    pub timezone: Option<String>,
148    /// `1` if on a trial plan.
149    #[serde(default)]
150    pub on_trial: Option<u8>,
151    /// Currency block (USD/AUD/EUR/etc).
152    #[serde(default, rename = "_currency")]
153    pub currency: Option<Currency>,
154}
155
156impl AccountData {
157    /// Shortcut for [`AccountData::user_email`].
158    pub fn email(&self) -> Option<&str> {
159        self.user_email.as_deref()
160    }
161}
162
163// ───────────────────── sms ─────────────────────
164
165/// A single SMS to be sent.
166///
167/// Build with [`SmsMessage::new`] then chain optional setters. Wrap in
168/// [`SmsMessageCollection`] to send.
169#[derive(Debug, Clone, Serialize, Default)]
170pub struct SmsMessage {
171    /// Sender ID — alphanumeric (e.g. `"MyBrand"`) or E.164 number you own.
172    /// If `None`, ClickSend picks a shared number.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub from: Option<String>,
175    /// Message text. Long messages get split into parts and billed per part.
176    pub body: String,
177    /// Recipient in E.164 (`+15551234567`). Either this or [`Self::list_id`].
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub to: Option<String>,
180    /// Free-form tag of where this came from (e.g. `"rust"`). Defaults to `"rust"`.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub source: Option<String>,
183    /// Unix timestamp for scheduled send. `None` = immediate.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub schedule: Option<i64>,
186    /// Your reference id; echoed back in delivery receipts and replies.
187    #[serde(skip_serializing_if = "Option::is_none", rename = "custom_string")]
188    pub custom_string: Option<String>,
189    /// Send to a saved contact list instead of [`Self::to`].
190    #[serde(skip_serializing_if = "Option::is_none", rename = "list_id")]
191    pub list_id: Option<i64>,
192    /// ISO country (e.g. `"US"`) — improves routing for some numbers.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub country: Option<String>,
195    /// Where replies should be emailed.
196    #[serde(skip_serializing_if = "Option::is_none", rename = "from_email")]
197    pub from_email: Option<String>,
198}
199
200impl SmsMessage {
201    /// New SMS with required `to` and `body`. Source defaults to `"rust"`.
202    pub fn new(to: impl Into<String>, body: impl Into<String>) -> Self {
203        Self {
204            to: Some(to.into()),
205            body: body.into(),
206            source: Some("rust".into()),
207            ..Default::default()
208        }
209    }
210
211    /// Set the sender id.
212    pub fn from(mut self, from: impl Into<String>) -> Self {
213        self.from = Some(from.into());
214        self
215    }
216
217    /// Schedule for later (unix timestamp).
218    pub fn schedule(mut self, ts: i64) -> Self {
219        self.schedule = Some(ts);
220        self
221    }
222
223    /// Tag the message with your own reference.
224    pub fn custom_string(mut self, s: impl Into<String>) -> Self {
225        self.custom_string = Some(s.into());
226        self
227    }
228}
229
230/// Batch of SMS sent in one API call. Pricing/sending is per-message.
231#[derive(Debug, Clone, Serialize)]
232pub struct SmsMessageCollection {
233    /// The messages to send.
234    pub messages: Vec<SmsMessage>,
235}
236
237impl SmsMessageCollection {
238    /// Wrap a vec of messages.
239    pub fn new(messages: Vec<SmsMessage>) -> Self {
240        Self { messages }
241    }
242}
243
244/// Response payload for `/sms/send` and `/sms/price`. Same shape — `price`
245/// just doesn't actually send.
246#[derive(Debug, Clone, Default, Deserialize)]
247pub struct SmsSendData {
248    /// Sum of `message_price` across all messages. ClickSend returns this
249    /// as a string; parsed to `f64` here.
250    #[serde(default, deserialize_with = "deser_str_or_f64")]
251    pub total_price: Option<f64>,
252    /// Total messages submitted.
253    #[serde(default)]
254    pub total_count: Option<u32>,
255    /// Successfully queued for sending.
256    #[serde(default)]
257    pub queued_count: Option<u32>,
258    /// Rejected (blocked sender id, opt-out, etc).
259    #[serde(default)]
260    pub blocked_count: Option<u32>,
261    /// Per-message results in the same order as the request.
262    #[serde(default)]
263    pub messages: Vec<SmsSendMessageResult>,
264    /// Currency block.
265    #[serde(default, rename = "_currency")]
266    pub currency: Option<Currency>,
267}
268
269/// Currency block embedded under `_currency` in many responses.
270#[derive(Debug, Clone, Default, Deserialize)]
271pub struct Currency {
272    /// Short code (e.g. `"USD"`).
273    #[serde(default)]
274    pub currency_name_short: Option<String>,
275    /// Long name (e.g. `"US Dollars"`).
276    #[serde(default)]
277    pub currency_name_long: Option<String>,
278    /// Dollar/major prefix (e.g. `"$"`).
279    #[serde(default)]
280    pub currency_prefix_d: Option<String>,
281    /// Cent/minor prefix (e.g. `"¢"`).
282    #[serde(default)]
283    pub currency_prefix_c: Option<String>,
284}
285
286/// One row returned by `/sms/send` and `/sms/price`.
287///
288/// Most fields are `Option` because `price` doesn't populate post-send fields
289/// (`carrier`, `status_code`, …) and `send` doesn't populate response-only
290/// fields. Inspect [`Self::status`] to tell what happened to each message.
291#[derive(Debug, Clone, Default, Deserialize)]
292pub struct SmsSendMessageResult {
293    /// `"out"` for outbound, `"in"` for inbound replies.
294    #[serde(default)]
295    pub direction: Option<String>,
296    /// Unix timestamp when ClickSend processed the message.
297    #[serde(default)]
298    pub date: Option<i64>,
299    /// Recipient (E.164).
300    #[serde(default)]
301    pub to: Option<String>,
302    /// Message body.
303    #[serde(default)]
304    pub body: Option<String>,
305    /// Sender id used.
306    #[serde(default)]
307    pub from: Option<String>,
308    /// Scheduled send time. Quirky: ClickSend returns `""` for "send now",
309    /// or a unix timestamp number — both map to `Option<i64>` here.
310    #[serde(default, deserialize_with = "deser_schedule")]
311    pub schedule: Option<i64>,
312    /// Globally unique message id (use this for cancel/receipts).
313    #[serde(default)]
314    pub message_id: Option<String>,
315    /// SMS parts (long messages get split). Each part is billed.
316    #[serde(default)]
317    pub message_parts: Option<u32>,
318    /// Per-message price. Returned as a string by the API.
319    #[serde(default, deserialize_with = "deser_str_or_f64")]
320    pub message_price: Option<f64>,
321    /// Echoed back from your request.
322    #[serde(default)]
323    pub custom_string: Option<String>,
324    /// Owner user id.
325    #[serde(default)]
326    pub user_id: Option<u64>,
327    /// Subaccount id (multi-tenant accounts).
328    #[serde(default)]
329    pub subaccount_id: Option<u64>,
330    /// Detected destination country (ISO).
331    #[serde(default)]
332    pub country: Option<String>,
333    /// Detected carrier (`"Vodafone"`, `"Verizon"`, …). Populated post-send.
334    #[serde(default)]
335    pub carrier: Option<String>,
336    /// `"SUCCESS"` / `"Sent"` / `"FAILED"` etc.
337    #[serde(default)]
338    pub status: Option<String>,
339    /// Numeric status (per `/sms/history` schema).
340    #[serde(default)]
341    pub status_code: Option<String>,
342    /// Human-readable status.
343    #[serde(default)]
344    pub status_text: Option<String>,
345    /// Numeric error code if delivery failed.
346    #[serde(default)]
347    pub error_code: Option<String>,
348    /// Human-readable error.
349    #[serde(default)]
350    pub error_text: Option<String>,
351    /// `true` if a shared sender pool number was used.
352    #[serde(default)]
353    pub is_shared_system_number: Option<bool>,
354}
355
356/// One row in `/sms/history` — a previously sent (or scheduled) message,
357/// including its current delivery status.
358#[derive(Debug, Clone, Default, Deserialize)]
359pub struct SmsHistoryItem {
360    /// `"out"` for outbound, `"in"` for inbound.
361    #[serde(default)]
362    pub direction: Option<String>,
363    /// Unix timestamp when ClickSend processed it.
364    #[serde(default)]
365    pub date: Option<i64>,
366    /// Recipient.
367    #[serde(default)]
368    pub to: Option<String>,
369    /// Body.
370    #[serde(default)]
371    pub body: Option<String>,
372    /// Sender id used.
373    #[serde(default)]
374    pub from: Option<String>,
375    /// Scheduled time (`""` → None, num → Some).
376    #[serde(default, deserialize_with = "deser_schedule")]
377    pub schedule: Option<i64>,
378    /// Message id.
379    #[serde(default)]
380    pub message_id: Option<String>,
381    /// Number of SMS parts.
382    #[serde(default)]
383    pub message_parts: Option<u32>,
384    /// Price (string in API, parsed here).
385    #[serde(default, deserialize_with = "deser_str_or_f64")]
386    pub message_price: Option<f64>,
387    /// Your reference.
388    #[serde(default)]
389    pub custom_string: Option<String>,
390    /// Owner user id.
391    #[serde(default)]
392    pub user_id: Option<u64>,
393    /// Subaccount id.
394    #[serde(default)]
395    pub subaccount_id: Option<u64>,
396    /// Destination country (ISO).
397    #[serde(default)]
398    pub country: Option<String>,
399    /// Carrier.
400    #[serde(default)]
401    pub carrier: Option<String>,
402    /// Status string.
403    #[serde(default)]
404    pub status: Option<String>,
405    /// Numeric status code.
406    #[serde(default)]
407    pub status_code: Option<String>,
408    /// Human-readable status.
409    #[serde(default)]
410    pub status_text: Option<String>,
411    /// Error code if any.
412    #[serde(default)]
413    pub error_code: Option<String>,
414    /// Error text if any.
415    #[serde(default)]
416    pub error_text: Option<String>,
417}
418
419/// One row in `/sms/receipts` — a delivery confirmation.
420#[derive(Debug, Clone, Default, Deserialize)]
421pub struct SmsReceiptItem {
422    /// Message id this receipt is for.
423    #[serde(default)]
424    pub message_id: Option<String>,
425    /// Status string (e.g. `"DELIVERED"`).
426    #[serde(default)]
427    pub status: Option<String>,
428    /// Numeric status code.
429    #[serde(default)]
430    pub status_code: Option<String>,
431    /// Human-readable status.
432    #[serde(default)]
433    pub status_text: Option<String>,
434    /// When the carrier reported delivery.
435    #[serde(default)]
436    pub timestamp: Option<i64>,
437    /// Sender id used.
438    #[serde(default)]
439    pub originator: Option<String>,
440}
441
442/// One row in `/sms/inbound` — an incoming SMS to your numbers.
443#[derive(Debug, Clone, Default, Deserialize)]
444pub struct SmsInboundItem {
445    /// Message id assigned by ClickSend.
446    #[serde(default)]
447    pub message_id: Option<String>,
448    /// Who texted you.
449    #[serde(default)]
450    pub from: Option<String>,
451    /// Which of your numbers received it.
452    #[serde(default)]
453    pub to: Option<String>,
454    /// Message body.
455    #[serde(default)]
456    pub body: Option<String>,
457    /// When it arrived.
458    #[serde(default)]
459    pub timestamp: Option<i64>,
460    /// Your reference (echoed from the original outbound).
461    #[serde(default)]
462    pub custom_string: Option<String>,
463    /// If this is a reply, the id of the message it's replying to.
464    #[serde(default)]
465    pub original_message_id: Option<String>,
466    /// Original outbound body (if this is a reply).
467    #[serde(default)]
468    pub original_body: Option<String>,
469    /// Sender id of the original outbound.
470    #[serde(default)]
471    pub originator: Option<String>,
472}
473
474// ───────────────────── mms ─────────────────────
475
476/// One MMS to be sent. Subject has a 20-char limit per ClickSend.
477#[derive(Debug, Clone, Serialize, Default)]
478pub struct MmsMessage {
479    /// Sender id.
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub from: Option<String>,
482    /// Message text.
483    pub body: String,
484    /// Subject (max 20 chars).
485    pub subject: String,
486    /// Recipient (E.164).
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub to: Option<String>,
489    /// Source tag.
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub source: Option<String>,
492    /// Unix timestamp for scheduled send.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub schedule: Option<i64>,
495    /// Your reference id.
496    #[serde(skip_serializing_if = "Option::is_none", rename = "custom_string")]
497    pub custom_string: Option<String>,
498    /// Send to a saved list instead of `to`.
499    #[serde(skip_serializing_if = "Option::is_none", rename = "list_id")]
500    pub list_id: Option<i64>,
501    /// Destination country (ISO).
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub country: Option<String>,
504    /// Where replies should be emailed.
505    #[serde(skip_serializing_if = "Option::is_none", rename = "from_email")]
506    pub from_email: Option<String>,
507}
508
509impl MmsMessage {
510    /// New MMS with required `to`, `subject`, `body`. Source defaults to `"rust"`.
511    pub fn new(
512        to: impl Into<String>,
513        subject: impl Into<String>,
514        body: impl Into<String>,
515    ) -> Self {
516        Self {
517            to: Some(to.into()),
518            subject: subject.into(),
519            body: body.into(),
520            source: Some("rust".into()),
521            ..Default::default()
522        }
523    }
524}
525
526/// Batch of MMS sharing one media URL.
527///
528/// **Gotcha:** `media_file` must be a publicly reachable URL (PNG/JPG/GIF) —
529/// ClickSend pulls it down. There is no upload-from-bytes path here.
530#[derive(Debug, Clone, Serialize)]
531pub struct MmsMessageCollection {
532    /// Public URL to the image.
533    pub media_file: String,
534    /// Messages to send (all share the same `media_file`).
535    pub messages: Vec<MmsMessage>,
536}
537
538impl MmsMessageCollection {
539    /// Wrap a media URL + messages.
540    pub fn new(media_file: impl Into<String>, messages: Vec<MmsMessage>) -> Self {
541        Self {
542            media_file: media_file.into(),
543            messages,
544        }
545    }
546}
547
548// ───────────────────── voice ─────────────────────
549
550/// One TTS voice call.
551///
552/// ClickSend will dial `to`, then a TTS engine reads `body` aloud in `lang`
553/// using the `voice` (`"female"`/`"male"`).
554#[derive(Debug, Clone, Serialize, Default)]
555pub struct VoiceMessage {
556    /// Recipient (E.164).
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub to: Option<String>,
559    /// Text the TTS engine reads aloud.
560    pub body: String,
561    /// `"female"` or `"male"`.
562    pub voice: String,
563    /// BCP-47-ish language tag (e.g. `"en-us"`, `"en-gb"`, `"de-de"`).
564    pub lang: String,
565    /// Destination country (ISO).
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub country: Option<String>,
568    /// Source tag.
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub source: Option<String>,
571    /// Your reference id.
572    #[serde(skip_serializing_if = "Option::is_none", rename = "custom_string")]
573    pub custom_string: Option<String>,
574    /// Send to a saved list instead of `to`.
575    #[serde(skip_serializing_if = "Option::is_none", rename = "list_id")]
576    pub list_id: Option<i64>,
577    /// Schedule for later (unix timestamp).
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub schedule: Option<i64>,
580    /// `1` to capture a DTMF keypress from the recipient.
581    #[serde(skip_serializing_if = "Option::is_none", rename = "require_input")]
582    pub require_input: Option<u8>,
583    /// `1` to detect voicemail and leave a message.
584    #[serde(skip_serializing_if = "Option::is_none", rename = "machine_detection")]
585    pub machine_detection: Option<u8>,
586}
587
588impl VoiceMessage {
589    /// New voice call with required `to` and `body`.
590    /// Defaults: voice=female, lang=en-us, source=rust.
591    pub fn new(to: impl Into<String>, body: impl Into<String>) -> Self {
592        Self {
593            to: Some(to.into()),
594            body: body.into(),
595            voice: "female".into(),
596            lang: "en-us".into(),
597            source: Some("rust".into()),
598            ..Default::default()
599        }
600    }
601
602    /// Override the voice (`"female"` or `"male"`).
603    pub fn voice(mut self, v: impl Into<String>) -> Self {
604        self.voice = v.into();
605        self
606    }
607
608    /// Override the language tag.
609    pub fn lang(mut self, v: impl Into<String>) -> Self {
610        self.lang = v.into();
611        self
612    }
613}
614
615/// Batch of voice calls.
616#[derive(Debug, Clone, Serialize)]
617pub struct VoiceMessageCollection {
618    /// Calls to dial.
619    pub messages: Vec<VoiceMessage>,
620}
621
622impl VoiceMessageCollection {
623    /// Wrap a vec of calls.
624    pub fn new(messages: Vec<VoiceMessage>) -> Self {
625        Self { messages }
626    }
627}
628
629// ───────────────────── email ─────────────────────
630
631/// One recipient in `to` / `cc` / `bcc`.
632#[derive(Debug, Clone, Serialize)]
633pub struct EmailRecipient {
634    /// Email address.
635    pub email: String,
636    /// Display name.
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub name: Option<String>,
639}
640
641/// `from` block for [`Email`].
642///
643/// **Major gotcha:** `email_address_id` is **NOT** an email — it is the
644/// numeric/string id ClickSend assigns after you register and verify a sender
645/// address in the dashboard. Plug in `"hello@you.com"` and it will be rejected.
646#[derive(Debug, Clone, Serialize)]
647pub struct EmailFrom {
648    /// ClickSend's id for a verified sender (NOT an email).
649    pub email_address_id: String,
650    /// Display name shown in the recipient's inbox.
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub name: Option<String>,
653}
654
655/// A transactional email payload for `/email/send`.
656#[derive(Debug, Clone, Serialize)]
657pub struct Email {
658    /// Primary recipients.
659    pub to: Vec<EmailRecipient>,
660    /// CC recipients.
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub cc: Option<Vec<EmailRecipient>>,
663    /// BCC recipients.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub bcc: Option<Vec<EmailRecipient>>,
666    /// Sender (uses [`EmailFrom::email_address_id`] — see its docs).
667    pub from: EmailFrom,
668    /// Subject line.
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub subject: Option<String>,
671    /// Body. ClickSend treats this as HTML.
672    pub body: String,
673    /// Attachments. Schema is open-ended — file a PR if you wire it up.
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub attachments: Option<Vec<serde_json::Value>>,
676    /// Scheduled send time (unix timestamp).
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub schedule: Option<i64>,
679}
680
681// ───────────────────── webhooks ─────────────────────
682
683/// Body of an **inbound SMS** webhook (ClickSend POSTs this to your URL when a
684/// number of yours receives a text). Decode with
685/// [`crate::webhook::parse_inbound_sms`].
686#[derive(Debug, Clone, Default, Deserialize)]
687pub struct InboundSmsWebhook {
688    /// Message id.
689    #[serde(default)]
690    pub message_id: Option<String>,
691    /// Sender (the texter).
692    #[serde(default)]
693    pub from: Option<String>,
694    /// Your number that received the text.
695    #[serde(default)]
696    pub to: Option<String>,
697    /// Message body.
698    #[serde(default)]
699    pub body: Option<String>,
700    /// When ClickSend received it.
701    #[serde(default)]
702    pub timestamp: Option<i64>,
703    /// Your reference id (echoed from the original outbound).
704    #[serde(default)]
705    pub custom_string: Option<String>,
706    /// If this is a reply, the id of the message it's replying to.
707    #[serde(default)]
708    pub original_message_id: Option<String>,
709    /// Original outbound body.
710    #[serde(default)]
711    pub original_body: Option<String>,
712    /// Sender id of the original outbound.
713    #[serde(default)]
714    pub originator: Option<String>,
715}
716
717/// Body of a **delivery receipt** webhook. Decode with
718/// [`crate::webhook::parse_delivery_receipt`].
719#[derive(Debug, Clone, Default, Deserialize)]
720pub struct DeliveryReceiptWebhook {
721    /// Message id this receipt is for.
722    #[serde(default)]
723    pub message_id: Option<String>,
724    /// Status string (e.g. `"DELIVERED"`).
725    #[serde(default)]
726    pub status: Option<String>,
727    /// Numeric status code.
728    #[serde(default)]
729    pub status_code: Option<String>,
730    /// Human-readable status.
731    #[serde(default)]
732    pub status_text: Option<String>,
733    /// Numeric error code if delivery failed.
734    #[serde(default)]
735    pub error_code: Option<String>,
736    /// Human-readable error.
737    #[serde(default)]
738    pub error_text: Option<String>,
739    /// When the carrier reported the result.
740    #[serde(default)]
741    pub timestamp: Option<i64>,
742    /// Your reference id.
743    #[serde(default)]
744    pub custom_string: Option<String>,
745    /// Sender id used for the outbound.
746    #[serde(default)]
747    pub originator: Option<String>,
748}