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/__init__.py +165 -0
- sendly/client.py +248 -0
- sendly/errors.py +169 -0
- sendly/resources/__init__.py +5 -0
- sendly/resources/account.py +264 -0
- sendly/resources/messages.py +1087 -0
- sendly/resources/webhooks.py +435 -0
- sendly/types.py +748 -0
- sendly/utils/__init__.py +26 -0
- sendly/utils/http.py +358 -0
- sendly/utils/validation.py +248 -0
- sendly/webhooks.py +245 -0
- sendly-3.8.1.dist-info/METADATA +589 -0
- sendly-3.8.1.dist-info/RECORD +15 -0
- sendly-3.8.1.dist-info/WHEEL +4 -0
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
|