The AgentDraft protocol specification
A coordination contract for any process — human, agent, or bot — that books on a shared calendar.
AgentDraft exposes four endpoint groups — availability, holds, commits, and audit. Agents authenticate with a scoped avs_live_… key, post a booking with start and end times and a metadata object, and get back either a booking record or a typed 409 Conflict carrying the winning agent's identity, priority, and the audit reference. Every state change writes an append-only audit row.
Updated
Principles
AgentDraft is a thin coordination layer. It owns three things, and nothing else:
- The rules a calendar's owner has set.
- The identity of agents acting on that calendar.
- The order in which competing bookings resolve.
Invariant No two bookings on the same calendar may share a 5-minute time bucket. The conflict engine enforces this at the storage layer via a conditional write — there is no race window.
Quick start
Issue an API key from your dashboard, install your client of choice, commit a booking. The whole loop takes under three minutes — or feel the engine first, right here:
Watch the engine resolve a race — live
Two agents reach for the same 30-minute slot. A house scheduler agent (priority 5) holds it first. Pick your agent's priority and run the real conflict engine — lower number wins.
pip install agentdraftv0.1.1from datetime import datetime, timedelta, timezone
from agentdraft import Client, Conflict
client = Client(api_key="avs_live_...")
start = datetime.now(timezone.utc) + timedelta(hours=4)
end = start + timedelta(minutes=30)
try:
booking = client.bookings.commit(
start=start, end=end,
idempotency_key="ik_call_42",
title="Discovery call",
invitee={"name": "Ada Lovelace", "email": "ada@example.com"},
)
print("booked:", booking.booking_id)
except Conflict as e:
print(f"outranked by {e.winning_agent_id} (rank {e.winning_agent_priority})")No account yet? Run this first with the public demo key:
curl "https://api.agentdraft.io/v1/availability?user_id=usr_demo_public&range_start=2027-01-04T00:00:00Z&range_end=2027-01-04T23:59:59Z" \ -H "Authorization: Bearer avs_demo_9c2f7a41b8e6403d9a1e7c5b2f0d8a63"
Reads work with no signup. Writes (like the booking above) return a 403 pointing here — get your own free key, no card, in about a minute: Get an API key .
The SDK throws Conflict (Python) / Conflict (TypeScript) when your agent loses a race; inspect winning_agent_id and winning_agent_priority to decide whether to fall back to a different slot, escalate to a human, or simply give up.
Use with an agent framework
Not every agent calls the API directly. Pick the package that matches how your agent runs:
- agentdraftYou're writing the agent yourself — install the SDK and call the API directly, as in the Quick start above.
- agentdraft-langchainYou're building on LangChain, LangGraph, CrewAI, or AutoGen — drop-in agent tools.
- agentdraft-mcpYour agent runs in an MCP host — Claude Desktop, Cursor, or Cline.
Endpoints
Scheduling
Returns open slots for a user / date range, filtered through the owner's working hours, focus blocks, minimum lead time, and active bookings & holds. Working hours and focus blocks are evaluated in the owner's timezone (from /v1/agents/me), so a slot is only offered if it falls inside the painted window in that zone.
The owner's timezone defaults to UTC until it is set. Because working hours are wall-clock rules read in that zone, leaving it on UTC silently shifts the whole grid — a 09:00–17:00 rule for an Eastern owner whose timezone is still UTC gates 9-to-5 UTC, i.e. the small hours of their morning. Set it in Dashboard → Rules or via PUT /v1/me/profile before trusting the slots a client renders.
Set working hours, buffers, focus blocks, minimum lead time, a daily meeting cap, and per-agent priority ranking. These are enforced, not just stored: out-of-policy slots drop from /v1/availability and a commit that violates one returns 422 with an error of outside_working_hours, focus_block, lead_time_not_met, or daily_cap_reached. Default buffers fill in when a booking omits its own. Patches bump rules_version, invalidating outstanding availability etags.
Set the owner's IANA timezone (e.g. America/New_York) — the zone every wall-clock rule is enforced in. It defaults to UTC, so set it before relying on working hours. Session-authed (dashboard login), not an agent key: an agent can read the zone via /v1/agents/me but cannot change it. GET /v1/me/profile reads it back.
The commit point. Returns 201 on success, 409 on outrank, 410 on stale etag, 422 on rule violation. Optional title and invitee ({ name, email, notes }) record who the slot is for — persisted on the booking, echoed in the response, and shown to the owner in the dashboard.
Mailbox
Every agent gets an inbound address at inbox.agentdraft.io. Replies thread back to the booking that started the conversation; outbound is DKIM-signed. Scoped by mailbox:read / mailbox:write on the API key. For synthetic QA, use the same mailbox surface to monitor magic-link, OTP, signup, and round-trip email flows .
Self-introspection: the agent's inbox address, whether free-form (non-booking) send is allowed on the current tier, and the daily send quota (cap, used today, remaining). Read once on startup — SDKs cache it.
Reverse-chronological list of the agent's inbound + outbound messages. Filter with direction, booking_id, or since (ISO-8601 UTC, inclusive). Cursor pagination via next_cursor.
One message with full plain-text body and parsed SPF / DKIM / DMARC verdicts.
Send mail from the agent's inbox address. From: is server-controlled — only reply_to is overridable. 402 if the tier requires a booking_id and you didn't supply one; 429 on daily-cap; 422 if the recipient is suppressed.
Threaded reply convenience: To:, In-Reply-To, booking_id and a Re:-prefixed subject are derived from the original. Pass only the body. 404 if the message isn't in the calling agent's mailbox.
Pre-flight check before composing: returns { suppressed, kind, suppressed_at } so the agent can surface a friendly error instead of hitting 422 recipient_suppressed on send. Raw bounce diagnostics are deliberately omitted.
Agent lifecycle
Dashboard-scoped (cookie-authed) endpoints for managing the agents that book on your behalf. Issuing, rotating, revoking, and bringing them back online. Each operation preserves the agent's agent_id, mailbox address, priority, and scopes — only the API secret changes.
Issue a new agent and its first API key. The plaintext key is returned once in the response body and never again — capture it on receipt.
Mint a new key for an active agent. The previous key stops authenticating the instant the rotation commits; there is no grace window. Returns 404 on revoked or unknown agents.
Revoke. The key stops authenticating immediately and the gsi1 lookup row is removed. The agent record itself is preserved so it can be brought back via unrevoke.
Restore a revoked agent. Because revocation wipes the stored key hash, unrevoke mints a new plaintext key — returned once in the body, same one-time semantics as the create response. Returns 404 if the agent is already active or doesn't belong to the caller.
Re-rank agents by passing an ordered array of agent_ids, top-to-bottom. The new ranking takes effect on the next call — conflict resolution reads priority live from each agent row, so there is no cache to invalidate.
Conflict resolution
Two agents race for 16:00. The lower-rank-number agent wins. Ties resolve by partition write order. Within the configurable bump window (default 30s), a higher-priority agent may overwrite a lower-priority commit, emitting an EVICTED audit event.
// commit pipeline (simplified)
TransactWriteItems([
PutItem(BucketRow#16:00, cond:
attribute_not_exists
OR (status = HOLD AND agent_priority >= mine)
OR (status = COMMITTED AND agent_priority > mine
AND committed_at > bump_cutoff)),
PutItem(BucketRow#16:05, cond: ...),
...
PutItem(Booking),
PutItem(AuditEvent),
])Error codes
| Status | Meaning | SDK exception |
|---|---|---|
| 201 | Booking committed (or held) | — |
| 401 | Missing / invalid API key | AuthError |
| 402 | Quota exceeded (monthly bookings or daily sends) | AgentDraftError |
| 403 | Key lacks required scope | AuthError |
| 409 | Outranked or frozen — or consent_required when a human approval gate is set | Conflict / ConsentRequired |
| 410 | Stale etag / expired hold | AgentDraftError |
| 422 | Rule violation | RuleViolation |
| 429 | Rate limited | RateLimited |
The default rate limit is 60 requests/second per API key with a burst allowance of 600; exceeding it returns 429 with a Retry-After header. When an agent is outranked it can counter-propose alternative slots rather than just retrying.
Audit log
Every state-changing operation writes one immutable audit event. The application IAM role has PutItem only — no app code can rewrite history. Retention is set by tier and enforced by DynamoDB TTL.
Frequently asked
Do I need an SDK, or can I call the API directly?
Either works. The SDKs are thin wrappers around the documented HTTP surface — they give you typed responses, ergonomic idempotency, and a typed Conflict exception for the 409 case, but the raw API is fully documented and stable on its own.
What's the difference between a hold and a commit?
A hold is a tentative reservation with a TTL (30 seconds by default) — used when an agent has picked a slot but is still confirming with a human. A commit is the permanent booking. Both go through the conflict engine, so a higher-priority commit can evict a lower-priority hold within the hold's lifetime.
How do I scope an agent's API key?
In the dashboard, mint an avs_live_… key with one or more scopes — availability:read, bookings:read, bookings:write, mailbox:read, mailbox:write, and rules:read. The FastAPI dependency require_scope(...) enforces it on every protected endpoint.