# socialrails

Official Python SDK for the [SocialRails](https://socialrails.com) API. Schedule and manage social media posts across Twitter/X, LinkedIn, Facebook, Instagram, TikTok, Bluesky, Pinterest, Threads, and YouTube.

## Installation

```bash
pip install socialrails
```

## Quick start

```python
from socialrails import SocialRails

client = SocialRails(api_key="sr_live_...")

# Create a scheduled post
post = client.posts.create(
    content="Launching our new feature today!",
    platform="twitter",
    scheduled_for="2026-03-15T14:30:00Z",
)
print(post["id"])

client.close()
```

## Async usage

```python
import asyncio
from socialrails import AsyncSocialRails

async def main():
    async with AsyncSocialRails(api_key="sr_live_...") as client:
        post = await client.posts.create(
            content="Hello from async Python!",
            platform="linkedin",
        )
        print(post["id"])

asyncio.run(main())
```

## Configuration

| Option | Environment variable | Description |
|---|---|---|
| `api_key` | `SOCIALRAILS_API_KEY` | Your API key (starts with `sr_live_`). Required. |
| `base_url` | -- | API base URL. Defaults to `https://socialrails.com/api/v1`. |

The constructor checks for the `SOCIALRAILS_API_KEY` environment variable when no `api_key` argument is provided:

```python
import os
os.environ["SOCIALRAILS_API_KEY"] = "sr_live_..."

client = SocialRails()  # picks up the env var automatically
```

## API reference

### Posts

#### `client.posts.create(...)`

Create a new post.

```python
post = client.posts.create(
    content="Check out our blog post!",
    platform="twitter",
    scheduled_for="2026-03-15T14:30:00Z",  # optional
    media=["workspace-id/api-uploads/images/uuid.jpg"],  # optional
    platform_settings={"selectedCommunities": ["12345"]},  # optional
)
```

**Thread support** (Twitter and Threads):

```python
post = client.posts.create(
    content="Thread intro",
    platform="twitter",
    thread=["First tweet", "Second tweet", "Third tweet"],
    thread_delay=5,  # minutes between tweets
)
```

#### `client.posts.list(...)`

List posts with optional filters and pagination.

```python
result = client.posts.list(
    status="scheduled",       # draft, scheduled, posted, failed
    platform="twitter",       # filter by platform
    sort="scheduled_for",     # created_at (default) or scheduled_for
    from_date="2026-03-01",   # ISO 8601 date
    to_date="2026-03-31",     # ISO 8601 date
    limit=25,                 # 1-100, default 50
    offset=0,                 # pagination offset
)
# result is a dict: the API returns {"data": [...], "pagination": {...}}
# The SDK unwraps "data", so result is the list of posts when no pagination,
# or the full response dict when pagination is present.
```

#### `client.posts.get(post_id)`

Get a single post by ID.

```python
post = client.posts.get("550e8400-e29b-41d4-a716-446655440000")
```

#### `client.posts.update(post_id, ...)`

Update a draft or scheduled post.

```python
updated = client.posts.update(
    "550e8400-e29b-41d4-a716-446655440000",
    content="Updated content!",
    scheduled_for="2026-03-20T10:00:00Z",
)
```

#### `client.posts.delete(post_id)`

Delete a draft or scheduled post.

```python
result = client.posts.delete("550e8400-e29b-41d4-a716-446655440000")
# {"id": "...", "deleted": True}
```

#### `client.posts.batch(...)`

Create posts across multiple platforms in a single request.

```python
result = client.posts.batch(
    content="Cross-platform announcement!",
    platforms=["twitter", "linkedin", "facebook"],
    scheduled_for="2026-03-15T14:30:00Z",
    platform_content={
        "twitter": "Short version for Twitter!",
        "linkedin": "Detailed professional version for LinkedIn.",
    },
)
print(result["batch_id"])
print(len(result["posts"]))
```

### Analytics

#### `client.analytics.get(...)`

Retrieve aggregate or per-post analytics.

```python
# Aggregate analytics for the last 30 days
analytics = client.analytics.get(period="30d")
print(analytics["total_posts"])
print(analytics["by_platform"])

# Filter by platform
analytics = client.analytics.get(platform="twitter", period="7d")

# Per-post analytics
post_analytics = client.analytics.get(post_id="550e8400-...")
```

### Accounts

#### `client.accounts.list()`

List all connected social media accounts.

```python
accounts = client.accounts.list()
for account in accounts:
    print(f"{account['provider']}: {account['name']} ({account['status']})")
```

#### `client.accounts.get_settings(account_id)`

Get platform capabilities and default settings for an account.

```python
settings = client.accounts.get_settings("550e8400-...")
print(settings["capabilities"]["character_limit"])
print(settings["available_tools"])
```

#### `client.accounts.trigger_tool(account_id, tool)`

Trigger a platform discovery tool (e.g. list Facebook pages, Pinterest boards).

```python
result = client.accounts.trigger_tool("550e8400-...", "pages")
for page in result["results"]:
    print(page)
```

### AI

#### `client.ai.generate(...)`

Generate AI-powered social media content.

```python
result = client.ai.generate(
    prompt="Write a tweet about our new Python SDK launch",
    platform="twitter",
    tone="professional",
)
print(result["content"])
```

### Workspace

#### `client.workspace.get()`

Get workspace information including plan limits and current usage.

```python
workspace = client.workspace.get()
print(f"Plan: {workspace['plan']}")
print(f"Posts this month: {workspace['usage']['posts_this_month']}")
print(f"Monthly limit: {workspace['limits']['max_posts_per_month']}")
```

### Media

#### `client.media.upload(file, filename, ...)`

Upload a media file via multipart form data.

```python
with open("photo.jpg", "rb") as f:
    media = client.media.upload(f, "photo.jpg")
print(media["key"])  # use this key when creating posts

# Upload a video thumbnail
with open("thumb.jpg", "rb") as f:
    thumb = client.media.upload(f, "thumb.jpg", media_type="thumbnail")
```

#### `client.media.upload_from_url(url, ...)`

Upload media from a remote URL.

```python
media = client.media.upload_from_url(
    "https://example.com/image.jpg",
    media_type="image",
)
print(media["key"])
```

### Webhooks

#### `client.webhooks.list()`

List all registered webhooks.

```python
webhooks = client.webhooks.list()
for wh in webhooks:
    print(f"{wh['url']} -> {wh['events']}")
```

#### `client.webhooks.create(url, events)`

Register a new webhook. Returns the signing secret (store it securely).

```python
webhook = client.webhooks.create(
    url="https://example.com/webhook",
    events=["post.published", "post.failed"],
)
print(webhook["id"])
print(webhook["secret"])  # save this -- only shown once
```

#### `client.webhooks.delete(webhook_id)`

Delete a webhook.

```python
result = client.webhooks.delete("550e8400-...")
```

## Error handling

All API errors raise `SocialRailsError` with `message`, `code`, and `status` attributes:

```python
from socialrails import SocialRails, SocialRailsError

client = SocialRails(api_key="sr_live_...")

try:
    client.posts.create(content="", platform="twitter")
except SocialRailsError as e:
    print(f"Error {e.status}: [{e.code}] {e.message}")
    # Error 400: [BAD_REQUEST] Field "content" is required and must be a string.
```

Common error codes:

| Code | Status | Description |
|---|---|---|
| `BAD_REQUEST` | 400 | Invalid request parameters |
| `UNAUTHORIZED` | 401 | Missing or invalid API key |
| `FORBIDDEN` | 403 | API key lacks required scope |
| `NOT_FOUND` | 404 | Resource not found |
| `RATE_LIMITED` | 429 | Too many requests |
| `LIMIT_EXCEEDED` | 429 | Monthly plan limit reached |
| `PAYLOAD_TOO_LARGE` | 413 | Request body or file too large |
| `INTERNAL_ERROR` | 500 | Server error |

## Context manager

Both clients support context managers for automatic cleanup:

```python
# Sync
with SocialRails(api_key="sr_live_...") as client:
    posts = client.posts.list()

# Async
async with AsyncSocialRails(api_key="sr_live_...") as client:
    posts = await client.posts.list()
```

## Requirements

- Python 3.8+
- [httpx](https://www.python-httpx.org/) >= 0.24.0

## License

MIT
