sendly 3.34.0__tar.gz → 3.37.0__tar.gz
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-3.34.0 → sendly-3.37.0}/PKG-INFO +160 -1
- {sendly-3.34.0 → sendly-3.37.0}/README.md +159 -0
- {sendly-3.34.0 → sendly-3.37.0}/pyproject.toml +1 -1
- {sendly-3.34.0 → sendly-3.37.0}/sendly/__init__.py +54 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/client.py +6 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/__init__.py +6 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/account.py +69 -0
- sendly-3.37.0/sendly/resources/links.py +183 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/messages.py +271 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/numbers.py +122 -0
- sendly-3.37.0/sendly/resources/tendlc.py +527 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/types.py +370 -0
- sendly-3.37.0/tests/test_tendlc.py +596 -0
- {sendly-3.34.0 → sendly-3.37.0}/.github/header.svg +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/.gitignore +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/CHANGELOG.md +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/examples/async_example.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/examples/error_handling.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/examples/send_sms.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/errors.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/business_upgrade.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/campaigns.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/contacts.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/conversations.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/drafts.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/enterprise.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/labels.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/media.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/rules.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/templates.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/verify.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/webhooks.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/__init__.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/http.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/validation.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/sendly/webhooks.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/conftest.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_async_client.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_client.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_errors.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_http_body_kwarg.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages.py.bak +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages_batch.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages_schedule.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_numbers.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_validation.py +0 -0
- {sendly-3.34.0 → sendly-3.37.0}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sendly
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.37.0
|
|
4
4
|
Summary: Official Sendly Python SDK for SMS messaging
|
|
5
5
|
Project-URL: Homepage, https://sendly.live
|
|
6
6
|
Project-URL: Documentation, https://sendly.live/docs
|
|
@@ -402,6 +402,13 @@ print(f"Credits used: {usage['creditsUsed']}")
|
|
|
402
402
|
result = client.account.create_api_key('Production Key')
|
|
403
403
|
print(f"New key: {result['key']}") # Only shown once!
|
|
404
404
|
|
|
405
|
+
# Rotate an API key (old key keeps working for a 24h grace period by default)
|
|
406
|
+
rotation = client.account.rotate_api_key('key_xxx')
|
|
407
|
+
print(f"New key: {rotation['newKey']['key']}") # Only shown once!
|
|
408
|
+
|
|
409
|
+
# Rotate with a wider overlap window (24-168 hours)
|
|
410
|
+
rotation = client.account.rotate_api_key('key_xxx', grace_period_hours=72)
|
|
411
|
+
|
|
405
412
|
# Revoke an API key
|
|
406
413
|
client.account.revoke_api_key('key_xxx')
|
|
407
414
|
```
|
|
@@ -431,6 +438,23 @@ owned = client.numbers.list()
|
|
|
431
438
|
for num in owned.numbers:
|
|
432
439
|
print(f'{num.phone_number} ({num.status})')
|
|
433
440
|
|
|
441
|
+
# Get one owned number (includes whether it's your default sender)
|
|
442
|
+
number = client.numbers.get('num_xxx')
|
|
443
|
+
print(f'{number.phone_number} default={number.is_default}')
|
|
444
|
+
|
|
445
|
+
# Make a number your default sender
|
|
446
|
+
client.numbers.update('num_xxx', is_default=True)
|
|
447
|
+
|
|
448
|
+
# Cancel a scheduled release and keep the number
|
|
449
|
+
client.numbers.update('num_xxx', pending_cancellation=False)
|
|
450
|
+
|
|
451
|
+
# Release a number (a paid number is kept until the end of the billed period)
|
|
452
|
+
result = client.numbers.release('num_xxx')
|
|
453
|
+
if result.scheduled:
|
|
454
|
+
print(f'Releases at {result.scheduled_release_at}')
|
|
455
|
+
else:
|
|
456
|
+
print('Released')
|
|
457
|
+
|
|
434
458
|
# Buy a number
|
|
435
459
|
chosen = available.numbers[0]
|
|
436
460
|
result = client.numbers.buy(
|
|
@@ -457,6 +481,103 @@ elif result.status in ('documents_required', 'payment_required'):
|
|
|
457
481
|
)
|
|
458
482
|
```
|
|
459
483
|
|
|
484
|
+
## 10DLC (Local Number Texting)
|
|
485
|
+
|
|
486
|
+
Register your business for carrier review so you can text from local
|
|
487
|
+
(10-digit) US numbers. Requires an API key with the `tendlc:read` /
|
|
488
|
+
`tendlc:write` scopes; writes need a live key. The flow has three steps:
|
|
489
|
+
brand → campaign → assign a number.
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
# 1. Register a brand and poll until it's verified
|
|
493
|
+
brand = client.ten_dlc.create_brand(
|
|
494
|
+
legal_name='Acme Holdings LLC',
|
|
495
|
+
ein='12-3456789',
|
|
496
|
+
website='https://acme.example',
|
|
497
|
+
email='ops@acme.example',
|
|
498
|
+
).data
|
|
499
|
+
# ...poll client.ten_dlc.get_brand(brand.id) until brand.status == 'verified'
|
|
500
|
+
# (or 'failed', with brand.failure_reasons explaining why)
|
|
501
|
+
|
|
502
|
+
# 2. Pre-check the use case, then create a campaign
|
|
503
|
+
check = client.ten_dlc.qualify(brand.id, 'MIXED').data
|
|
504
|
+
if check.qualified:
|
|
505
|
+
campaign = client.ten_dlc.create_campaign(
|
|
506
|
+
brand_id=brand.id,
|
|
507
|
+
use_case='MIXED',
|
|
508
|
+
description='Order updates and support replies for Acme customers',
|
|
509
|
+
message_flow='Customers opt in at checkout on acme.example',
|
|
510
|
+
sample_messages=['Your order #123 has shipped!'],
|
|
511
|
+
opt_out_keywords='STOP',
|
|
512
|
+
).data
|
|
513
|
+
# ...poll client.ten_dlc.get_campaign(campaign.id) until status == 'active'
|
|
514
|
+
|
|
515
|
+
# 3. Assign a number you own; it can send once the assignment is 'Active'
|
|
516
|
+
assignment = client.ten_dlc.assign_number(campaign.id, '+15551234567').data
|
|
517
|
+
print(assignment.status)
|
|
518
|
+
|
|
519
|
+
# List what you have registered
|
|
520
|
+
brands = client.ten_dlc.list_brands()
|
|
521
|
+
campaigns = client.ten_dlc.list_campaigns()
|
|
522
|
+
assignments = client.ten_dlc.list_assignments()
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Group MMS
|
|
526
|
+
|
|
527
|
+
Send one message to 2-8 US/Canada recipients who share a single thread; replies
|
|
528
|
+
fan out to everyone. Provide `text`, `media_urls`, or both. Requires the
|
|
529
|
+
`group_mms` feature.
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
group = client.messages.send_group(
|
|
533
|
+
to=['+14155551234', '+14155555678'],
|
|
534
|
+
text='Dinner at 7?'
|
|
535
|
+
)
|
|
536
|
+
print(f'{group.id}: {group.status}') # 'sent' (or 'delivered' on a test key)
|
|
537
|
+
print(group.group_message_id) # stable group thread id, when available
|
|
538
|
+
|
|
539
|
+
# With media
|
|
540
|
+
client.messages.send_group(
|
|
541
|
+
to=['+14155551234', '+14155555678'],
|
|
542
|
+
text='Here is the menu',
|
|
543
|
+
media_urls=['https://example.com/menu.jpg'],
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## AI Enhance
|
|
548
|
+
|
|
549
|
+
Polish message copy before sending. Pass `text` and/or a `message_type` to steer
|
|
550
|
+
the tone.
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
result = client.messages.enhance(text='ur order shipped', message_type='transactional')
|
|
554
|
+
print(result.enhanced) # cleaned-up copy
|
|
555
|
+
print(result.explanation) # why it changed
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Branded Links (URL Shortener)
|
|
559
|
+
|
|
560
|
+
Mint branded short links for a destination URL, list them with click analytics,
|
|
561
|
+
and flip a per-link kill switch. Branded, owned-domain links improve
|
|
562
|
+
deliverability and give you click data. Gated behind the `url_shortener` flag.
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
# Shorten a URL
|
|
566
|
+
link = client.links.create('https://example.com/spring-sale?utm_source=sms')
|
|
567
|
+
print(link.short_url) # https://sendly.live/l/Ab3xY7
|
|
568
|
+
print(link.code) # Ab3xY7
|
|
569
|
+
|
|
570
|
+
# List your links with click counts and a 14-day daily histogram
|
|
571
|
+
listing = client.links.list(limit=20)
|
|
572
|
+
print(f'{listing.total} links')
|
|
573
|
+
for lk in listing.links:
|
|
574
|
+
print(f'{lk.short_url} -> {lk.destination_url} ({lk.click_count} clicks)')
|
|
575
|
+
|
|
576
|
+
# Kill a link (its redirect 404s until re-enabled)
|
|
577
|
+
client.links.disable(link.code)
|
|
578
|
+
client.links.enable(link.code)
|
|
579
|
+
```
|
|
580
|
+
|
|
460
581
|
## Error Handling
|
|
461
582
|
|
|
462
583
|
The SDK provides typed exception classes:
|
|
@@ -663,6 +784,44 @@ List the numbers the account already owns.
|
|
|
663
784
|
|
|
664
785
|
Buy a number. Returns `status` of `provisioning`, `documents_required`, or `payment_required`; when documents/payment are required, `action` carries a hosted page URL + code to hand to the user before re-calling with `action_code`.
|
|
665
786
|
|
|
787
|
+
### `client.ten_dlc`
|
|
788
|
+
|
|
789
|
+
#### `list_brands() -> TenDlcBrandListResponse`
|
|
790
|
+
|
|
791
|
+
List the brands registered for carrier review.
|
|
792
|
+
|
|
793
|
+
#### `create_brand(legal_name, dba=None, ein=None, entity_type=None, ...) -> TenDlcBrandResponse`
|
|
794
|
+
|
|
795
|
+
Register a brand for carrier review — step 1 of enabling local-number texting. The brand starts `pending`; poll `get_brand()` until it becomes `verified`.
|
|
796
|
+
|
|
797
|
+
#### `get_brand(id) -> TenDlcBrandResponse`
|
|
798
|
+
|
|
799
|
+
Fetch one brand; also refreshes its carrier-review status, so polling shows progress (`pending` → `verified`/`failed`).
|
|
800
|
+
|
|
801
|
+
#### `qualify(brand_id, use_case) -> TenDlcQualifyResponse`
|
|
802
|
+
|
|
803
|
+
Pre-check whether a use case qualifies for a brand on the carrier network before creating a campaign.
|
|
804
|
+
|
|
805
|
+
#### `list_campaigns() -> TenDlcCampaignListResponse`
|
|
806
|
+
|
|
807
|
+
List your messaging campaigns.
|
|
808
|
+
|
|
809
|
+
#### `create_campaign(brand_id, use_case, description, message_flow, sample_messages, ...) -> TenDlcCampaignResponse`
|
|
810
|
+
|
|
811
|
+
Create a campaign under a verified brand and submit it for carrier review. Starts `pending`; poll `get_campaign()` until it becomes `active`.
|
|
812
|
+
|
|
813
|
+
#### `get_campaign(id) -> TenDlcCampaignResponse`
|
|
814
|
+
|
|
815
|
+
Fetch one campaign; also refreshes its carrier-review status, including throughput once carriers approve.
|
|
816
|
+
|
|
817
|
+
#### `assign_number(campaign_id, phone_number) -> TenDlcAssignmentResponse`
|
|
818
|
+
|
|
819
|
+
Assign a number you own to an active campaign, making the number sendable. Idempotent — re-assigning the same number to the same campaign returns the existing assignment.
|
|
820
|
+
|
|
821
|
+
#### `list_assignments() -> TenDlcAssignmentListResponse`
|
|
822
|
+
|
|
823
|
+
List your number-to-campaign assignments.
|
|
824
|
+
|
|
666
825
|
## Enterprise
|
|
667
826
|
|
|
668
827
|
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (`sk_live_v1_master_*`).
|
|
@@ -367,6 +367,13 @@ print(f"Credits used: {usage['creditsUsed']}")
|
|
|
367
367
|
result = client.account.create_api_key('Production Key')
|
|
368
368
|
print(f"New key: {result['key']}") # Only shown once!
|
|
369
369
|
|
|
370
|
+
# Rotate an API key (old key keeps working for a 24h grace period by default)
|
|
371
|
+
rotation = client.account.rotate_api_key('key_xxx')
|
|
372
|
+
print(f"New key: {rotation['newKey']['key']}") # Only shown once!
|
|
373
|
+
|
|
374
|
+
# Rotate with a wider overlap window (24-168 hours)
|
|
375
|
+
rotation = client.account.rotate_api_key('key_xxx', grace_period_hours=72)
|
|
376
|
+
|
|
370
377
|
# Revoke an API key
|
|
371
378
|
client.account.revoke_api_key('key_xxx')
|
|
372
379
|
```
|
|
@@ -396,6 +403,23 @@ owned = client.numbers.list()
|
|
|
396
403
|
for num in owned.numbers:
|
|
397
404
|
print(f'{num.phone_number} ({num.status})')
|
|
398
405
|
|
|
406
|
+
# Get one owned number (includes whether it's your default sender)
|
|
407
|
+
number = client.numbers.get('num_xxx')
|
|
408
|
+
print(f'{number.phone_number} default={number.is_default}')
|
|
409
|
+
|
|
410
|
+
# Make a number your default sender
|
|
411
|
+
client.numbers.update('num_xxx', is_default=True)
|
|
412
|
+
|
|
413
|
+
# Cancel a scheduled release and keep the number
|
|
414
|
+
client.numbers.update('num_xxx', pending_cancellation=False)
|
|
415
|
+
|
|
416
|
+
# Release a number (a paid number is kept until the end of the billed period)
|
|
417
|
+
result = client.numbers.release('num_xxx')
|
|
418
|
+
if result.scheduled:
|
|
419
|
+
print(f'Releases at {result.scheduled_release_at}')
|
|
420
|
+
else:
|
|
421
|
+
print('Released')
|
|
422
|
+
|
|
399
423
|
# Buy a number
|
|
400
424
|
chosen = available.numbers[0]
|
|
401
425
|
result = client.numbers.buy(
|
|
@@ -422,6 +446,103 @@ elif result.status in ('documents_required', 'payment_required'):
|
|
|
422
446
|
)
|
|
423
447
|
```
|
|
424
448
|
|
|
449
|
+
## 10DLC (Local Number Texting)
|
|
450
|
+
|
|
451
|
+
Register your business for carrier review so you can text from local
|
|
452
|
+
(10-digit) US numbers. Requires an API key with the `tendlc:read` /
|
|
453
|
+
`tendlc:write` scopes; writes need a live key. The flow has three steps:
|
|
454
|
+
brand → campaign → assign a number.
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
# 1. Register a brand and poll until it's verified
|
|
458
|
+
brand = client.ten_dlc.create_brand(
|
|
459
|
+
legal_name='Acme Holdings LLC',
|
|
460
|
+
ein='12-3456789',
|
|
461
|
+
website='https://acme.example',
|
|
462
|
+
email='ops@acme.example',
|
|
463
|
+
).data
|
|
464
|
+
# ...poll client.ten_dlc.get_brand(brand.id) until brand.status == 'verified'
|
|
465
|
+
# (or 'failed', with brand.failure_reasons explaining why)
|
|
466
|
+
|
|
467
|
+
# 2. Pre-check the use case, then create a campaign
|
|
468
|
+
check = client.ten_dlc.qualify(brand.id, 'MIXED').data
|
|
469
|
+
if check.qualified:
|
|
470
|
+
campaign = client.ten_dlc.create_campaign(
|
|
471
|
+
brand_id=brand.id,
|
|
472
|
+
use_case='MIXED',
|
|
473
|
+
description='Order updates and support replies for Acme customers',
|
|
474
|
+
message_flow='Customers opt in at checkout on acme.example',
|
|
475
|
+
sample_messages=['Your order #123 has shipped!'],
|
|
476
|
+
opt_out_keywords='STOP',
|
|
477
|
+
).data
|
|
478
|
+
# ...poll client.ten_dlc.get_campaign(campaign.id) until status == 'active'
|
|
479
|
+
|
|
480
|
+
# 3. Assign a number you own; it can send once the assignment is 'Active'
|
|
481
|
+
assignment = client.ten_dlc.assign_number(campaign.id, '+15551234567').data
|
|
482
|
+
print(assignment.status)
|
|
483
|
+
|
|
484
|
+
# List what you have registered
|
|
485
|
+
brands = client.ten_dlc.list_brands()
|
|
486
|
+
campaigns = client.ten_dlc.list_campaigns()
|
|
487
|
+
assignments = client.ten_dlc.list_assignments()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Group MMS
|
|
491
|
+
|
|
492
|
+
Send one message to 2-8 US/Canada recipients who share a single thread; replies
|
|
493
|
+
fan out to everyone. Provide `text`, `media_urls`, or both. Requires the
|
|
494
|
+
`group_mms` feature.
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
group = client.messages.send_group(
|
|
498
|
+
to=['+14155551234', '+14155555678'],
|
|
499
|
+
text='Dinner at 7?'
|
|
500
|
+
)
|
|
501
|
+
print(f'{group.id}: {group.status}') # 'sent' (or 'delivered' on a test key)
|
|
502
|
+
print(group.group_message_id) # stable group thread id, when available
|
|
503
|
+
|
|
504
|
+
# With media
|
|
505
|
+
client.messages.send_group(
|
|
506
|
+
to=['+14155551234', '+14155555678'],
|
|
507
|
+
text='Here is the menu',
|
|
508
|
+
media_urls=['https://example.com/menu.jpg'],
|
|
509
|
+
)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## AI Enhance
|
|
513
|
+
|
|
514
|
+
Polish message copy before sending. Pass `text` and/or a `message_type` to steer
|
|
515
|
+
the tone.
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
result = client.messages.enhance(text='ur order shipped', message_type='transactional')
|
|
519
|
+
print(result.enhanced) # cleaned-up copy
|
|
520
|
+
print(result.explanation) # why it changed
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Branded Links (URL Shortener)
|
|
524
|
+
|
|
525
|
+
Mint branded short links for a destination URL, list them with click analytics,
|
|
526
|
+
and flip a per-link kill switch. Branded, owned-domain links improve
|
|
527
|
+
deliverability and give you click data. Gated behind the `url_shortener` flag.
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
# Shorten a URL
|
|
531
|
+
link = client.links.create('https://example.com/spring-sale?utm_source=sms')
|
|
532
|
+
print(link.short_url) # https://sendly.live/l/Ab3xY7
|
|
533
|
+
print(link.code) # Ab3xY7
|
|
534
|
+
|
|
535
|
+
# List your links with click counts and a 14-day daily histogram
|
|
536
|
+
listing = client.links.list(limit=20)
|
|
537
|
+
print(f'{listing.total} links')
|
|
538
|
+
for lk in listing.links:
|
|
539
|
+
print(f'{lk.short_url} -> {lk.destination_url} ({lk.click_count} clicks)')
|
|
540
|
+
|
|
541
|
+
# Kill a link (its redirect 404s until re-enabled)
|
|
542
|
+
client.links.disable(link.code)
|
|
543
|
+
client.links.enable(link.code)
|
|
544
|
+
```
|
|
545
|
+
|
|
425
546
|
## Error Handling
|
|
426
547
|
|
|
427
548
|
The SDK provides typed exception classes:
|
|
@@ -628,6 +749,44 @@ List the numbers the account already owns.
|
|
|
628
749
|
|
|
629
750
|
Buy a number. Returns `status` of `provisioning`, `documents_required`, or `payment_required`; when documents/payment are required, `action` carries a hosted page URL + code to hand to the user before re-calling with `action_code`.
|
|
630
751
|
|
|
752
|
+
### `client.ten_dlc`
|
|
753
|
+
|
|
754
|
+
#### `list_brands() -> TenDlcBrandListResponse`
|
|
755
|
+
|
|
756
|
+
List the brands registered for carrier review.
|
|
757
|
+
|
|
758
|
+
#### `create_brand(legal_name, dba=None, ein=None, entity_type=None, ...) -> TenDlcBrandResponse`
|
|
759
|
+
|
|
760
|
+
Register a brand for carrier review — step 1 of enabling local-number texting. The brand starts `pending`; poll `get_brand()` until it becomes `verified`.
|
|
761
|
+
|
|
762
|
+
#### `get_brand(id) -> TenDlcBrandResponse`
|
|
763
|
+
|
|
764
|
+
Fetch one brand; also refreshes its carrier-review status, so polling shows progress (`pending` → `verified`/`failed`).
|
|
765
|
+
|
|
766
|
+
#### `qualify(brand_id, use_case) -> TenDlcQualifyResponse`
|
|
767
|
+
|
|
768
|
+
Pre-check whether a use case qualifies for a brand on the carrier network before creating a campaign.
|
|
769
|
+
|
|
770
|
+
#### `list_campaigns() -> TenDlcCampaignListResponse`
|
|
771
|
+
|
|
772
|
+
List your messaging campaigns.
|
|
773
|
+
|
|
774
|
+
#### `create_campaign(brand_id, use_case, description, message_flow, sample_messages, ...) -> TenDlcCampaignResponse`
|
|
775
|
+
|
|
776
|
+
Create a campaign under a verified brand and submit it for carrier review. Starts `pending`; poll `get_campaign()` until it becomes `active`.
|
|
777
|
+
|
|
778
|
+
#### `get_campaign(id) -> TenDlcCampaignResponse`
|
|
779
|
+
|
|
780
|
+
Fetch one campaign; also refreshes its carrier-review status, including throughput once carriers approve.
|
|
781
|
+
|
|
782
|
+
#### `assign_number(campaign_id, phone_number) -> TenDlcAssignmentResponse`
|
|
783
|
+
|
|
784
|
+
Assign a number you own to an active campaign, making the number sendable. Idempotent — re-assigning the same number to the same campaign returns the existing assignment.
|
|
785
|
+
|
|
786
|
+
#### `list_assignments() -> TenDlcAssignmentListResponse`
|
|
787
|
+
|
|
788
|
+
List your number-to-campaign assignments.
|
|
789
|
+
|
|
631
790
|
## Enterprise
|
|
632
791
|
|
|
633
792
|
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (`sk_live_v1_master_*`).
|
|
@@ -51,8 +51,10 @@ from .resources.business_upgrade import (
|
|
|
51
51
|
AsyncBusinessUpgradeResource,
|
|
52
52
|
BusinessUpgradeResource,
|
|
53
53
|
)
|
|
54
|
+
from .resources.links import AsyncLinksResource, LinksResource
|
|
54
55
|
from .resources.media import AsyncMediaResource, MediaResource
|
|
55
56
|
from .resources.numbers import AsyncNumbersResource, NumbersResource
|
|
57
|
+
from .resources.tendlc import AsyncTenDlcResource, TenDlcResource
|
|
56
58
|
|
|
57
59
|
# Types
|
|
58
60
|
from .types import (
|
|
@@ -126,6 +128,29 @@ from .types import (
|
|
|
126
128
|
NumberCountry,
|
|
127
129
|
OwnedNumber,
|
|
128
130
|
OwnedNumbersResponse,
|
|
131
|
+
ReleaseNumberResponse,
|
|
132
|
+
# Group MMS + AI types
|
|
133
|
+
EnhanceMessageResponse,
|
|
134
|
+
GroupMessageResponse,
|
|
135
|
+
SendGroupMessageRequest,
|
|
136
|
+
# URL shortener types
|
|
137
|
+
CreateShortLinkResponse,
|
|
138
|
+
ShortLink,
|
|
139
|
+
ShortLinkListResponse,
|
|
140
|
+
UpdateShortLinkResponse,
|
|
141
|
+
# 10DLC types
|
|
142
|
+
TenDlcAssignment,
|
|
143
|
+
TenDlcAssignmentListResponse,
|
|
144
|
+
TenDlcAssignmentResponse,
|
|
145
|
+
TenDlcBrand,
|
|
146
|
+
TenDlcBrandListResponse,
|
|
147
|
+
TenDlcBrandResponse,
|
|
148
|
+
TenDlcCampaign,
|
|
149
|
+
TenDlcCampaignListResponse,
|
|
150
|
+
TenDlcCampaignResponse,
|
|
151
|
+
TenDlcQualifyResponse,
|
|
152
|
+
TenDlcQualifyResult,
|
|
153
|
+
TenDlcThroughput,
|
|
129
154
|
OptInPage,
|
|
130
155
|
PricingTier,
|
|
131
156
|
QuotaSettings,
|
|
@@ -272,12 +297,41 @@ __all__ = [
|
|
|
272
297
|
"NumberCountry",
|
|
273
298
|
"OwnedNumber",
|
|
274
299
|
"OwnedNumbersResponse",
|
|
300
|
+
"ReleaseNumberResponse",
|
|
301
|
+
# Group MMS + AI types
|
|
302
|
+
"EnhanceMessageResponse",
|
|
303
|
+
"GroupMessageResponse",
|
|
304
|
+
"SendGroupMessageRequest",
|
|
305
|
+
# URL shortener types
|
|
306
|
+
"CreateShortLinkResponse",
|
|
307
|
+
"ShortLink",
|
|
308
|
+
"ShortLinkListResponse",
|
|
309
|
+
"UpdateShortLinkResponse",
|
|
310
|
+
# 10DLC types
|
|
311
|
+
"TenDlcAssignment",
|
|
312
|
+
"TenDlcAssignmentListResponse",
|
|
313
|
+
"TenDlcAssignmentResponse",
|
|
314
|
+
"TenDlcBrand",
|
|
315
|
+
"TenDlcBrandListResponse",
|
|
316
|
+
"TenDlcBrandResponse",
|
|
317
|
+
"TenDlcCampaign",
|
|
318
|
+
"TenDlcCampaignListResponse",
|
|
319
|
+
"TenDlcCampaignResponse",
|
|
320
|
+
"TenDlcQualifyResponse",
|
|
321
|
+
"TenDlcQualifyResult",
|
|
322
|
+
"TenDlcThroughput",
|
|
323
|
+
# Links resources
|
|
324
|
+
"LinksResource",
|
|
325
|
+
"AsyncLinksResource",
|
|
275
326
|
# Media resources
|
|
276
327
|
"MediaResource",
|
|
277
328
|
"AsyncMediaResource",
|
|
278
329
|
# Numbers resources
|
|
279
330
|
"NumbersResource",
|
|
280
331
|
"AsyncNumbersResource",
|
|
332
|
+
# 10DLC resources
|
|
333
|
+
"TenDlcResource",
|
|
334
|
+
"AsyncTenDlcResource",
|
|
281
335
|
# Business upgrade resources
|
|
282
336
|
"BusinessUpgradeResource",
|
|
283
337
|
"AsyncBusinessUpgradeResource",
|
|
@@ -17,11 +17,13 @@ from .resources.conversations import AsyncConversationsResource, ConversationsRe
|
|
|
17
17
|
from .resources.drafts import AsyncDraftsResource, DraftsResource
|
|
18
18
|
from .resources.enterprise import AsyncEnterpriseResource, EnterpriseResource
|
|
19
19
|
from .resources.labels import AsyncLabelsResource, LabelsResource
|
|
20
|
+
from .resources.links import AsyncLinksResource, LinksResource
|
|
20
21
|
from .resources.media import AsyncMediaResource, MediaResource
|
|
21
22
|
from .resources.messages import AsyncMessagesResource, MessagesResource
|
|
22
23
|
from .resources.numbers import AsyncNumbersResource, NumbersResource
|
|
23
24
|
from .resources.rules import AsyncRulesResource, RulesResource
|
|
24
25
|
from .resources.templates import AsyncTemplatesResource, TemplatesResource
|
|
26
|
+
from .resources.tendlc import AsyncTenDlcResource, TenDlcResource
|
|
25
27
|
from .resources.verify import AsyncVerifyResource, VerifyResource
|
|
26
28
|
from .resources.webhooks import AsyncWebhooksResource, WebhooksResource
|
|
27
29
|
from .types import RateLimitInfo, SendlyConfig
|
|
@@ -119,8 +121,10 @@ class Sendly:
|
|
|
119
121
|
self.contacts = ContactsResource(self._http)
|
|
120
122
|
self.media = MediaResource(self._http)
|
|
121
123
|
self.numbers = NumbersResource(self._http)
|
|
124
|
+
self.ten_dlc = TenDlcResource(self._http)
|
|
122
125
|
self.enterprise = EnterpriseResource(self._http)
|
|
123
126
|
self.labels = LabelsResource(self._http)
|
|
127
|
+
self.links = LinksResource(self._http)
|
|
124
128
|
self.drafts = DraftsResource(self._http)
|
|
125
129
|
self.rules = RulesResource(self._http)
|
|
126
130
|
self.business_upgrade = BusinessUpgradeResource(self._http)
|
|
@@ -258,8 +262,10 @@ class AsyncSendly:
|
|
|
258
262
|
self.contacts = AsyncContactsResource(self._http)
|
|
259
263
|
self.media = AsyncMediaResource(self._http)
|
|
260
264
|
self.numbers = AsyncNumbersResource(self._http)
|
|
265
|
+
self.ten_dlc = AsyncTenDlcResource(self._http)
|
|
261
266
|
self.enterprise = AsyncEnterpriseResource(self._http)
|
|
262
267
|
self.labels = AsyncLabelsResource(self._http)
|
|
268
|
+
self.links = AsyncLinksResource(self._http)
|
|
263
269
|
self.drafts = AsyncDraftsResource(self._http)
|
|
264
270
|
self.rules = AsyncRulesResource(self._http)
|
|
265
271
|
self.business_upgrade = AsyncBusinessUpgradeResource(self._http)
|
|
@@ -6,11 +6,13 @@ from .conversations import AsyncConversationsResource, ConversationsResource
|
|
|
6
6
|
from .drafts import AsyncDraftsResource, DraftsResource
|
|
7
7
|
from .enterprise import AsyncEnterpriseResource, EnterpriseResource
|
|
8
8
|
from .labels import AsyncLabelsResource, LabelsResource
|
|
9
|
+
from .links import AsyncLinksResource, LinksResource
|
|
9
10
|
from .media import AsyncMediaResource, MediaResource
|
|
10
11
|
from .messages import AsyncMessagesResource, MessagesResource
|
|
11
12
|
from .numbers import AsyncNumbersResource, NumbersResource
|
|
12
13
|
from .rules import AsyncRulesResource, RulesResource
|
|
13
14
|
from .templates import AsyncTemplatesResource, TemplatesResource
|
|
15
|
+
from .tendlc import AsyncTenDlcResource, TenDlcResource
|
|
14
16
|
from .verify import AsyncVerifyResource, VerifyResource
|
|
15
17
|
|
|
16
18
|
__all__ = [
|
|
@@ -26,6 +28,8 @@ __all__ = [
|
|
|
26
28
|
"AsyncEnterpriseResource",
|
|
27
29
|
"LabelsResource",
|
|
28
30
|
"AsyncLabelsResource",
|
|
31
|
+
"LinksResource",
|
|
32
|
+
"AsyncLinksResource",
|
|
29
33
|
"MediaResource",
|
|
30
34
|
"AsyncMediaResource",
|
|
31
35
|
"MessagesResource",
|
|
@@ -38,4 +42,6 @@ __all__ = [
|
|
|
38
42
|
"AsyncTemplatesResource",
|
|
39
43
|
"RulesResource",
|
|
40
44
|
"AsyncRulesResource",
|
|
45
|
+
"TenDlcResource",
|
|
46
|
+
"AsyncTenDlcResource",
|
|
41
47
|
]
|
|
@@ -192,6 +192,42 @@ class AccountResource:
|
|
|
192
192
|
|
|
193
193
|
self._http.request("DELETE", f"/account/keys/{key_id}")
|
|
194
194
|
|
|
195
|
+
def rotate_api_key(
|
|
196
|
+
self, key_id: str, grace_period_hours: Optional[int] = None
|
|
197
|
+
) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Rotate an API key.
|
|
200
|
+
|
|
201
|
+
Mints a replacement key and, by default, keeps the old key working for a
|
|
202
|
+
24-hour grace period so you can roll the new secret out before the old
|
|
203
|
+
one stops. Pass ``grace_period_hours`` (24-168) to widen the window, or
|
|
204
|
+
the caller can revoke the old key immediately once the new one is live.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
key_id: ID of the API key to rotate
|
|
208
|
+
grace_period_hours: Overlap window in hours (24-168, default 24)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dict with 'newKey' (metadata plus the full 'key' secret - only shown
|
|
212
|
+
once - and a 'warning'), 'oldKey' (the predecessor), and 'message'
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
>>> result = client.account.rotate_api_key('key_xxx')
|
|
216
|
+
>>> print(f"Save this key: {result['newKey']['key']}") # Only shown once!
|
|
217
|
+
"""
|
|
218
|
+
if not key_id:
|
|
219
|
+
raise ValueError("API key ID is required")
|
|
220
|
+
|
|
221
|
+
body: Dict[str, Any] = {}
|
|
222
|
+
if grace_period_hours is not None:
|
|
223
|
+
if not isinstance(grace_period_hours, int) or not (
|
|
224
|
+
24 <= grace_period_hours <= 168
|
|
225
|
+
):
|
|
226
|
+
raise ValueError("grace_period_hours must be between 24 and 168")
|
|
227
|
+
body["gracePeriodHours"] = grace_period_hours
|
|
228
|
+
|
|
229
|
+
return self._http.request("POST", f"/account/keys/{key_id}/rotate", body=body)
|
|
230
|
+
|
|
195
231
|
|
|
196
232
|
class AsyncAccountResource:
|
|
197
233
|
"""
|
|
@@ -284,3 +320,36 @@ class AsyncAccountResource:
|
|
|
284
320
|
raise ValueError("API key ID is required")
|
|
285
321
|
|
|
286
322
|
await self._http.request("DELETE", f"/account/keys/{key_id}")
|
|
323
|
+
|
|
324
|
+
async def rotate_api_key(
|
|
325
|
+
self, key_id: str, grace_period_hours: Optional[int] = None
|
|
326
|
+
) -> Dict[str, Any]:
|
|
327
|
+
"""
|
|
328
|
+
Rotate an API key (async).
|
|
329
|
+
|
|
330
|
+
Mints a replacement key and, by default, keeps the old key working for a
|
|
331
|
+
24-hour grace period. Pass ``grace_period_hours`` (24-168) to widen the
|
|
332
|
+
overlap window.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
key_id: ID of the API key to rotate
|
|
336
|
+
grace_period_hours: Overlap window in hours (24-168, default 24)
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict with 'newKey' (metadata plus the full 'key' secret - only shown
|
|
340
|
+
once - and a 'warning'), 'oldKey' (the predecessor), and 'message'
|
|
341
|
+
"""
|
|
342
|
+
if not key_id:
|
|
343
|
+
raise ValueError("API key ID is required")
|
|
344
|
+
|
|
345
|
+
body: Dict[str, Any] = {}
|
|
346
|
+
if grace_period_hours is not None:
|
|
347
|
+
if not isinstance(grace_period_hours, int) or not (
|
|
348
|
+
24 <= grace_period_hours <= 168
|
|
349
|
+
):
|
|
350
|
+
raise ValueError("grace_period_hours must be between 24 and 168")
|
|
351
|
+
body["gracePeriodHours"] = grace_period_hours
|
|
352
|
+
|
|
353
|
+
return await self._http.request(
|
|
354
|
+
"POST", f"/account/keys/{key_id}/rotate", body=body
|
|
355
|
+
)
|