The AgentDraft API reference
A coordination contract for any process — human, agent, or bot — that books on a shared calendar.
AgentDraft exposes five endpoint groups — scheduling, mailbox, approvals, agent lifecycle, and agent identity. 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. An agent can also pause for a human sign-off before any consequential action. 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.2from 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, or AutoGen — drop-in agent tools. (CrewAI needs its own native 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 . The per-agent inbox guide covers isolation, quotas, and the mail passport.
Mailbox size limit: inbound messages are accepted by Amazon SES before AgentDraft sees them. The hard receive cap is 40 MB, including headers, MIME structure, and base64-encoded attachments. Larger messages are rejected during SMTP with 552 5.2.3 Message length exceeds limit set by recipient; they do not reach AgentDraft storage, webhooks, or the message API. Monitors and forwarding agents should send a short text summary plus an artifact URL or object key instead of forwarding large raw MIME bodies, logs, screenshots, or PDFs. Treat 552 5.2.3 as permanent, not retryable.
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.
Verified mail passport: every outbound message carries X-AgentDraft-Passport, a compact JWS signed by the deployment's Ed25519 attestation key over the sender, recipient, subject, Message-ID, body SHA-256, agent id, org id, and booking id. The signed Message-ID is preserved in X-AgentDraft-Message-ID because SES rewrites the RFC header. A recipient can verify it offline against /.well-known/jwks.json. AgentDraft verifies passports on inbound mail and stamps verified_agent_sender: true only when the signature and every binding match; an invalid passport is stored with the message but never raises trust. When the sender published a public A2A card, the message also carries Agent-Card: …/v1/agents/{id}/card.json. Every outbound message also carries a footer link to the agent's public verification page.
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.
Approvals
Pause for a human before doing something consequential. Body takes action_type (a namespaced string like deploy.production), summary (one line, ≤500 chars), an optional evidence object (≤64 KB serialized — larger returns 422 evidence_too_large), and an optional ttl_seconds clamped to the workspace ceiling. Returns 201 with {approval_id, status, expires_at} and honours Idempotency-Key.
The action need not be an AgentDraft one. AgentDraft owns the decision and its evidence trail, not the action — gate a deploy, a migration, or a payment on the same endpoint. Requires the approvals:request scope, which is not granted to new keys by default.
The polling surface — current status plus resolution_note, resolved_by, and resolved_at once a human decides. Terminal statuses are approved, denied, and expired; treat an expiry as not approved. Both SDKs wrap this as approvals.wait().
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),
])Approvals
An approval is a human sign-off gate an agent opens before doing something consequential. The agent calls POST /v1/approvals with an action_type, a one-line summary, and an optional evidence object, then waits. You approve or deny it in the dashboard; the agent is told the outcome the moment you decide. The action itself need not be an AgentDraft one — the same call gates a deploy, a refund, a migration, or an outbound email. AgentDraft owns the decision and its evidence trail, not the action.
For the evidence contract, TTL bounds, and signed receipts, read the approval gate guide .
How it works
- Grant the scope. Approvals ride on approvals:request, which is deliberately not in the default scope set (new keys get availability:read and bookings:write). An agent that can open a request can page a human, so you opt in per key when you issue or edit it in the dashboard.
- The agent asks. POST /v1/approvals returns 201 immediately with { approval_id, status, expires_at }. Nothing is blocked yet — the agent chooses when to wait.
- You decide. The request lands in your approvals queue with its evidence, and — unless you turn it off in Settings — one email nudge carrying the total number of requests waiting on you. Approve or deny, with an optional note.
- The agent finds out. The next poll of GET /v1/approvals/{id} returns the terminal status, your note, who decided, and when. A webhook fires and an audit event is written on every transition.
from agentdraft import Client
client = Client(api_key="avs_live_...") # needs the approvals:request scope
# 1. Ask. Returns immediately — nothing has happened yet.
approval = client.approvals.request(
action_type="deploy.production",
summary="Ship v2.3.0 to production",
evidence={"commit": "a1b2c3d", "migrations": 2, "diff": "+412 −87"},
ttl_seconds=1800,
)
# 2. Wait for a human. Returns on approved, denied, OR expired.
result = client.approvals.wait(approval["approval_id"])
# 3. Fail closed — only "approved" means go.
if result["status"] != "approved":
raise SystemExit(f"stood down: {result['status']}")
deploy()Statuses
A request starts pending and moves exactly once into one of three terminal statuses. Resolution is race-free: a concurrent approve and deny resolve once, and the loser is told the request was already decided.
| Status | Means | What your agent should do |
|---|---|---|
| pending | Waiting on a human | Keep polling, or call wait() |
| approved | A human said yes | Proceed. This is the only go signal |
| denied | A human said no | Stand down; read resolution_note |
| expired | The TTL lapsed before anyone decided | Stand down — expiry is not approval |
Fail closed. Both SDKs' approvals.wait() return on any terminal status, so branch on result["status"] == "approved" rather than treating a return as a yes. Left unset, wait() follows the request's own expires_at and never loops forever; ApprovalTimeout is raised only when you pass a timeout shorter than the request's life, and the request stays pending and resolvable when it does.
Limits
| Field | Limit |
|---|---|
| action_type | 3–64 chars, lowercase, namespaced — a dot is required (deploy.production, mailbox.send_external). Keeping it groupable is what makes the queue and the audit log filterable instead of free text. |
| summary | 1–500 chars, non-blank. This is the line a human judges. |
| evidence | Any JSON object, ≤ 64 KB serialized. Larger returns 422 evidence_too_large. |
| ttl_seconds | ≥ 60, default 3600 (1 hour), clamped to a 86400 (24 h) ceiling. |
| Idempotency-Key | Optional header; a retry with the same key replays the original 201 instead of opening a second request. |
Webhooks and audit
Every transition fires one webhook — approval.requested, approval.approved, approval.denied, approval.expired — and writes one immutable audit row. Subscribe on the webhooks page if you'd rather be pushed than poll. The evidence blob is deliberately absent from the webhook payload: it can be 64 KB of anything the agent attached, and an endpoint URL is a weaker place to put it than the authenticated GET /v1/approvals/{id}. Resolved requests stay readable for 30 days.
There are no approve/deny links in the notification email on purpose — a one-click approve from an inbox is an unauthenticated capability URL. The email links to the passkey-gated dashboard instead.
Agent identity
Every agent can carry a portable, cryptographic identity. The same Ed25519 hierarchy signs approval receipts, delegation tokens, A2A cards, and outbound HTTP requests. AgentDraft publishes the public halves; the private halves are returned once and never stored.
Team and above. Receipts, delegation, the Web Bot Auth signing key, and MCP CIMD are gated by the signed_attestations entitlement. The public JWKS and the A2A 1.0 card are available to every tier.
Public discovery
The Ed25519 public keys used to verify every AgentDraft attestation: approval and consent receipts, delegation tokens, and the A2A 1.0 card. The kid is the RFC 7638 JWK thumbprint, so it changes when the key rotates. Cached for one hour.
RFC 8414 metadata for the token-exchange surface: issuer, jwks_uri, token_endpoint (/v1/identity/exchange), the token-exchange grant, and the two RAR types (/rar/approval, /rar/booking). AgentDraft is not a general OAuth authorization server: the exchange authenticates the caller with its existing agent credential.
The signed A2A 1.0 AgentCard. The payload is JCS-canonicalized and carries a detached Ed25519 JWS in signatures; verify it against /.well-known/jwks.json. The 0.2.0 card at /.well-known/agent.json remains for older clients.
The agent's public Web Bot Auth directory, served as application/http-message-signatures-directory+json. It contains the public JWK plus agent_id, reply_mailbox, and human_delegated. This is the jwks_uri a verifier fetches from the Signature-Agent header. Revoked agents, agents without a signing key, and orgs below Team return 404.
The public record behind agentdraft.io/verify/{agent_id}: agent id, status, reply mailbox, public Ed25519 key, public A2A card, and MCP client id. It never exposes the owner's user id, org id, email, scopes, or priority. Revoked, deleted, and unknown agents return 404.
A compatibility index for verifiers that only accept the operator-level .well-known path. It lists the public keys of agents whose owner published a public A2A card (a2a_public=true) and annotates each JWK with agent_id. Cached for 60 seconds. The per-agent directory above remains canonical: prefer it when the verifier can follow Signature-Agent.
Signing and delegation
Mint or rotate the agent's Ed25519 Web Bot Auth key. The response contains signing_kid and signing_private_jwk; the private half is shown once and never stored. Rotating replaces the public key immediately, so the previous kid stops verifying. Session-authed (dashboard login), Team+.
RFC 8693 token exchange. An agent presents its avs_live_… key (or an existing delegated token) and asks for a shorter-lived token narrowed to one audience and a subset of its scopes. The subject is the human owner; the agent is the RFC 8693 act claim. Optional approval_id binds the exact approval evidence into a RAR entry, and ttl_seconds is clamped to the configured maximum. Team+.
Response: access_token, issued_token_type, token_type: Bearer, and expires_in. The token is accepted by current_agent only after the live agent row is re-read, so revoking the agent kills outstanding tokens.
from agentdraft import agent_signature_fields, sign_request
# private_jwk is the one-time value from POST .../signing-key.
headers = sign_request(
method="POST",
url="https://forms.example.com/contact",
private_jwk=private_jwk,
agent_id="agt_01...",
body=body,
)
# Send the four headers with the request. If the site uses SiftFy,
# forward them to siteverify as agent_signature:
agent_signature = agent_signature_fields(
headers, method="POST", authority="forms.example.com", path="/contact"
)import jwt
from jwt import PyJWKClient
# Verify an approval or delegation receipt offline; no AgentDraft call.
jwks = PyJWKClient("https://api.agentdraft.io/.well-known/jwks.json")
claims = jwt.decode(
token,
jwks.get_signing_key_from_jwt(token).key,
algorithms=["EdDSA"],
audience="https://deploy.example.com",
)The SDKs wrap that check in one call: verify_approval_token(token, audience=..., action_type=..., evidence_sha256=hash_evidence(payload)) in Python and verifyApprovalToken(token, { audience, actionType, evidenceSha256 }) in TypeScript. Both verify the signature, issuer, audience, expiry, acting agent, and RAR binding; pass a pre-fetched JWKS for an air-gapped verifier.
MCP client identity
The agent's MCP Client ID Metadata Document (CIMD). The URL is the OAuth client_id; the document names the agent, its reply mailbox, and a jwks_uri pointing at the agent's Web Bot Auth directory, with token_endpoint_auth_method: private_key_jwt and EdDSA. Without a redirect URI it advertises token exchange only. Cached for five minutes with an ETag. Team+; revoked agents, keyless agents, and lower tiers return 404.
Set or clear the exact redirect URI published in the CIMD document. Send {"redirect_uri": "https://agent.example.com/oauth/callback"} to add authorization_code and response_types: ["code"]; send {"redirect_uri": null} to remove them. The URI must be absolute HTTPS (HTTP is allowed for localhost in development) with no fragment or userinfo. Session-authed, Team+.
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, rules:read, and approvals:request. The FastAPI dependency require_scope(...) enforces it on every protected endpoint.
How do I make an agent ask me before it does something?
Give its key the approvals:request scope in the dashboard, then have the agent call client.approvals.request(...) and wait on the result before acting. The request shows up in your approvals queue with whatever evidence the agent attached, and only a status of approved should be treated as a go. See 05 Approvals.
What happens if I don't answer an approval request in time?
It expires and the agent is told it was not approved. Requests carry a TTL — one hour by default, up to 24 hours — and expiry is a terminal status, not a retry. Approvals fail closed on purpose: silence never becomes consent. An expired request stays readable for 30 days and the expiry is audited like any other decision.
How does an agent prove its identity to a third-party site?
Mint a Web Bot Auth key on the Agents page (Team+), sign the outbound request with sign_request(), and send the four headers. The verifier fetches the per-agent directory named in Signature-Agent and checks the Ed25519 signature; the content classifier still runs. For MCP, use the agent's oauth-client.json URL as the OAuth client_id with private_key_jwt. See 06 Agent identity.
Management MCP
A workspace owner on Team or above can create one avs_admin_… key in Settings and let an MCP host configure the whole tenant. This is separate from the agent-facing scheduling MCP, which keeps using per-agent avs_live_… keys.
Endpoint
https://api.agentdraft.io/mcpSend Authorization: Bearer avs_admin_… or x-api-key: avs_admin_…. The endpoint is stateless Streamable HTTP over JSON-RPC: the admin key is the only tenancy input, and no tool accepts org_id or user_id.
stdio bridge
npx -y agentdraft-admin-mcpSet AGENTDRAFT_ADMIN_KEY and optionally AGENTDRAFT_BASE_URL. The bridge forwards the same tools over stdio and never prints the key.
Tools
| Group | Tools |
|---|---|
| Agents | whoami, list_agents, create_agent, rotate_agent_key, revoke_agent, unrevoke_agent, reorder_agents, set_agent_mailbox |
| Rules | get_rules, update_rules |
| Webhooks | list_webhooks, create_webhook, delete_webhook, test_webhook, get_webhook_deliveries, rotate_webhook_signing_secret |
| Calendars | list_calendars, connect_calendar, subscribe_ics_calendar, disconnect_calendar, sync_calendar |
| Team, profile, consent | get_team, invite_team_member, revoke_team_invite, remove_team_member, update_profile, get_consent_policy, update_consent_policy |
| Audit, approvals, usage | query_audit, list_approvals, get_approval, resolve_approval, get_usage |
| Advanced identity (P1) | rotate_agent_signing_key, set_agent_a2a_public, set_agent_oauth_redirect |
Safety
- No tool can mint, rotate, revoke, reveal, or list an admin key — that stays in the dashboard, so a leaked key cannot perpetuate itself.
- No money movement: no plan change, checkout completion, refund, or cancellation.
- No permanent agent delete and no workspace deletion; team member removal is marked destructive and can be restored by re-invite.
- Destructive tools carry destructiveHint; the server tells the model to confirm with the human first.
- Every write is audited with actor_type: admin_key, and plaintext agent keys and webhook secrets never enter the audit log.