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.
Files changed (48) hide show
  1. {sendly-3.34.0 → sendly-3.37.0}/PKG-INFO +160 -1
  2. {sendly-3.34.0 → sendly-3.37.0}/README.md +159 -0
  3. {sendly-3.34.0 → sendly-3.37.0}/pyproject.toml +1 -1
  4. {sendly-3.34.0 → sendly-3.37.0}/sendly/__init__.py +54 -0
  5. {sendly-3.34.0 → sendly-3.37.0}/sendly/client.py +6 -0
  6. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/__init__.py +6 -0
  7. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/account.py +69 -0
  8. sendly-3.37.0/sendly/resources/links.py +183 -0
  9. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/messages.py +271 -0
  10. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/numbers.py +122 -0
  11. sendly-3.37.0/sendly/resources/tendlc.py +527 -0
  12. {sendly-3.34.0 → sendly-3.37.0}/sendly/types.py +370 -0
  13. sendly-3.37.0/tests/test_tendlc.py +596 -0
  14. {sendly-3.34.0 → sendly-3.37.0}/.github/header.svg +0 -0
  15. {sendly-3.34.0 → sendly-3.37.0}/.gitignore +0 -0
  16. {sendly-3.34.0 → sendly-3.37.0}/CHANGELOG.md +0 -0
  17. {sendly-3.34.0 → sendly-3.37.0}/examples/async_example.py +0 -0
  18. {sendly-3.34.0 → sendly-3.37.0}/examples/error_handling.py +0 -0
  19. {sendly-3.34.0 → sendly-3.37.0}/examples/send_sms.py +0 -0
  20. {sendly-3.34.0 → sendly-3.37.0}/sendly/errors.py +0 -0
  21. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/business_upgrade.py +0 -0
  22. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/campaigns.py +0 -0
  23. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/contacts.py +0 -0
  24. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/conversations.py +0 -0
  25. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/drafts.py +0 -0
  26. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/enterprise.py +0 -0
  27. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/labels.py +0 -0
  28. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/media.py +0 -0
  29. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/rules.py +0 -0
  30. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/templates.py +0 -0
  31. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/verify.py +0 -0
  32. {sendly-3.34.0 → sendly-3.37.0}/sendly/resources/webhooks.py +0 -0
  33. {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/__init__.py +0 -0
  34. {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/http.py +0 -0
  35. {sendly-3.34.0 → sendly-3.37.0}/sendly/utils/validation.py +0 -0
  36. {sendly-3.34.0 → sendly-3.37.0}/sendly/webhooks.py +0 -0
  37. {sendly-3.34.0 → sendly-3.37.0}/tests/conftest.py +0 -0
  38. {sendly-3.34.0 → sendly-3.37.0}/tests/test_async_client.py +0 -0
  39. {sendly-3.34.0 → sendly-3.37.0}/tests/test_client.py +0 -0
  40. {sendly-3.34.0 → sendly-3.37.0}/tests/test_errors.py +0 -0
  41. {sendly-3.34.0 → sendly-3.37.0}/tests/test_http_body_kwarg.py +0 -0
  42. {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages.py +0 -0
  43. {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages.py.bak +0 -0
  44. {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages_batch.py +0 -0
  45. {sendly-3.34.0 → sendly-3.37.0}/tests/test_messages_schedule.py +0 -0
  46. {sendly-3.34.0 → sendly-3.37.0}/tests/test_numbers.py +0 -0
  47. {sendly-3.34.0 → sendly-3.37.0}/tests/test_validation.py +0 -0
  48. {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.34.0
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_*`).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sendly"
7
- version = "3.34.0"
7
+ version = "3.37.0"
8
8
  description = "Official Sendly Python SDK for SMS messaging"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
+ )