Metadata-Version: 2.5
Name: postzen-sdk
Version: 2.2.1
Summary: Official Python SDK for the PostZen Public API - manage profiles, accounts, OAuth connect, media, posts, inbox, comment automations, contacts, queues, analytics, API keys, and webhooks
Project-URL: Homepage, https://postzen.dev
Project-URL: Repository, https://github.com/postzen-dev/postzen-python
Project-URL: Documentation, https://docs.postzen.dev
Author: PostZen
License: MIT
License-File: LICENSE
Keywords: api,postzen,publishing,sdk,social media
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: datamodel-code-generator>=0.26.0; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.21.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center">
  <img src=".github/assets/postzen-icon.png" alt="PostZen" width="96" />
</p>

<h1 align="center">PostZen Python SDK</h1>

<p align="center">
  <a href="https://pypi.org/project/postzen-sdk/"><img src="https://img.shields.io/pypi/v/postzen-sdk" alt="PyPI version" /></a>
  <a href="https://pypi.org/project/postzen-sdk/"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license" /></a>
</p>

<p align="center"><strong>One API to post everywhere. 8 platforms, zero headaches.</strong></p>

The official Python SDK for the [PostZen API](https://docs.postzen.dev) — manage profiles, accounts, OAuth connect, media, posts, inbox, comment automations, contacts, queues, analytics, API keys, and webhooks with a single integration.

## Installation

```bash
pip install postzen-sdk
```

The import name is `postzen`. Requires Python 3.10+.

## Quick Start

Set `POSTZEN_API_KEY` and create a post:

```python
from postzen import PostZen

client = PostZen()

post = client.posts.create_post(
    content="Hello from PostZen",
    platforms=[{"platform": "instagram", "accountId": "acc_123"}],
    publish_now=True,
)

print(post.post.field_id)
```

Wire fields named `_id` are exposed as `field_id` on pydantic models, so a profile id is available as `profile.field_id`.

## Authentication

The client authenticates with a bearer API key, resolved in this order:

1. The `api_key` argument passed to `PostZen(...)`.
2. The `POSTZEN_API_KEY` environment variable.

If neither is set, `PostZen()` raises `ValueError`.

```python
from postzen import PostZen

# Explicit key
client = PostZen(api_key="pzn_live_...")

# Or rely on POSTZEN_API_KEY in the environment
client = PostZen()
```

## Configuration

```python
from postzen import PostZen

client = PostZen(
    api_key="pzn_live_...",
    base_url="https://api.postzen.dev",
    timeout=30.0,
    max_retries=3,
)
```

| Option | Description | Default |
|--------|-------------|---------|
| `api_key` | PostZen API key. If omitted, `POSTZEN_API_KEY` is used. | `None` |
| `base_url` | API base URL. Useful for tests and staging. | `https://api.postzen.dev` |
| `timeout` | Request timeout in seconds. | `30.0` |
| `max_retries` | Retries for connection errors and idempotent-method timeouts. See [Retry behavior](#retry-behavior). | `3` |

## Examples

### Schedule a Post

```python
from postzen import PostZen

client = PostZen()

client.posts.create_post(
    content="Scheduled from PostZen",
    platforms=[{"platform": "linkedin", "accountId": "acc_123"}],
    scheduled_for="2026-08-01T15:00:00Z",
    timezone="America/New_York",
)
```

### Platform-Specific Content

```python
from postzen import PostZen

client = PostZen()

client.posts.create_post(
    content="Shared copy",
    platforms=[
        {
            "platform": "instagram",
            "accountId": "acc_ig",
            "customContent": "Instagram copy",
            "settings": {"postType": "reel", "firstComment": "#postzen"},
        },
        {
            "platform": "youtube",
            "accountId": "acc_yt",
            "settings": {"title": "Launch update", "privacyStatus": "public"},
        },
    ],
    publish_now=True,
)
```

### Upload Media

`client.media.upload()` does it in one step: it presigns an upload slot, PUTs the
raw bytes to PostZen-hosted storage, and returns the public URL to reference in a
post's `media_items`. The content type is inferred from the file extension (pass
`content_type=` to override), and `size` is computed for you.

```python
from postzen import PostZen

client = PostZen()

# From a file path (str or os.PathLike)
media = client.media.upload("./photo.jpg")
print(media.public_url, media.key, media.type, media.size, media.filename)

# Or from raw bytes — filename is required so the content type can be inferred
with open("./photo.jpg", "rb") as fh:
    media = client.media.upload(fh.read(), filename="photo.jpg")

client.posts.create_post(
    content="Media post",
    media_items=[{"url": media.public_url}],
    publish_now=True,
)
```

The async client exposes `await client.media.aupload(...)` with the same signature.

### List Connected Accounts

```python
from postzen import PostZen

client = PostZen()

accounts = client.accounts.list_accounts(platform="instagram")
for account in accounts.accounts:
    print(account.field_id, account.username)
```

### Paginate Accounts

`list_accounts` accepts `page` and `limit` (max 100) and returns a `pagination` object with `page`, `limit`, `total`, and `totalPages`.

```python
from postzen import PostZen

client = PostZen()

page = 1
while True:
    result = client.accounts.list_accounts(page=page, limit=50)
    for account in result.accounts:
        print(account.field_id, account.username)

    pagination = result.pagination
    if not pagination or page >= pagination.totalPages:
        break
    page += 1
```

### Async Support

```python
import asyncio

from postzen import PostZen


async def main():
    async with PostZen() as client:
        profiles = await client.profiles.alist_profiles()
        print(profiles.profiles)


asyncio.run(main())
```

## Error Handling

```python
from postzen import PostZen
from postzen.client.exceptions import PostZenAPIError, PostZenRateLimitError

client = PostZen()

try:
    client.posts.create_post(content="Hello", publish_now=True)
except PostZenRateLimitError as exc:
    print(exc.limit, exc.remaining, exc.reset_time)
except PostZenAPIError as exc:
    print(exc.status_code, exc.details)
```

Exception hierarchy:

| Exception | Meaning |
|-----------|---------|
| `PostZenError` | Base SDK exception |
| `PostZenAPIError` | Base API response exception |
| `PostZenAuthenticationError` | HTTP 401 |
| `PostZenPaymentRequiredError` | HTTP 402 |
| `PostZenForbiddenError` | HTTP 403 |
| `PostZenNotFoundError` | HTTP 404 |
| `PostZenRateLimitError` | HTTP 429 with `limit`, `remaining`, and `reset_time` |
| `PostZenConnectionError` | Connection failure after retries |
| `PostZenTimeoutError` | Timeout after retries |
| `PostZenValidationError` | Client-side validation error |

The client updates `client.rate_limit_info` from `X-RateLimit-*` headers on every response.

### Retry behavior

Connection errors (the connection never opened) are always retried with exponential backoff, up to `max_retries`. Timeouts are retried only on idempotent methods (`GET`, `PUT`, `DELETE`); a read/write timeout on a `POST` — such as `create_post` — is *not* retried, because the request may have already reached the server and an automatic retry could silently create a duplicate. Instead, `PostZenTimeoutError` is raised immediately. To retry a `POST` safely, pass a stable `x_request_id` to `create_post` so the server treats the replay as idempotent:

```python
client.posts.create_post(
    content="Hello from PostZen",
    platforms=[{"platform": "instagram", "accountId": "acc_123"}],
    publish_now=True,
    x_request_id="a1b2c3d4-launch-2026-08-01",
)
```

## SDK Reference

### Profiles
| Method | Description |
|--------|-------------|
| `profiles.list_profiles()` | List profiles |
| `profiles.create_profile()` | Create a profile |
| `profiles.get_profile()` | Get a profile |
| `profiles.update_profile()` | Update a profile |
| `profiles.delete_profile()` | Delete a profile |

### Accounts
| Method | Description |
|--------|-------------|
| `accounts.list_accounts()` | List accounts |
| `accounts.create_pinterest_board()` | Create Pinterest board |
| `accounts.get_pinterest_boards()` | List Pinterest boards |
| `accounts.update_pinterest_boards()` | Set default Pinterest board |
| `accounts.disconnect_account()` | Disconnect an account |

### Connect (OAuth)
| Method | Description |
|--------|-------------|
| `connect.list_pinterest_boards_for_selection()` | List Pinterest boards for the connect flow |
| `connect.create_connect_url()` | Create an OAuth connect URL |
| `connect.complete_connect()` | Complete an OAuth connection |
| `connect.select_pinterest_board()` | Select Pinterest board |

### Media
| Method | Description |
|--------|-------------|
| `media.create_media_presign()` | Create a presigned media upload URL |
| `media.upload_media_direct()` | Upload a file directly |

### Posts
| Method | Description |
|--------|-------------|
| `posts.list_post_comments()` | List comments on a LinkedIn post |
| `posts.list_post_reactions()` | List reactions on a LinkedIn post |
| `posts.list_posts()` | List posts |
| `posts.create_post()` | Create a post |
| `posts.get_post()` | Get a post |
| `posts.update_post()` | Update a post |
| `posts.delete_post()` | Delete a post |
| `posts.bulk_upload_posts()` | Bulk upload posts from CSV |

### Inbox
| Method | Description |
|--------|-------------|
| `inbox.list_inbox_conversation_messages()` | List messages in a conversation |
| `inbox.list_inbox_conversations()` | List direct message conversations |
| `inbox.list_inbox_post_comments()` | List comments on a post |
| `inbox.get_inbox_conversation()` | Get a direct message conversation |
| `inbox.update_inbox_conversation()` | Archive or unarchive a conversation |
| `inbox.delete_inbox_comment()` | Delete a comment |
| `inbox.hide_inbox_comment()` | Hide a comment |
| `inbox.mark_inbox_conversation_read()` | Mark a conversation read |
| `inbox.reply_to_inbox_post()` | Comment on a post or reply to a comment |
| `inbox.search_inbox_conversations()` | Search direct message conversations |
| `inbox.send_inbox_message()` | Send a direct message |
| `inbox.unhide_inbox_comment()` | Unhide a comment |

### Comment Automations
| Method | Description |
|--------|-------------|
| `comment_automations.list_comment_automation_logs()` | List comment automation logs |
| `comment_automations.list_comment_automations()` | List comment automations |
| `comment_automations.create_comment_automation()` | Create a comment automation |
| `comment_automations.get_comment_automation()` | Get a comment automation |
| `comment_automations.update_comment_automation()` | Update a comment automation |
| `comment_automations.delete_comment_automation()` | Delete a comment automation |

### Contacts
| Method | Description |
|--------|-------------|
| `contacts.list_contacts()` | List contacts |
| `contacts.create_contact()` | Create a contact |
| `contacts.get_contact()` | Get a contact |
| `contacts.get_contact_channels()` | Get contact channels |
| `contacts.update_contact()` | Update a contact |
| `contacts.delete_contact()` | Delete a contact |
| `contacts.bulk_create_contacts()` | Bulk create contacts |

### Queues
| Method | Description |
|--------|-------------|
| `queues.list_queue_slots()` | Get a queue schedule |
| `queues.create_queue_slot()` | Create a queue |
| `queues.get_next_queue_slot()` | Get the next queue slot |
| `queues.update_queue_slot()` | Update a queue |
| `queues.delete_queue_slot()` | Delete a queue |
| `queues.preview_queue()` | Preview upcoming queue slots |

### Analytics
| Method | Description |
|--------|-------------|
| `analytics.get_analytics()` | Get post analytics |
| `analytics.get_best_time_to_post()` | Get the best times to post |
| `analytics.get_daily_metrics()` | Get daily analytics metrics |
| `analytics.get_follower_stats()` | Get follower statistics |
| `analytics.get_post_timeline()` | Get a post analytics timeline |
| `analytics.sync_external_posts()` | Synchronize external posts |

### API Keys
| Method | Description |
|--------|-------------|
| `api_keys.list_api_keys()` | List API keys |
| `api_keys.create_api_key()` | Create an API key |
| `api_keys.delete_api_key()` | Delete an API key |

### Webhooks
| Method | Description |
|--------|-------------|
| `webhooks.list_webhook_deliveries()` | List webhook deliveries |
| `webhooks.list_webhooks()` | List webhooks |
| `webhooks.create_webhook()` | Create a webhook |
| `webhooks.get_webhook()` | Get a webhook |
| `webhooks.update_webhook()` | Update a webhook |
| `webhooks.delete_webhook()` | Delete a webhook |
| `webhooks.redeliver_webhook_delivery()` | Redeliver a webhook event |
| `webhooks.test_webhook()` | Test a webhook |

## Requirements

- Python 3.10 or later
- A PostZen API key ([create one](https://app.postzen.dev/api-keys))

Runtime dependencies:

- `httpx>=0.27.0`
- `pydantic>=2.0.0`

## PostZen developer tools

- [Documentation](https://docs.postzen.dev)
- [API reference](https://docs.postzen.dev/api-reference)
- [Node.js SDK](https://github.com/postzen-dev/postzen-node) — `@postzen/node`
- [CLI](https://github.com/postzen-dev/postzen-cli) — `@postzen/cli`
- [MCP server](https://docs.postzen.dev/mcp)
- [Dashboard & API keys](https://app.postzen.dev/api-keys)

## License

MIT
