Zoplio · Co-founder and CTO · 2026

A public API for an assistant that books meetings

REST, two SDKs and a hosted MCP server, built June to September 2026.

Zoplio was an AI assistant that books your meetings. The developer API lets software do what a user does in a chat: say who to meet, and Zoplio writes to each person over WhatsApp or e-mail, agrees on a time and books it. I wrote all of the backend; this page is about the part other developers touch. The API is asynchronous by design: a create call returns at once with status: negotiating, and the outcome arrives later, by polling or by a signed webhook.

API docs · SDK source (MIT) · OpenAPI 3.1 reference

01

What it is

The REST API lives at api.zoplio.com under /v1: create, fetch, list, cancel and reschedule a meeting, read this month’s usage, and manage webhook subscriptions. The account that owns the API key is the organizer, so a meeting runs on that person’s calendar, in their timezone and working hours. participants holds 1 to 8 people, all of them invited. An optional organizer field covers the on-behalf case, such as an agency booking for a client: that person’s calendar is used, and the meeting still bills to the key’s account.

Around it sit a TypeScript SDK on npm, a Python SDK on PyPI that mirrors it one to one, and a hosted MCP server, so an agent such as Claude can book a meeting as one of its tools. The client libraries, the OpenAPI reference and the examples are open source under MIT. The scheduling engine itself, the negotiation logic and the prompts, runs on the hosted service and is not in the repository; there is no self-hosted mode.

02

Keys, limits, quotas and errors

Every /v1 route takes Authorization: Bearer zpl_…. Keys are created in the dashboard and shown once. An unknown key gets 401, an expired or inactive one 403, and if the auth backend itself is down the API answers 503 with upstream_error, so an outage never reads as “your key is bad”.

Rate limits are 60 requests a minute per key in a sliding window, shared by REST and the MCP server, with a ceiling of 300 a minute per IP across all keys. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After in seconds, and rejected requests still count toward the window.

Billing is per confirmed meeting, so negotiating is free. The free plan covers 3 confirmed meetings and 15 meeting requests per calendar month in UTC. A meeting still being arranged holds one of the 3 until it confirms or falls through, and cancelling it frees the slot. Past either limit the API returns 402 quota_exceeded with a message that says which limit it was, and GET /v1/usage shows what is left.

Every error uses one envelope, {"error": {"code", "message", "details"?}}, with a fixed set of codes: unauthorized, rate_limited, validation_failed, not_found, conflict, quota_exceeded and upstream_error. validation_failed adds a list of fields and messages. Both SDKs raise it as one typed error that keeps the code, the HTTP status and the details.

Webhook URLs are checked when you subscribe: the host is resolved in DNS, and plain http URLs, hosts that do not resolve and hosts that resolve to private or internal addresses are rejected.

03

Safe retries

POST /v1/meetings accepts an X-Idempotency-Key header of 1 to 128 characters. Replaying a key returns the meeting the first call created, with its current status, which may already be confirmed, instead of inviting everyone a second time.

Rescheduling is the harder case. A call looks like “set the slot to X”, so repeating it looks harmless, but each call opens a new negotiation round with the participants, and running out of rounds cancels the meeting. A client that times out and retries could walk a healthy meeting into cancellation. So reschedule takes the same key, and a replay returns the original proposal instead of opening another round. Cancel needs no key: it converges on a state, and cancelling an already cancelled meeting returns cancelled again.

MCP made this harder, because the protocol has no field to carry a key. The MCP handler for reschedule derives one from the arguments and feeds it into the same idempotency path as REST. The replay cache is scoped to the authenticated user, holds the in-flight result so a concurrent duplicate waits for the same answer, and replays only successes, so a failed attempt stays retryable. I described this, with its limits, in a comment on the MCP specification’s issue about retries of mutating tool calls. The point I would make to any tool author: retry safety cannot be read off the argument shape. Effects that set a target state can be made safe with a state check; incremental ones, such as rounds, messages or charges, need a key.

04

Signed webhooks

A subscription receives five events: meeting.created, meeting.confirmed, meeting.rescheduled (with the slot the meeting left), meeting.cancelled and negotiation.failed. Its whsec_ secret is returned once, at creation. Every delivery carries X-Zoplio-Signature, the hex HMAC-SHA256 of the raw body keyed with that secret, and X-Zoplio-Delivery-Id, a stable id of the event.

Delivery is at least once. A receiver has 10 seconds to answer with a 2xx; a failed delivery is retried up to 2 more times, after 1 and then 5 minutes, with the same delivery id, so receivers dedupe on it. After 10 consecutive failures the subscription is disabled.

The signature covers the raw bytes, not the parsed JSON, because re-serializing a parsed body may not give the same bytes. Both SDKs verify it with a constant-time comparison. This is the TypeScript version, unchanged from the repository:

static verifyWebhookSignature(  rawBody: string | Uint8Array,  signatureHeader: string,  secret: string,): boolean {  if (!signatureHeader || !secret) return false;  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');  const provided = signatureHeader.trim().toLowerCase();  const a = Buffer.from(expected, 'utf8');  const b = Buffer.from(provided, 'utf8');  if (a.length !== b.length) return false;  return timingSafeEqual(a, b);}

A runnable webhook receiver in Express shows the whole path: verify the signature over the raw body, skip ids it has already seen, and acknowledge fast.

05

SDKs and the MCP server

@zoplio/sdk-js and zoplio on PyPI are thin clients: one method per endpoint, the idempotency key as an option on create and reschedule, the typed error, and the webhook verifier. Each request identifies the SDK and its version in User-Agent. Continuous integration runs the TypeScript tests on Node 18, 20 and 22 and the Python tests on 3.10 to 3.14, and a separate check rejects any literal date in the docs and examples that is in the past or less than 30 days away, because a copied example with a stale date would book the past.

The MCP server runs at api.zoplio.com/mcp over stateless Streamable HTTP, behind the same zpl_ keys and the same rate limit as REST. It exposes five tools, each mapped one to one to a REST operation: schedule, get status, list, cancel and reschedule. Webhook management stays out of it on purpose, since webhooks are an integration concern and not something an agent does in a conversation. Connecting Claude Code is one command; @zoplio/claude-tool documents the setup for Claude Code, Cursor and Claude Desktop and ships a small config helper.

06

Known limits

  • The SDKs do not retry on their own. Each method makes one request and raises the typed error on failure; backing off on 429 with Retry-After, and retrying with the same idempotency key, is left to the caller.
  • The MCP replay cache lives in one process. It keeps entries for 15 minutes, is not durable across restarts or replicas, and cannot tell a retry from a deliberate repeat of the same move inside that window.
  • Create over MCP has no durable key. On REST, create is protected by a unique index on the client’s key, but the MCP tool has nowhere to receive one, so it falls back to a fresh id per call. This is why I support a standard key field in the protocol.
  • The Python SDK returns plain dicts. Typed return values were planned for a minor release, since they change static types.
07

Status and links

I co-founded Zoplio and was its CTO from March to October 2026. Zoplio has been on hold since October 2026: on its own, it was a feature rather than a standalone product. The last release of the SDKs is 0.3.1, with 0.1.2 of the MCP package, from 26 September 2026.

API docs · SDK source · OpenAPI reference · @zoplio/sdk-js · zoplio on PyPI · @zoplio/claude-tool · MCP retry discussion

Questions about any of it: hi@kubadvorak.com. Back to the home page.