Python Examples
These examples demonstrate how to use the Tailwind API with Python.
Replace the example account, board, post, media, and destination values with your own. Scheduling examples use a date in 2099 so they remain valid; choose the future date and time you actually want.
pip install requestsimport osimport requestsfrom datetime import datetime, timezone
API_URL = 'https://api-v1.tailwind.ai'API_KEY = os.environ['TAILWIND_API_KEY']
headers = { 'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}List Accounts
Section titled “List Accounts”def list_accounts(): response = requests.get( f'{API_URL}/v1/accounts', headers=headers ) response.raise_for_status() return response.json()['data']['accounts']
# Usageaccounts = list_accounts()for account in accounts: print(f"Account: {account['displayName']} ({account['id']})")List Boards
Section titled “List Boards”def list_boards(account_id: str): response = requests.get( f'{API_URL}/v1/accounts/{account_id}/boards', headers=headers ) response.raise_for_status() return response.json()['data']['boards']
# Usageboards = list_boards('123456')for board in boards: print(f"Board: {board['name']} ({board['id']})")Create and Schedule a Post
Section titled “Create and Schedule a Post”def create_post(account_id: str, post_data: dict): response = requests.post( f'{API_URL}/v1/accounts/{account_id}/posts', headers=headers, json=post_data ) response.raise_for_status() return response.json()['data']['post']
# Usage - Create a scheduled postpost = create_post('123456', { 'mediaUrl': 'https://example.com/my-image.jpg', 'title': 'Amazing Recipe', 'description': 'Try this delicious recipe! #recipes #cooking', 'url': 'https://myblog.com/recipe', 'boardId': '1106196864631757445', 'altText': 'A colorful plate of food', 'sendAt': '2099-01-20T14:00:00Z'})
print(f"Created post: {post['id']}")print(f"Status: {post['status']}")Create and Schedule a Video Post
Section titled “Create and Schedule a Video Post”# Set mediaType to 'video' for video pinsvideo_post = create_post('123456', { 'mediaUrl': 'https://example.com/my-video.mp4', 'mediaType': 'video', 'title': 'Watch This Tutorial', 'description': 'Step-by-step guide #tutorial #video', 'url': 'https://myblog.com/tutorial', 'boardId': '1106196864631757445', 'sendAt': '2099-01-20T14:00:00Z'})
print(f"Video post created: {video_post['id']}")Create a Draft Post
Section titled “Create a Draft Post”# Omit sendAt to create a draftdraft = create_post('123456', { 'mediaUrl': 'https://example.com/my-image.jpg', 'title': 'Draft for Review', 'description': 'I will schedule this later', 'boardId': '1106196864631757445'})
print(f"Draft created: {draft['id']}")# draft['status'] will be 'draft'Schedule a Draft
Section titled “Schedule a Draft”If the draft doesn’t already have a boardId, provide one when scheduling:
def schedule_post(account_id: str, post_id: str, send_at: str, board_id: str = None): body = {'sendAt': send_at} if board_id: body['boardId'] = board_id
response = requests.post( f'{API_URL}/v1/accounts/{account_id}/posts/{post_id}/schedule', headers=headers, json=body ) response.raise_for_status() return response.json()['data']['post']
# Usagescheduled = schedule_post( '123456', 'post_abc123', '2099-01-25T09:00:00Z', '1106196864631757445' # Required if draft has no board assigned)
print(f"Scheduled for: {datetime.fromtimestamp(scheduled['sendAt'], tz=timezone.utc)}")List Posts with Filtering
Section titled “List Posts with Filtering”def list_posts(account_id: str, **kwargs): params = {} if 'status' in kwargs: params['status'] = kwargs['status'] if 'limit' in kwargs: params['limit'] = kwargs['limit'] if 'cursor' in kwargs: params['cursor'] = kwargs['cursor'] if 'start_date' in kwargs: params['startDate'] = kwargs['start_date'] if 'end_date' in kwargs: params['endDate'] = kwargs['end_date']
response = requests.get( f'{API_URL}/v1/accounts/{account_id}/posts', headers=headers, params=params ) response.raise_for_status() data = response.json()['data'] return { 'posts': data['posts'], 'cursor': data.get('cursor') }
# List queued posts (default)queued = list_posts('123456')print(f"Queued posts: {len(queued['posts'])}")
# List draftsdrafts = list_posts('123456', status='draft')print(f"Drafts: {len(drafts['posts'])}")
# List sent posts in a date rangesent = list_posts( '123456', status='sent', start_date='2024-01-01T00:00:00Z', end_date='2024-01-31T23:59:59Z', limit=100)print(f"Sent in January: {len(sent['posts'])}")Delete a Post
Section titled “Delete a Post”def delete_post(account_id: str, post_id: str): response = requests.delete( f'{API_URL}/v1/accounts/{account_id}/posts/{post_id}', headers=headers ) response.raise_for_status() return True
# Usagedelete_post('123456', 'post_abc123')print('Post deleted')Complete Example: Bulk Scheduler
Section titled “Complete Example: Bulk Scheduler”def bulk_schedule(account_id: str, posts: list): """Schedule multiple posts with a delay between each.""" results = []
for post_data in posts: try: post = create_post(account_id, post_data) results.append({'success': True, 'post': post}) print(f"Scheduled: {post['id']}") except Exception as e: results.append({ 'success': False, 'error': str(e), 'data': post_data }) print(f"Failed: {e}")
return results
# Usageposts_to_schedule = [ { 'mediaUrl': 'https://example.com/image1.jpg', 'title': 'Post 1', 'description': 'First post', 'url': 'https://example.com/post-1', 'boardId': '1106196864631757445', 'sendAt': '2099-01-20T09:00:00Z' }, { 'mediaUrl': 'https://example.com/image2.jpg', 'title': 'Post 2', 'description': 'Second post', 'url': 'https://example.com/post-2', 'boardId': '1106196864631757445', 'sendAt': '2099-01-20T14:00:00Z' }, { 'mediaUrl': 'https://example.com/image3.jpg', 'title': 'Post 3', 'description': 'Third post', 'url': 'https://example.com/post-3', 'boardId': '1106196864631757445', 'sendAt': '2099-01-20T19:00:00Z' }]
results = bulk_schedule('123456', posts_to_schedule)successful = sum(1 for r in results if r['success'])print(f"Scheduled {successful} posts")Using Dataclasses for Type Safety
Section titled “Using Dataclasses for Type Safety”from dataclasses import dataclassfrom typing import Optional, Listfrom datetime import datetime
@dataclassclass Account: id: str user_id: Optional[str] display_name: Optional[str] username: Optional[str] avatar_url: Optional[str] token_authorized: bool is_domain_verified: bool created_at: Optional[int]
@dataclassclass Board: id: str name: str is_collaborator: bool is_secret: bool
@dataclassclass Post: id: str status: str # 'draft' | 'queued' | 'sent' | 'uploading' media_url: str media_type: str # 'image' | 'video' title: Optional[str] description: Optional[str] url: Optional[str] board_id: Optional[str] alt_text: Optional[str] send_at: Optional[int] sent_at: Optional[int] created_at: int pin_id: Optional[str] is_simplified_pin: bool
def parse_account(data: dict) -> Account: return Account( id=data['id'], user_id=data.get('userId'), display_name=data['displayName'], username=data['username'], avatar_url=data.get('avatarUrl'), token_authorized=data['tokenAuthorized'], is_domain_verified=data['isDomainVerified'], created_at=data.get('createdAt') )
def parse_post(data: dict) -> Post: return Post( id=data['id'], status=data['status'], media_url=data['mediaUrl'], media_type=data['mediaType'], title=data.get('title'), description=data.get('description'), url=data.get('url'), board_id=data.get('boardId'), alt_text=data.get('altText'), send_at=data.get('sendAt'), sent_at=data.get('sentAt'), created_at=data['createdAt'], pin_id=data.get('pinId'), is_simplified_pin=data['isSimplifiedPin'] )Error Handling
Section titled “Error Handling”from requests.exceptions import HTTPError
def safe_api_call(func): """Decorator for handling API errors.""" def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except HTTPError as e: if e.response.status_code == 401: print('Authentication failed. Check your API key.') elif e.response.status_code == 429: print('Daily rate limit exceeded. Retry after midnight UTC.') elif e.response.status_code == 404: print('Resource not found. Check your IDs.') else: print(f'API error: {e}') raise return wrapper
@safe_api_calldef get_accounts(): return list_accounts()
# Usageaccounts = get_accounts()Async Example with aiohttp
Section titled “Async Example with aiohttp”import asyncioimport aiohttp
async def list_accounts_async(): async with aiohttp.ClientSession() as session: async with session.get( f'{API_URL}/v1/accounts', headers=headers ) as response: data = await response.json() return data['data']['accounts']
async def create_posts_async(account_id: str, posts: list): """Create multiple posts concurrently.""" async with aiohttp.ClientSession() as session: tasks = [] for post_data in posts: task = session.post( f'{API_URL}/v1/accounts/{account_id}/posts', headers=headers, json=post_data ) tasks.append(task)
responses = await asyncio.gather(*tasks, return_exceptions=True) results = [] for i, response in enumerate(responses): if isinstance(response, Exception): results.append({'success': False, 'error': str(response)}) else: data = await response.json() results.append({'success': True, 'post': data['data']['post']})
return results
# Usageaccounts = asyncio.run(list_accounts_async())