Skip to main content

zoplio (Python SDK)

Official Zoplio Python SDK for the Zoplio API v1. 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

pip install zoplio

Get an API key

Sign in at zoplio.com/dashboard/api with Google, go to API keys and click Create key. Keys start with zpl_; copy yours right away, it is shown once. The free plan covers 3 confirmed meetings and 15 meeting requests per calendar month (UTC).

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

import os
from datetime import date, timedelta

from zoplio import ZoplioClient, ZoplioError

zoplio = ZoplioClient(api_key=os.environ["ZOPLIO_API_KEY"])  # base_url defaults to https://api.zoplio.com


def next_weekday(weekday: int) -> str:
    """The next given weekday (0 = Monday ... 6 = Sunday) after today, as YYYY-MM-DD."""
    today = date.today()
    return (today + timedelta(days=(weekday - today.weekday()) % 7 or 7)).isoformat()


# Create a meeting. You are the organizer; Zoplio invites Jana. With no date
# fields Zoplio proposes free working-day slots over the next week.
created = zoplio.schedule_meeting(
    participants=[{"phone": "+15555550100", "name": "Jana"}],  # fictional number: use a real one
    title="Intro call",
    duration_minutes=30,
    idempotency_key="order-42-intro-call",  # optional, safe retries
)
print(created["meetingId"], created["status"], created["proposedSlots"])

# Somebody who told Zoplio to stop contacting them is never invited. The
# meeting is still created with the rest of the list, so check this before
# you report who it is with; both keys are absent when everybody went in.
if created.get("skippedParticipants"):
    print(created["skippedMessage"], created["skippedParticipants"])
    # -> [{"name": "Jana", "reason": "opted_out"}]

# 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=next_weekday(3),  # next Thursday
    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"])  # frees the slot on the free plan

# Plan and this month's usage: {"plan", "month", "confirmed", "creates", "limits": {"confirmed", "creates"}}
usage = zoplio.get_usage()

An exact time: send preferred_date + preferred_time + timezone, and Jana gets a yes/no for that one slot:

zoplio.schedule_meeting(
    participants=[{"phone": "+15555550100", "name": "Jana"}],
    title="Intro call",
    preferred_date=next_weekday(2),  # next Wednesday
    preferred_time="14:00",
    timezone="Europe/Prague",  # always send it with preferred_time
)

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:

zoplio.schedule_meeting(
    organizer={"email": "petr@example.com", "name": "Petr"},
    participants=[{"phone": "+15555550100", "name": "Jana"}],
    title="Intro call",
)

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:

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. zoplio.get_usage() shows where you stand, and cancelling a meeting that is still being arranged frees its slot.

rate_limited (HTTP 429) means more than 60 requests/min for this key. The response carries a Retry-After header (seconds); wait that long before retrying, since throttled requests still count toward the window.

Webhooks

# 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 over the RAW request body, then dedupe: delivery is at-least-once and a retry carries the same X-Zoplio-Delivery-Id (also the body's id).

# 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()  # {"id", "event", "payload", "timestamp"}
    if already_handled(delivery.get("id")):  # a retry of an event you processed
        return "", 200
    return "", 200

Development

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

Metadata

Release files for zoplio 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zoplio 0.3.1
File Size Uploaded
zoplio-0.3.1.tar.gz 16.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zoplio 0.3.1
File Interpreter ABI Platform
zoplio-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 29.2 kB

Release files / zoplio-0.3.1.tar.gz

Download URL zoplio-0.3.1.tar.gz
Size 16.2 kB
Tags Source
SHA-256 checksum
How to use checksums
60e3982d9d2c6e7faef4fc346a1afbbafeb4d9d2dae018404e1c79f9f15817c5
BLAKE2b-256 checksum
How to use checksums
2172015c77931c0b9ac65e48e4003d35f93b676a74e2e8e3ba28401d5edef35d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / zoplio-0.3.1-py3-none-any.whl

Download URL zoplio-0.3.1-py3-none-any.whl
Size 13.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5d1d9c56348b538a06054e428aa4e60799d3657e8133645e978fe4c74749758e
BLAKE2b-256 checksum
How to use checksums
35291d7ff40d5f894c7ef655512466927b2aa55cdad7bfa18c5c19124497534c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page