Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [1.3.0] 2026-04-28

- Added `set_twitter_byo(api_key, api_secret)` and `clear_twitter_byo()` for X/Twitter Bring-Your-Own-Keys support. When set, every outbound request includes `X-Twitter-OAuth1-Api-Key` and `X-Twitter-OAuth1-Api-Secret` headers — required for posting to X after the March 31, 2026 BYO enforcement date.

## [1.2.5] 2026-01-20
- Updates missing endpoint base url reference from app.ayrshare.com to api.ayrshare.com

Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ from ayrshare import SocialPost
social = SocialPost('DJED-DKEP-SJWK-WJKS') # get an API Key at ayrshare.com
```

### X/Twitter Bring-Your-Own-Keys (BYO)

Starting **March 31, 2026**, X/Twitter operations through Ayrshare require your own X Developer App credentials. Set them once after construction and every subsequent SDK call will include the required `X-Twitter-OAuth1-*` headers.

``` python
from ayrshare import SocialPost

social = SocialPost(API_KEY)
social.set_twitter_byo(MY_X_API_KEY, MY_X_API_SECRET)

social.post({"post": "Hello from BYO", "platforms": ["twitter"]})
```

Multi-tenant rotation:

``` python
social.set_twitter_byo(tenant_a_key, tenant_a_secret)
social.post({"post": "Hello from BYO", "platforms": ["twitter"]})

social.clear_twitter_byo().set_twitter_byo(tenant_b_key, tenant_b_secret)
social.post({"post": "Hello from BYO", "platforms": ["twitter"]})
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

See the [X/Twitter BYO setup guide](https://docs.ayrshare.com/dashboard/connect-social-accounts/x-twitter-byo-keys) for instructions on obtaining your X consumer key and secret.

### History, Post, Delete Example

This simple example shows how to post, get history, and delete the post. This example assumes you have a free API key from [Ayrshare](https://www.ayrshare.com) and have enabled X/Twitter, Facebook, and LinkedIn. Note, Instagram, Telegram, YouTube, TikTok, and Reddit also available.
Expand Down
17 changes: 17 additions & 0 deletions ayrshare/ayrshare.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ def setProfileKey(self, PROFILE_KEY):
self.headers['Profile-Key'] = PROFILE_KEY
return self

def set_twitter_byo(self, api_key, api_secret):
"""Attach X/Twitter Bring-Your-Own-Keys consumer credentials to every
subsequent request.

Required for posting to X after the March 31, 2026 enforcement date.
See https://docs.ayrshare.com/dashboard/connect-social-accounts/x-twitter-byo-keys.
"""
self.headers['X-Twitter-OAuth1-Api-Key'] = api_key
self.headers['X-Twitter-OAuth1-Api-Secret'] = api_secret
return self

def clear_twitter_byo(self):
"""Remove any previously-set X/Twitter BYO headers."""
self.headers.pop('X-Twitter-OAuth1-Api-Key', None)
self.headers.pop('X-Twitter-OAuth1-Api-Secret', None)
return self

def post(self, data, headers=None):
return doPost("post", data, self.headers)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

HERE = pathlib.Path(__file__).parent

VERSION = '1.2.5'
VERSION = '1.3.0'
PACKAGE_NAME = 'social-post-api'
AUTHOR = 'Ayrshare'
AUTHOR_EMAIL = 'support@ayrshare.com'
Expand Down
Empty file added tests/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions tests/test_byo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import unittest

from ayrshare import SocialPost


KEY_HEADER = 'X-Twitter-OAuth1-Api-Key'
SECRET_HEADER = 'X-Twitter-OAuth1-Api-Secret'


class TwitterBYOHeadersTest(unittest.TestCase):
def test_headers_absent_when_byo_never_set(self):
social = SocialPost('API_KEY')
self.assertNotIn(KEY_HEADER, social.headers)
self.assertNotIn(SECRET_HEADER, social.headers)

def test_set_twitter_byo_injects_both_headers(self):
social = SocialPost('API_KEY')
social.set_twitter_byo('ck_123', 'cs_456')
self.assertEqual(social.headers[KEY_HEADER], 'ck_123')
self.assertEqual(social.headers[SECRET_HEADER], 'cs_456')

def test_clear_twitter_byo_removes_both_headers(self):
social = SocialPost('API_KEY')
social.set_twitter_byo('ck_123', 'cs_456')
social.clear_twitter_byo()
self.assertNotIn(KEY_HEADER, social.headers)
self.assertNotIn(SECRET_HEADER, social.headers)

def test_clear_twitter_byo_is_noop_when_nothing_set(self):
social = SocialPost('API_KEY')
# Should not raise even when nothing is set.
social.clear_twitter_byo()
self.assertNotIn(KEY_HEADER, social.headers)

def test_setters_are_chainable(self):
social = SocialPost('API_KEY')
self.assertIs(social.set_twitter_byo('a', 'b'), social)
self.assertIs(social.clear_twitter_byo(), social)

def test_byo_coexists_with_profile_key(self):
social = SocialPost('API_KEY')
social.setProfileKey('PK').set_twitter_byo('ck', 'cs')
self.assertEqual(social.headers['Profile-Key'], 'PK')
self.assertEqual(social.headers[KEY_HEADER], 'ck')
self.assertEqual(social.headers[SECRET_HEADER], 'cs')

def test_instances_have_independent_byo_state(self):
a = SocialPost('A').set_twitter_byo('a_k', 'a_s')
b = SocialPost('B')
self.assertNotIn(KEY_HEADER, b.headers)
self.assertEqual(a.headers[KEY_HEADER], 'a_k')


if __name__ == '__main__':
unittest.main()