sendly 3.8.1__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.
sendly/types.py ADDED
@@ -0,0 +1,748 @@
1
+ """
2
+ Sendly Python SDK Types
3
+
4
+ This module contains all type definitions and data models.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Any, Dict, List, Literal, Optional
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ # ============================================================================
14
+ # Enums
15
+ # ============================================================================
16
+
17
+
18
+ class MessageStatus(str, Enum):
19
+ """Message delivery status"""
20
+
21
+ QUEUED = "queued"
22
+ SENT = "sent"
23
+ DELIVERED = "delivered"
24
+ FAILED = "failed"
25
+
26
+
27
+ class SenderType(str, Enum):
28
+ """How the message was sent"""
29
+
30
+ NUMBER_POOL = "number_pool"
31
+ ALPHANUMERIC = "alphanumeric"
32
+ SANDBOX = "sandbox"
33
+
34
+
35
+ class MessageType(str, Enum):
36
+ """Message type for compliance classification"""
37
+
38
+ MARKETING = "marketing"
39
+ TRANSACTIONAL = "transactional"
40
+
41
+
42
+ class PricingTier(str, Enum):
43
+ """SMS pricing tier"""
44
+
45
+ DOMESTIC = "domestic"
46
+ TIER1 = "tier1"
47
+ TIER2 = "tier2"
48
+ TIER3 = "tier3"
49
+
50
+
51
+ # ============================================================================
52
+ # Configuration
53
+ # ============================================================================
54
+
55
+
56
+ class SendlyConfig(BaseModel):
57
+ """Configuration options for the Sendly client"""
58
+
59
+ api_key: str = Field(..., description="Your Sendly API key")
60
+ base_url: str = Field(
61
+ default="https://sendly.live/api/v1",
62
+ description="Base URL for the Sendly API",
63
+ )
64
+ timeout: float = Field(
65
+ default=30.0,
66
+ description="Request timeout in seconds",
67
+ )
68
+ max_retries: int = Field(
69
+ default=3,
70
+ description="Maximum number of retry attempts",
71
+ )
72
+
73
+
74
+ # ============================================================================
75
+ # Messages
76
+ # ============================================================================
77
+
78
+
79
+ class SendMessageRequest(BaseModel):
80
+ """Request payload for sending an SMS message"""
81
+
82
+ to: str = Field(
83
+ ...,
84
+ description="Destination phone number in E.164 format",
85
+ examples=["+15551234567"],
86
+ )
87
+ text: str = Field(
88
+ ...,
89
+ description="Message content",
90
+ min_length=1,
91
+ )
92
+ from_: Optional[str] = Field(
93
+ default=None,
94
+ alias="from",
95
+ description="Sender ID or phone number",
96
+ )
97
+ message_type: Optional[MessageType] = Field(
98
+ default=None,
99
+ alias="messageType",
100
+ description="Message type: 'marketing' (default, subject to quiet hours) or 'transactional' (24/7)",
101
+ )
102
+
103
+ class Config:
104
+ populate_by_name = True
105
+
106
+
107
+ class Message(BaseModel):
108
+ """A sent or received SMS message"""
109
+
110
+ id: str = Field(..., description="Unique message identifier")
111
+ to: str = Field(..., description="Destination phone number")
112
+ from_: Optional[str] = Field(
113
+ default=None, alias="from", description="Sender ID or phone number"
114
+ )
115
+ text: str = Field(..., description="Message content")
116
+ status: MessageStatus = Field(..., description="Delivery status")
117
+ direction: Literal["outbound", "inbound"] = Field(
118
+ default="outbound", description="Message direction"
119
+ )
120
+ error: Optional[str] = Field(default=None, description="Error message if failed")
121
+ segments: int = Field(default=1, description="Number of SMS segments")
122
+ credits_used: int = Field(default=0, alias="creditsUsed", description="Credits charged")
123
+ is_sandbox: bool = Field(default=False, alias="isSandbox", description="Sandbox mode flag")
124
+ sender_type: Optional[SenderType] = Field(
125
+ default=None, alias="senderType", description="How the message was sent"
126
+ )
127
+ telnyx_message_id: Optional[str] = Field(
128
+ default=None, alias="telnyxMessageId", description="Telnyx message ID for tracking"
129
+ )
130
+ warning: Optional[str] = Field(
131
+ default=None, description="Warning message (e.g., when 'from' is ignored)"
132
+ )
133
+ sender_note: Optional[str] = Field(
134
+ default=None, alias="senderNote", description="Note about sender behavior"
135
+ )
136
+ created_at: Optional[str] = Field(
137
+ default=None, alias="createdAt", description="Creation timestamp"
138
+ )
139
+ delivered_at: Optional[str] = Field(
140
+ default=None, alias="deliveredAt", description="Delivery timestamp"
141
+ )
142
+
143
+ class Config:
144
+ populate_by_name = True
145
+
146
+
147
+ class MessageListResponse(BaseModel):
148
+ """Response from listing messages"""
149
+
150
+ data: List[Message] = Field(..., description="List of messages")
151
+ count: int = Field(..., description="Total count")
152
+
153
+
154
+ class ListMessagesOptions(BaseModel):
155
+ """Options for listing messages"""
156
+
157
+ limit: Optional[int] = Field(
158
+ default=50,
159
+ ge=1,
160
+ le=100,
161
+ description="Maximum number of messages to return",
162
+ )
163
+
164
+
165
+ # ============================================================================
166
+ # Scheduled Messages
167
+ # ============================================================================
168
+
169
+
170
+ class ScheduledMessageStatus(str, Enum):
171
+ """Scheduled message status"""
172
+
173
+ SCHEDULED = "scheduled"
174
+ SENT = "sent"
175
+ CANCELLED = "cancelled"
176
+ FAILED = "failed"
177
+
178
+
179
+ class ScheduleMessageRequest(BaseModel):
180
+ """Request payload for scheduling an SMS message"""
181
+
182
+ to: str = Field(
183
+ ...,
184
+ description="Destination phone number in E.164 format",
185
+ )
186
+ text: str = Field(
187
+ ...,
188
+ description="Message content",
189
+ min_length=1,
190
+ )
191
+ scheduled_at: str = Field(
192
+ ...,
193
+ alias="scheduledAt",
194
+ description="When to send (ISO 8601, must be > 1 minute in future)",
195
+ )
196
+ from_: Optional[str] = Field(
197
+ default=None,
198
+ alias="from",
199
+ description="Sender ID (for international destinations only)",
200
+ )
201
+ message_type: Optional[MessageType] = Field(
202
+ default=None,
203
+ alias="messageType",
204
+ description="Message type: 'marketing' (default, subject to quiet hours) or 'transactional' (24/7)",
205
+ )
206
+
207
+ class Config:
208
+ populate_by_name = True
209
+
210
+
211
+ class ScheduledMessage(BaseModel):
212
+ """A scheduled SMS message"""
213
+
214
+ id: str = Field(..., description="Unique message identifier")
215
+ to: str = Field(..., description="Destination phone number")
216
+ from_: Optional[str] = Field(default=None, alias="from", description="Sender ID")
217
+ text: str = Field(..., description="Message content")
218
+ status: ScheduledMessageStatus = Field(..., description="Current status")
219
+ scheduled_at: str = Field(..., alias="scheduledAt", description="When message is scheduled")
220
+ credits_reserved: int = Field(
221
+ default=0, alias="creditsReserved", description="Credits reserved"
222
+ )
223
+ error: Optional[str] = Field(default=None, description="Error message if failed")
224
+ created_at: Optional[str] = Field(
225
+ default=None, alias="createdAt", description="Creation timestamp"
226
+ )
227
+ cancelled_at: Optional[str] = Field(
228
+ default=None, alias="cancelledAt", description="Cancellation timestamp"
229
+ )
230
+ sent_at: Optional[str] = Field(default=None, alias="sentAt", description="Sent timestamp")
231
+
232
+ class Config:
233
+ populate_by_name = True
234
+
235
+
236
+ class ScheduledMessageListResponse(BaseModel):
237
+ """Response from listing scheduled messages"""
238
+
239
+ data: List[ScheduledMessage] = Field(..., description="List of scheduled messages")
240
+ count: int = Field(..., description="Total count")
241
+
242
+
243
+ class ListScheduledMessagesOptions(BaseModel):
244
+ """Options for listing scheduled messages"""
245
+
246
+ limit: Optional[int] = Field(
247
+ default=50,
248
+ ge=1,
249
+ le=100,
250
+ description="Maximum number of messages to return",
251
+ )
252
+ offset: Optional[int] = Field(
253
+ default=0,
254
+ ge=0,
255
+ description="Number of messages to skip",
256
+ )
257
+ status: Optional[ScheduledMessageStatus] = Field(
258
+ default=None,
259
+ description="Filter by status",
260
+ )
261
+
262
+
263
+ class CancelledMessageResponse(BaseModel):
264
+ """Response from cancelling a scheduled message"""
265
+
266
+ id: str = Field(..., description="Message ID")
267
+ status: Literal["cancelled"] = Field(..., description="Status (always cancelled)")
268
+ credits_refunded: int = Field(..., alias="creditsRefunded", description="Credits refunded")
269
+ cancelled_at: str = Field(..., alias="cancelledAt", description="Cancellation timestamp")
270
+
271
+ class Config:
272
+ populate_by_name = True
273
+
274
+
275
+ # ============================================================================
276
+ # Batch Messages
277
+ # ============================================================================
278
+
279
+
280
+ class BatchStatus(str, Enum):
281
+ """Batch status"""
282
+
283
+ PROCESSING = "processing"
284
+ COMPLETED = "completed"
285
+ PARTIAL_FAILURE = "partial_failure"
286
+ FAILED = "failed"
287
+
288
+
289
+ class BatchMessageItem(BaseModel):
290
+ """A single message in a batch request"""
291
+
292
+ to: str = Field(..., description="Destination phone number in E.164 format")
293
+ text: str = Field(..., description="Message content")
294
+
295
+
296
+ class BatchMessageRequest(BaseModel):
297
+ """Request payload for sending batch messages"""
298
+
299
+ messages: List[BatchMessageItem] = Field(
300
+ ...,
301
+ description="Array of messages to send (max 1000)",
302
+ min_length=1,
303
+ max_length=1000,
304
+ )
305
+ from_: Optional[str] = Field(
306
+ default=None,
307
+ alias="from",
308
+ description="Sender ID (for international destinations only)",
309
+ )
310
+ message_type: Optional[MessageType] = Field(
311
+ default=None,
312
+ alias="messageType",
313
+ description="Message type: 'marketing' (default, subject to quiet hours) or 'transactional' (24/7)",
314
+ )
315
+
316
+ class Config:
317
+ populate_by_name = True
318
+
319
+
320
+ class BatchMessageResult(BaseModel):
321
+ """Result for a single message in a batch"""
322
+
323
+ id: Optional[str] = Field(default=None, description="Message ID (if successful)")
324
+ to: str = Field(..., description="Destination phone number")
325
+ status: Literal["queued", "failed"] = Field(..., description="Status")
326
+ error: Optional[str] = Field(default=None, description="Error message (if failed)")
327
+
328
+
329
+ class BatchMessageResponse(BaseModel):
330
+ """Response from sending batch messages"""
331
+
332
+ batch_id: str = Field(..., alias="batchId", description="Unique batch identifier")
333
+ status: BatchStatus = Field(..., description="Current batch status")
334
+ total: int = Field(..., description="Total number of messages")
335
+ queued: int = Field(..., description="Messages queued successfully")
336
+ sent: int = Field(..., description="Messages sent")
337
+ failed: int = Field(..., description="Messages that failed")
338
+ credits_used: int = Field(..., alias="creditsUsed", description="Total credits used")
339
+ messages: List[BatchMessageResult] = Field(..., description="Individual message results")
340
+ created_at: str = Field(..., alias="createdAt", description="Creation timestamp")
341
+ completed_at: Optional[str] = Field(
342
+ default=None, alias="completedAt", description="Completion timestamp"
343
+ )
344
+
345
+ class Config:
346
+ populate_by_name = True
347
+
348
+
349
+ class BatchListResponse(BaseModel):
350
+ """Response from listing batches"""
351
+
352
+ data: List[BatchMessageResponse] = Field(..., description="List of batches")
353
+ count: int = Field(..., description="Total count")
354
+
355
+
356
+ class ListBatchesOptions(BaseModel):
357
+ """Options for listing batches"""
358
+
359
+ limit: Optional[int] = Field(
360
+ default=50,
361
+ ge=1,
362
+ le=100,
363
+ description="Maximum number of batches to return",
364
+ )
365
+ offset: Optional[int] = Field(
366
+ default=0,
367
+ ge=0,
368
+ description="Number of batches to skip",
369
+ )
370
+ status: Optional[BatchStatus] = Field(
371
+ default=None,
372
+ description="Filter by status",
373
+ )
374
+
375
+
376
+ # ============================================================================
377
+ # Errors
378
+ # ============================================================================
379
+
380
+
381
+ class ApiErrorResponse(BaseModel):
382
+ """Error response from the API"""
383
+
384
+ error: str = Field(..., description="Error code")
385
+ message: str = Field(..., description="Error message")
386
+ credits_needed: Optional[int] = Field(
387
+ default=None, alias="creditsNeeded", description="Credits needed"
388
+ )
389
+ current_balance: Optional[int] = Field(
390
+ default=None, alias="currentBalance", description="Current balance"
391
+ )
392
+ retry_after: Optional[int] = Field(
393
+ default=None, alias="retryAfter", description="Seconds to wait"
394
+ )
395
+
396
+ class Config:
397
+ populate_by_name = True
398
+ extra = "allow"
399
+
400
+
401
+ # ============================================================================
402
+ # Rate Limiting
403
+ # ============================================================================
404
+
405
+
406
+ class RateLimitInfo(BaseModel):
407
+ """Rate limit information from response headers"""
408
+
409
+ limit: int = Field(..., description="Max requests per window")
410
+ remaining: int = Field(..., description="Remaining requests")
411
+ reset: int = Field(..., description="Seconds until reset")
412
+
413
+
414
+ # ============================================================================
415
+ # Constants
416
+ # ============================================================================
417
+
418
+ # Credits per SMS by tier
419
+ CREDITS_PER_SMS: Dict[PricingTier, int] = {
420
+ PricingTier.DOMESTIC: 1,
421
+ PricingTier.TIER1: 8,
422
+ PricingTier.TIER2: 12,
423
+ PricingTier.TIER3: 16,
424
+ }
425
+
426
+ # Supported countries by tier
427
+ SUPPORTED_COUNTRIES: Dict[PricingTier, List[str]] = {
428
+ PricingTier.DOMESTIC: ["US", "CA"],
429
+ PricingTier.TIER1: ["GB", "PL", "PT", "RO", "CZ", "HU", "CN", "KR", "IN", "PH", "TH", "VN"],
430
+ PricingTier.TIER2: [
431
+ "FR",
432
+ "ES",
433
+ "SE",
434
+ "NO",
435
+ "DK",
436
+ "FI",
437
+ "IE",
438
+ "JP",
439
+ "AU",
440
+ "NZ",
441
+ "SG",
442
+ "HK",
443
+ "MY",
444
+ "ID",
445
+ "BR",
446
+ "AR",
447
+ "CL",
448
+ "CO",
449
+ "ZA",
450
+ "GR",
451
+ ],
452
+ PricingTier.TIER3: [
453
+ "DE",
454
+ "IT",
455
+ "NL",
456
+ "BE",
457
+ "AT",
458
+ "CH",
459
+ "MX",
460
+ "IL",
461
+ "AE",
462
+ "SA",
463
+ "EG",
464
+ "NG",
465
+ "KE",
466
+ "TW",
467
+ "PK",
468
+ "TR",
469
+ ],
470
+ }
471
+
472
+ # All supported country codes
473
+ ALL_SUPPORTED_COUNTRIES: List[str] = [
474
+ country for countries in SUPPORTED_COUNTRIES.values() for country in countries
475
+ ]
476
+
477
+
478
+ # ============================================================================
479
+ # Sandbox Test Numbers
480
+ # ============================================================================
481
+
482
+
483
+ class SandboxTestNumbers:
484
+ """Test phone numbers for sandbox mode.
485
+ Use these with test API keys (sk_test_*) to simulate different scenarios.
486
+ """
487
+
488
+ SUCCESS = "+15005550000" # Always succeeds - any number not in error list succeeds
489
+ INVALID = "+15005550001" # Fails with invalid_number error
490
+ UNROUTABLE = "+15005550002" # Fails with unroutable destination error
491
+ QUEUE_FULL = "+15005550003" # Fails with queue_full error
492
+ RATE_LIMITED = "+15005550004" # Fails with rate_limit_exceeded error
493
+ CARRIER_VIOLATION = "+15005550006" # Fails with carrier_violation error
494
+
495
+
496
+ SANDBOX_TEST_NUMBERS = SandboxTestNumbers()
497
+
498
+
499
+ # ============================================================================
500
+ # Webhooks
501
+ # ============================================================================
502
+
503
+
504
+ class WebhookEventType(str, Enum):
505
+ """Webhook event types"""
506
+
507
+ MESSAGE_SENT = "message.sent"
508
+ MESSAGE_DELIVERED = "message.delivered"
509
+ MESSAGE_FAILED = "message.failed"
510
+ MESSAGE_BOUNCED = "message.bounced"
511
+
512
+
513
+ class WebhookMode(str, Enum):
514
+ """Webhook event mode filter"""
515
+
516
+ ALL = "all" # Receive both test and live events
517
+ TEST = "test" # Only sandbox/test events
518
+ LIVE = "live" # Only production events (requires verification)
519
+
520
+
521
+ class CircuitState(str, Enum):
522
+ """Circuit breaker state for webhook delivery"""
523
+
524
+ CLOSED = "closed"
525
+ OPEN = "open"
526
+ HALF_OPEN = "half_open"
527
+
528
+
529
+ class DeliveryStatus(str, Enum):
530
+ """Webhook delivery status"""
531
+
532
+ PENDING = "pending"
533
+ DELIVERED = "delivered"
534
+ FAILED = "failed"
535
+ CANCELLED = "cancelled"
536
+
537
+
538
+ class Webhook(BaseModel):
539
+ """A configured webhook endpoint"""
540
+
541
+ id: str = Field(..., description="Unique webhook identifier (whk_xxx)")
542
+ url: str = Field(..., description="HTTPS endpoint URL")
543
+ events: List[str] = Field(..., description="Event types subscribed to")
544
+ description: Optional[str] = Field(default=None, description="Optional description")
545
+ mode: WebhookMode = Field(default=WebhookMode.ALL, description="Event mode filter")
546
+ is_active: bool = Field(..., alias="isActive", description="Whether webhook is active")
547
+ failure_count: int = Field(default=0, alias="failureCount", description="Consecutive failures")
548
+ last_failure_at: Optional[str] = Field(
549
+ default=None, alias="lastFailureAt", description="Last failure timestamp"
550
+ )
551
+ circuit_state: CircuitState = Field(
552
+ default=CircuitState.CLOSED, alias="circuitState", description="Circuit breaker state"
553
+ )
554
+ circuit_opened_at: Optional[str] = Field(
555
+ default=None, alias="circuitOpenedAt", description="When circuit was opened"
556
+ )
557
+ api_version: str = Field(default="2024-01", alias="apiVersion", description="API version")
558
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Custom metadata")
559
+ created_at: str = Field(..., alias="createdAt", description="Creation timestamp")
560
+ updated_at: str = Field(..., alias="updatedAt", description="Last update timestamp")
561
+ total_deliveries: int = Field(default=0, alias="totalDeliveries", description="Total attempts")
562
+ successful_deliveries: int = Field(
563
+ default=0, alias="successfulDeliveries", description="Successful deliveries"
564
+ )
565
+ success_rate: float = Field(default=0, alias="successRate", description="Success rate (0-100)")
566
+ last_delivery_at: Optional[str] = Field(
567
+ default=None, alias="lastDeliveryAt", description="Last successful delivery"
568
+ )
569
+
570
+ class Config:
571
+ populate_by_name = True
572
+
573
+
574
+ class WebhookCreatedResponse(Webhook):
575
+ """Response when creating a webhook (includes secret once)"""
576
+
577
+ secret: str = Field(..., description="Webhook signing secret - only shown once!")
578
+
579
+
580
+ class CreateWebhookOptions(BaseModel):
581
+ """Options for creating a webhook"""
582
+
583
+ url: str = Field(..., description="HTTPS endpoint URL")
584
+ events: List[str] = Field(..., description="Event types to subscribe to")
585
+ description: Optional[str] = Field(default=None, description="Optional description")
586
+ mode: Optional[WebhookMode] = Field(
587
+ default=None, description="Event mode filter (all, test, live)"
588
+ )
589
+ metadata: Optional[Dict[str, Any]] = Field(default=None, description="Custom metadata")
590
+
591
+
592
+ class UpdateWebhookOptions(BaseModel):
593
+ """Options for updating a webhook"""
594
+
595
+ url: Optional[str] = Field(default=None, description="New URL")
596
+ events: Optional[List[str]] = Field(default=None, description="New event subscriptions")
597
+ description: Optional[str] = Field(default=None, description="New description")
598
+ is_active: Optional[bool] = Field(default=None, alias="isActive", description="Enable/disable")
599
+ mode: Optional[WebhookMode] = Field(
600
+ default=None, description="Event mode filter (all, test, live)"
601
+ )
602
+ metadata: Optional[Dict[str, Any]] = Field(default=None, description="Custom metadata")
603
+
604
+ class Config:
605
+ populate_by_name = True
606
+
607
+
608
+ class WebhookDelivery(BaseModel):
609
+ """A webhook delivery attempt"""
610
+
611
+ id: str = Field(..., description="Unique delivery identifier (del_xxx)")
612
+ webhook_id: str = Field(..., alias="webhookId", description="Webhook ID")
613
+ event_id: str = Field(..., alias="eventId", description="Event ID for idempotency")
614
+ event_type: str = Field(..., alias="eventType", description="Event type")
615
+ attempt_number: int = Field(..., alias="attemptNumber", description="Attempt number (1-6)")
616
+ max_attempts: int = Field(..., alias="maxAttempts", description="Maximum attempts")
617
+ status: DeliveryStatus = Field(..., description="Delivery status")
618
+ response_status_code: Optional[int] = Field(
619
+ default=None, alias="responseStatusCode", description="HTTP status code"
620
+ )
621
+ response_time_ms: Optional[int] = Field(
622
+ default=None, alias="responseTimeMs", description="Response time in ms"
623
+ )
624
+ error_message: Optional[str] = Field(
625
+ default=None, alias="errorMessage", description="Error message"
626
+ )
627
+ error_code: Optional[str] = Field(default=None, alias="errorCode", description="Error code")
628
+ next_retry_at: Optional[str] = Field(
629
+ default=None, alias="nextRetryAt", description="Next retry time"
630
+ )
631
+ created_at: str = Field(..., alias="createdAt", description="Creation timestamp")
632
+ delivered_at: Optional[str] = Field(
633
+ default=None, alias="deliveredAt", description="Delivery timestamp"
634
+ )
635
+
636
+ class Config:
637
+ populate_by_name = True
638
+
639
+
640
+ class WebhookTestResult(BaseModel):
641
+ """Response from testing a webhook"""
642
+
643
+ success: bool = Field(..., description="Whether test was successful")
644
+ status_code: Optional[int] = Field(
645
+ default=None, alias="statusCode", description="HTTP status code"
646
+ )
647
+ response_time_ms: Optional[int] = Field(
648
+ default=None, alias="responseTimeMs", description="Response time in ms"
649
+ )
650
+ error: Optional[str] = Field(default=None, description="Error message if failed")
651
+
652
+ class Config:
653
+ populate_by_name = True
654
+
655
+
656
+ class WebhookSecretRotation(BaseModel):
657
+ """Response from rotating webhook secret"""
658
+
659
+ webhook: Webhook = Field(..., description="The webhook")
660
+ new_secret: str = Field(..., alias="newSecret", description="New signing secret")
661
+ old_secret_expires_at: str = Field(
662
+ ..., alias="oldSecretExpiresAt", description="When old secret expires"
663
+ )
664
+ message: str = Field(..., description="Message about grace period")
665
+
666
+ class Config:
667
+ populate_by_name = True
668
+
669
+
670
+ # ============================================================================
671
+ # Account & Credits
672
+ # ============================================================================
673
+
674
+
675
+ class Account(BaseModel):
676
+ """Account information"""
677
+
678
+ id: str = Field(..., description="User ID")
679
+ email: str = Field(..., description="Email address")
680
+ name: Optional[str] = Field(default=None, description="Display name")
681
+ created_at: str = Field(..., alias="createdAt", description="Account creation date")
682
+
683
+ class Config:
684
+ populate_by_name = True
685
+
686
+
687
+ class Credits(BaseModel):
688
+ """Credit balance information"""
689
+
690
+ balance: int = Field(..., description="Available credit balance")
691
+ reserved_balance: int = Field(
692
+ default=0, alias="reservedBalance", description="Credits reserved for scheduled messages"
693
+ )
694
+ available_balance: int = Field(
695
+ default=0, alias="availableBalance", description="Total usable credits"
696
+ )
697
+
698
+ class Config:
699
+ populate_by_name = True
700
+
701
+
702
+ class TransactionType(str, Enum):
703
+ """Credit transaction type"""
704
+
705
+ PURCHASE = "purchase"
706
+ USAGE = "usage"
707
+ REFUND = "refund"
708
+ ADJUSTMENT = "adjustment"
709
+ BONUS = "bonus"
710
+
711
+
712
+ class CreditTransaction(BaseModel):
713
+ """A credit transaction record"""
714
+
715
+ id: str = Field(..., description="Transaction ID")
716
+ type: TransactionType = Field(..., description="Transaction type")
717
+ amount: int = Field(..., description="Amount (positive for in, negative for out)")
718
+ balance_after: int = Field(..., alias="balanceAfter", description="Balance after transaction")
719
+ description: str = Field(..., description="Transaction description")
720
+ message_id: Optional[str] = Field(
721
+ default=None, alias="messageId", description="Related message ID"
722
+ )
723
+ created_at: str = Field(..., alias="createdAt", description="Transaction timestamp")
724
+
725
+ class Config:
726
+ populate_by_name = True
727
+
728
+
729
+ class ApiKey(BaseModel):
730
+ """An API key"""
731
+
732
+ id: str = Field(..., description="Key ID")
733
+ name: str = Field(..., description="Key name/label")
734
+ type: Literal["test", "live"] = Field(..., description="Key type")
735
+ prefix: str = Field(..., description="Key prefix for identification")
736
+ last_four: str = Field(..., alias="lastFour", description="Last 4 characters")
737
+ permissions: List[str] = Field(default_factory=list, description="Permissions granted")
738
+ created_at: str = Field(..., alias="createdAt", description="Creation timestamp")
739
+ last_used_at: Optional[str] = Field(
740
+ default=None, alias="lastUsedAt", description="Last used timestamp"
741
+ )
742
+ expires_at: Optional[str] = Field(
743
+ default=None, alias="expiresAt", description="Expiration timestamp"
744
+ )
745
+ is_revoked: bool = Field(default=False, alias="isRevoked", description="Whether revoked")
746
+
747
+ class Config:
748
+ populate_by_name = True