Metadata-Version: 2.4
Name: zoplio
Version: 0.3.0
Summary: Official Zoplio Python SDK for the Zoplio API v1
Author-email: Zoplio <hello@zoplio.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/Zoplio/zoplio-sdk
Project-URL: Repository, https://github.com/Zoplio/zoplio-sdk
Project-URL: Issues, https://github.com/Zoplio/zoplio-sdk/issues
Keywords: zoplio,scheduling,ai-agent,meetings,mcp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Dynamic: license-file

# zoplio (Python SDK)

Official Zoplio Python SDK for the [Zoplio API v1](https://github.com/Zoplio/zoplio-sdk/blob/main/docs/quickstart.md). MIT licensed.

Zoplio schedules meetings for you: you say who to invite and roughly when, Zoplio negotiates with every invitee over WhatsApp/email and confirms a slot.

Requires Python >= 3.10. Depends on `httpx`.

## Install

```bash
pip install zoplio
```

## Roles

The account that owns the API key is the organizer. `participants` are the people Zoplio invites. To arrange a meeting for someone else, set `organizer`.

- You are a Zoplio user like anyone on WhatsApp: a meeting you create runs on your calendar, in your timezone and working hours, Zoplio tells you when the invitees answer, and it counts against your plan.
- Every participant (1-8) is invited; one is enough. Your own number or e-mail as a participant is rejected with `validation_failed`.
- `organizer` is the optional on-behalf mode (an agency booking for a client): that person is then the organizer, every participant is still invited, and the meeting still bills to your account.

## Usage

```python
from zoplio import ZoplioClient, ZoplioError

zoplio = ZoplioClient(api_key="zpl_...")  # base_url defaults to https://api.zoplio.com

# Create a meeting. You are the organizer; Zoplio invites Jana.
created = zoplio.schedule_meeting(
    participants=[{"phone": "+420777123456", "name": "Jana"}],
    title="Intro call",
    duration_minutes=30,
    preferred_date="2026-09-16",
    preferred_time="14:00",
    timezone="Europe/Prague",  # always send it with preferred_time
    idempotency_key="order-42-intro-call",  # optional, safe retries
)
print(created["meetingId"], created["status"], created["proposedSlots"])

# Poll status (or use webhooks instead). Each entry of meeting["participants"]
# carries status, attending and role ("organizer" | "participant").
meeting = zoplio.get_meeting(created["meetingId"])

# List / reschedule / cancel.
zoplio.list_meetings(status="confirmed", limit=10)
zoplio.reschedule_meeting(
    created["meetingId"],
    preferred_date="2026-09-17",
    preferred_time="10:00",
    timezone="Europe/Prague",
    idempotency_key="order-42-intro-call-move-1",  # a retry replays instead of opening another round
)
zoplio.cancel_meeting(created["meetingId"])
```

On behalf of someone else: set `organizer`. Petr is then the organizer (his calendar and timezone; Zoplio tells him the meeting is being arranged and again once it confirms) and Jana is invited:

```python
zoplio.schedule_meeting(
    organizer={"email": "petr@example.com", "name": "Petr"},
    participants=[{"phone": "+420777123456", "name": "Jana"}],
    title="Intro call",
    preferred_date="2026-09-16",
    preferred_time="14:00",
    timezone="Europe/Prague",
)
```

Scheduling mode is picked by the date fields you send: exact (`preferred_date` + `preferred_time` + `timezone`), day (`preferred_date` only), range (`earliest_date` + `latest_date`) or open ask (`open_ask=True` plus the window). With no date fields Zoplio proposes one slot per working day over the next seven days at the start of the organizer's working hours (09:00 by default), rendered in each invitee's own timezone.

## Errors

Every non-2xx response raises `ZoplioError` with the contract envelope:

```python
try:
    zoplio.get_meeting("nope")
except ZoplioError as err:
    err.status_code  # 404
    err.code         # 'not_found' | 'unauthorized' | 'rate_limited' | 'validation_failed' | 'conflict' | 'quota_exceeded' | 'upstream_error'
    str(err)         # human-readable message
    err.details      # [{"field", "message"}] on validation_failed
```

`quota_exceeded` (HTTP 402) means your account's free-plan limit was reached this calendar month, the same limits as for any Zoplio user: 3 confirmed meetings and 15 meeting requests per calendar month (UTC). Meetings still being arranged count against the 3 until they confirm or fall through; `str(err)` says which limit it was.

## Webhooks

```python
# Subscribe. The secret is returned exactly once.
hook = zoplio.create_webhook(
    url="https://example.com/zoplio-hook",
    events=["meeting.confirmed", "meeting.rescheduled", "meeting.cancelled"],
    # omit `events` for all five: meeting.created, meeting.confirmed,
    # meeting.cancelled, meeting.rescheduled, negotiation.failed
)
save_secret(hook["secret"])  # whsec_...

zoplio.list_webhooks()
zoplio.delete_webhook(hook["id"])
```

`meeting.rescheduled` fires the moment a confirmed meeting re-opens to move; its payload carries `previousSlot`, and a fresh `meeting.confirmed` (or a cancellation) follows when the renegotiation resolves.

Verify deliveries with the static helper. Pass the RAW request body:

```python
# e.g. Flask
@app.post("/zoplio-hook")
def zoplio_hook():
    ok = ZoplioClient.verify_webhook_signature(
        request.get_data(),                          # raw bytes
        request.headers.get("X-Zoplio-Signature", ""),
        os.environ["ZOPLIO_WEBHOOK_SECRET"],         # whsec_...
    )
    if not ok:
        return "", 401
    delivery = request.get_json()  # {"event", "payload", "timestamp"}
    return "", 200
```

## Development

```bash
pip install -e ".[dev]"
python -m pytest
python -m mypy src/zoplio
```
