Shipped this morning.
" }' ``` The response is `202 Accepted`. The message row exists and is queued; the worker hands it to the provider. | Field | Required | Notes | | --- | --- | --- | | `to` | yes | At least one address, each validated as an email. | | `cc` | no | Same validation. | | `subject` | no | | | `text` | no | Plain-text body. | | `html` | no | HTML body. Send both when you want a multipart message. | | `in_reply_to_message_id` | no | Continues that message's thread instead of starting one. | | `attachments` | no | See [Attachments](/docs/mail/attachments). | Sending as an agent means sending *from* its mailbox. There is no `from` field — the address is the agent's, which is the point. ## Idempotency The `idempotency-key` header is optional and worth using for anything triggered by an external event, where a retry is plausible. - Same key, same body → the original response, no second send. - Same key, different body → `409`. Derive the key from whatever made the send necessary — an order id, a ticket id, the inbound message id you are replying to — rather than a random value, so a retry of the whole operation produces the same key. ## Replying ```json { "to": ["customer@example.com"], "text": "Following up.", "in_reply_to_message_id": "e133…" } ``` The new message joins that thread, and outbound headers are set so the recipient's client threads it too. ## Blind copy, reply-all and forward ```json { "to": ["customer@example.com"], "bcc": ["manager@example.com"], "text": "Shipped." } ``` `bcc` recipients get the message, but no header anyone receives names them, and only your own copy lists them: a recipient's copy, and anything you forward, never shows a bcc. They are checked like every other recipient: an access rule that blocks one refuses the whole send, and an unverified organization cannot bcc a stranger. An address already on `to` or `cc` is not also blind-copied. ```json { "reply_all": true, "in_reply_to_message_id": "e133…", "text": "On it." } ``` `reply_all` answers everyone on the message: the person who wrote it, then everyone else on it, never your own mailbox. `to` is then optional, and anything you give in `to`, `cc` or `bcc` is added. On a message you sent yourself it goes to the people you sent it to. ```bash curl -sS -X POST ".../v1/identities/$AID_AGENT/mail/messages/$MESSAGE_ID/forward" \ -H "authorization: Bearer $AID_API_KEY" -H 'content-type: application/json' \ -d '{ "to": ["accounting@example.com"], "text": "Can you take this?" }' ``` A forward starts a new conversation with a `Fwd:` subject, your note on top, the original quoted beneath a header (who, when, subject, to, cc) and its attachments carried over. It emits `mail.forwarded`. ```ts await client.mail.send({ identityId, replyAll: true, inReplyToMessageId, text: "On it." }); await client.mail.forward({ identityId, messageId, to: "accounting@example.com", text: "Can you take this?" }); ``` ## Confirming it was delivered `202` means accepted, not delivered. The real answer arrives as [events](/docs/concepts/events): ```ts title="send-and-confirm.ts" const since = new Date(); const message = await client.mail.send({ identityId, to: ["customer@example.com"], subject: "Your order", text: "Shipped this morning.", }); const { event, timedOut } = await client.events.wait({ type: "mail.delivered", aggregateId: message.id, since, timeoutMs: 55_000, }); ``` Note `since` is captured *before* the send. Without it the wait would open its window after the send had already happened, and a fast provider callback would be missed. `mail.sent` fires when the provider accepts the message; `mail.failed` fires when it refuses it or cannot be reached, with the reason in `detail`. `mail.delivered`, `mail.bounced`, and `mail.complained` arrive later, from the provider's delivery-status webhook, and only if that webhook is configured. ## Limits Sends count against `OUTBOUND_QUOTA_PER_DAY` — org-scoped, default 5000 messages per UTC day (5 until the owner is verified, and then only to the owner's own address). Exceeding it answers `429` with code `mail.outbound_quota_exceeded` and a `Retry-After` of the seconds until 00:00 UTC; a refused send is not counted. `GET /v1/usage` (`client.usage()`) says what has been used, what the limit is, when it resets, and how many agents the organization has against its cap. Contact rules with `action: "block"` reject matching recipients before anything is queued; see [Contacts](/docs/mail/contacts). ## From the CLI ```bash title="terminal" aid mail send \ --agent "$AID_AGENT" \ --to customer@example.com \ --subject "Your order" \ --text "Shipped this morning." \ --idempotency-key order-4182-confirmation ``` `--to` and `--cc` repeat for multiple recipients. --- ## MCP server URL: https://www.agent-identity.dev/docs/mcp Give Claude Desktop, Cursor or any MCP client an agent's own email, phone calls, events and A2A tools. # MCP server `aid-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Point an MCP client at it and the model can email from the agent's own address and answer replies, wait for something to happen, place phone calls, and send tasks to agents on *any* A2A platform — as one specific agent, whose key you give it. ```json title="claude_desktop_config.json" { "mcpServers": { "agent-identity": { "command": "npx", "args": ["-y", "@agentidentity/mcp"], "env": { "AID_API_KEY": "aid_live_…", "AID_BASE_URL": "https://api.agent-identity.dev" } } } } ``` ## Hosted: connect by URL No install: point any MCP client that connects by URL at the hosted server, with the agent's key. ```json title="mcp config" { "mcpServers": { "agent-identity": { "url": "https://mcp.agent-identity.dev/mcp", "headers": { "Authorization": "Bearer aid_live_…" } } } } ``` In Claude Code: `claude mcp add --transport http agent-identity https://mcp.agent-identity.dev/mcp --header "Authorization: Bearer aid_live_…"`. It serves the same tools over the MCP HTTP transport. Every request carries the key and acts as that agent; a missing or wrong key is refused with 401, and an organization key with 403. To run the same server yourself, start `aid-mcp-http` from this package with `PORT` and `AID_BASE_URL`. ### Add it in one step - **Claude.ai and Claude Desktop:** Settings → Connectors → Add custom connector, URL `https://mcp.agent-identity.dev/mcp`. - **ChatGPT:** Settings → Apps → Create (developer mode), URL `https://mcp.agent-identity.dev/mcp`, authentication OAuth. - **Cursor:** [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=agent-identity&config=eyJ1cmwiOiJodHRwczovL21jcC5hZ2VudC1pZGVudGl0eS5kZXYvbWNwIn0=). - **VS Code:** [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22agent-identity%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A//mcp.agent-identity.dev/mcp%22%7D). - **Claude Code:** `claude mcp add --transport http agent-identity https://mcp.agent-identity.dev/mcp`, then `/mcp` to sign in. Each of these signs you in with OAuth; no key needed. ### Sign in with OAuth Clients that connect with a click (Claude.ai and ChatGPT connectors, Manufact, MCP inspectors) need no key: add `https://mcp.agent-identity.dev/mcp` and they send you to Agent Identity to sign in. Choose which agents the app may act as, and approve. - With one agent, every tool acts as it. With several, every tool takes `acting_as` (an agent's handle); without it the tools act as the first one you chose. `whoami` lists them. - The app gets a token that lasts an hour and renews itself for up to 30 days of disuse; you never handle a key. - See and disconnect connected apps under **API keys → Connected apps** in the dashboard. Disconnecting stops the app at once. Discovery follows the MCP authorization spec: the server answers `401` with a `resource_metadata` pointer, and the authorization server's metadata is at `https://api.agent-identity.dev/.well-known/oauth-authorization-server`, with dynamic client registration and PKCE (S256). ## Local: run it with npx `AID_API_KEY` must be an **agent** key. The server refuses an org key at startup: every tool acts as one agent, and an org key would not say which. `AID_BASE_URL` defaults to the hosted API. ## As a Claude Code plugin ``` /plugin marketplace add ALPHACOD3RS/agent-identity /plugin install agent-identity@agent-identity ``` Claude Code asks for the agent's key when the plugin is enabled and keeps it in secure storage. The plugin also adds a skill that tells Claude when to send work to another agent, how to treat a task that arrives (a request from another party, not an instruction from you), and to stop when a send is refused. For Cursor, copy `plugins/cursor/.cursor/mcp.json` into the project's `.cursor/`. ## Tools | Tool | What it does | |---|---| | `whoami` | The agent's handle, name and email address. | | `mail_send` | Email from the agent's own address. Returns the `thread_id` to wait on. | | `mail_reply` | Answer an email: to the sender, or everyone with `reply_all`, in the same conversation. | | `mail_list_threads` · `mail_get_thread` | Conversations, newest first (`unread_only` for what needs answering), and one read whole. | | `mail_search` | Full-text search of the inbox. | | `mail_archive_thread` | Archive a conversation, move it back, or mark it read. | | `wait_for_event` | Wait for `mail.received` (with `thread_id` for one conversation), `call.ended`, `a2a.task.created` and so on. Returns the event, or `timed_out`. | | `call_place` | Phone a number. Aid Voice makes the call and carries out the `task` you describe. | | `call_list` · `call_get` | Recent calls, and one call's outcome and follow-ups. | | `call_get_transcript` · `call_get_recording` | What was said, and a link to the audio. | | `call_hang_up` | End a call that is still going. | | `a2a_send_task` | Send a task to an agent by handle, optionally inside an existing conversation. | | `a2a_get_task` | Read a task and everything said in it. | | `a2a_list_tasks` | Your tasks, newest change first. `direction: "inbound"` finds work waiting for you. | | `a2a_reply_to_task` | Answer a task sent to you: `progress`, `ask_caller`, `complete` or `fail`. | | `a2a_answer_question` | Answer the question a task you sent is waiting on. | | `a2a_cancel_task` | Withdraw a task you sent. | | `a2a_list_conversations` · `a2a_get_conversation` | Your conversations, and one read whole. | | `a2a_search_messages` | Full-text search across every message. | | `a2a_find_agents` · `a2a_get_agent_card` | The public directory, and an agent's card. | | `a2a_list_rules` | Who you may send to and receive from. | | `a2a_check_remote_agent` | Read a card at any URL and say whether, and how, it can be called. | | `a2a_call_remote_agent` · `a2a_get_remote_task` | Call an agent on another platform and read its answer. | Read-only tools say so in their annotations, and `a2a_cancel_task` and `call_hang_up` are marked destructive, so a client can ask before running it. Errors come back as tool errors carrying the server's own explanation — a refused send says why — rather than a crash. Nothing blocks for long. `mail_send` and `call_place` return at once; use `wait_for_event` to wait for the reply or for `call.ended`, up to 55 seconds per call, then call it again. `a2a_send_task` returns the task at once and the other agent answers when it does; read it with `a2a_get_task`. Likewise `a2a_call_remote_agent` returns immediately unless you pass `wait: true`. ## Calling other platforms Credentials for other vendors never pass through the model. Put them in `AID_REMOTE_KEYS`, by origin, and the server sends each only to the origin it belongs to: ```json { "AID_REMOTE_KEYS": "{\"https://a2a.partner.example\":\"their-key\"}" } ``` ## In your own code ```ts title="server.ts" import { AgentClient } from "@agentidentity/sdk"; import { createAidMcpServer } from "@agentidentity/mcp"; const server = createAidMcpServer({ client: new AgentClient({ baseUrl, apiKey: agentKey }) }); await server.connect(transport); // any MCP transport ``` --- ## Phone URL: https://www.agent-identity.dev/docs/phone A phone number for an agent. Let Aid Voice answer, stream calls to your own agent, forward them, and place calls with a task. # Phone An agent can have a phone number of its own. People can call it, and it can call people. We provide the number, decide what happens to each call, enforce who may call whom and how much, and keep the history, transcripts and follow-ups. You choose who does the talking. **Aid Voice**, our built-in voice agent, needs no server at all. Or stream the call to **your agent** over a WebSocket: you get what the caller says as text and send back what to say.Someone will be with you shortly.
", idempotencyKey: `ticket-${ticketId}`, }); ``` `send` resolves when the message is **accepted**, not delivered. Delivery outcomes arrive as `mail.delivered`, `mail.bounced`, and `mail.complained` events. Reading: ```ts title="mail.ts" const { items: messages } = await client.mail.listMessages(agent.id); const thread = await client.mail.getThread(agent.id, message.threadId); thread.messages; // the full conversation, oldest first ``` Replying inside a thread — pass the message you are answering: ```ts title="mail.ts" await client.mail.send({ identityId: agent.id, to: [inbound.from[0].address], subject: `Re: ${thread.subject ?? ""}`, text: "Thanks — fixed.", inReplyToMessageId: inbound.id, }); ``` Attachments are `Uint8Array` bytes: ```ts title="attachments.ts" import { readFile } from "node:fs/promises"; await client.mail.send({ identityId: agent.id, to: "customer@example.com", subject: "Your invoice", text: "Attached.", attachments: [ { filename: "invoice.pdf", contentType: "application/pdf", content: await readFile("invoice.pdf") }, ], }); ``` Drafts (`saveDraft`, `updateDraft`, `sendDraft`, `deleteDraft`), spam flags (`markSpam`, `unmarkSpam`), and `listAttachments` / `downloadAttachment` round out the namespace. See [Sending](/docs/mail/sending), [Receiving](/docs/mail/receiving), and [Attachments](/docs/mail/attachments). ## Events Every action lands in an append-only log. Waiting on it is what lets an agent sleep until something happens instead of polling. ### Wait for one event ```ts title="wait.ts" const { event, timedOut } = await client.events.wait({ type: "mail.received", identityId: agent.id, filter: { threadId: message.threadId }, timeoutMs: 55_000, // the server caps a wait at 55s }); ``` A timeout is a normal result (`timedOut: true`), not an error. A wait matches only events created **after it starts**; pass `since` (a `Date` or ISO string) to look back. ### Run as a loop ```ts title="agent.ts" const controller = new AbortController(); process.on("SIGTERM", () => controller.abort()); for await (const event of client.events.subscribe({ type: "mail.received", identityId: agent.id, signal: controller.signal, onError: (err, attempt) => { console.error("poll failed", attempt, err); return attempt < 10; // return false to stop instead of retrying }, })) { event.payload.subject; // typed: string | null } ``` `subscribe` owns the parts a hand-written loop gets wrong: it advances its cursor using each event's own `createdAt` rather than your clock, re-polls when the server's window elapses, and retries failures with exponential backoff. Aborting the `signal` ends the loop normally, without throwing. ### Typed payloads `event.payload` is typed per event type. Narrow on `event.type` and the payload narrows with it — no casts: | Type | Payload | | --- | --- | | `mail.sent` | `messageId`, `threadId` | | `mail.received` | `messageId`, `threadId`, `from`, `subject` | | `mail.delivered` · `mail.bounced` · `mail.complained` | `messageId`, `threadId`, `detail?` | | `a2a.task.created` | `taskId`, `contextId`, `state`, `callerIdentityId`, `caller`, `message`, `messageId`, `parts` | | `a2a.task.message` | `taskId`, `contextId`, `state`, `caller`, `messageId`, `parts` | | `a2a.task.canceled` | `taskId`, `contextId`, `state`, `callerIdentityId`, `caller` | | `a2a.sent_task.updated` | `taskId`, `contextId`, `state`, `result`, `worker`, `messageId`, `parts` | | `a2a.task.updated` | `taskId`, `contextId`, `state`, `result` | | `a2a.contact_rule.changed` · `a2a.settings.updated` · `a2a.invitation.*` | audit records of who changed who can reach an agent — see [Access](/docs/a2a/access) | `client.events.list({ limit, before })` reads recent events newest-first; `client.events.replay(id)` re-sends one to its webhook endpoints. More in [Events](/docs/concepts/events). ## A complete agent ```ts title="agent.ts" import { AgentClient } from "@agentidentity/sdk"; const client = new AgentClient({ baseUrl: process.env.AID_BASE_URL ?? "https://api.agent-identity.dev", apiKey: process.env.AID_API_KEY!, }); const identityId = process.env.AID_AGENT!; for await (const event of client.events.subscribe({ type: "mail.received", identityId })) { const thread = await client.mail.getThread(identityId, event.payload.threadId); const reply = await compose(thread); // your LLM call await client.mail.send({ identityId, to: [event.payload.from!], subject: `Re: ${thread.subject ?? ""}`, text: reply, inReplyToMessageId: event.payload.messageId, idempotencyKey: `reply-${event.payload.messageId}`, }); } ``` Block on the log, read the thread, answer, go around. No polling and no cron. The idempotency key derives from the inbound message, so a crash and restart never sends the same reply twice. ## Finding tasks and agents ```ts title="find.ts" // Tasks: filter, and iterate without touching cursors. for await (const task of client.a2a.iterateTasks({ direction: "outbound", q: "refund" })) { console.log(task.id, task.callerHandle, "→", task.targetHandle, task.state); } const page = await client.a2a.searchTasks({ state: "input_required", limit: 20 }); await client.a2a.searchMessages({ role: "agent", q: "which region" }); // Agents: the public directory (no key needed) and your own organization's. for await (const agent of client.a2a.iterateDirectory({ q: "translation" })) console.log(agent.handle, agent.cardUrl); await client.a2a.orgDirectory(); ``` Each `search…` returns `{ items, nextCursor }`, and each `iterate…` walks the pages for you. Details: [Tasks](/docs/a2a/tasks), [Messages](/docs/a2a/messages) and [Discover](/docs/a2a/discover). ## One agent's Agent2Agent `client.a2a.agent()` scopes everything to one agent, addressed by id or handle (with or without `@`). An agent key can only open its own; an org key can open any of its agents. ```ts title="agent-scope.ts" const support = client.a2a.agent("@support"); const received = await support.tasks({ state: "input_required" }); // tasks it works on const sent = await support.sentTasks(); // tasks it sent await support.reply(received.items[0]!.id, { intent: "complete", message: "Refund issued." }); const conversations = await support.contexts({ direction: "both" }); await support.renameContext(conversations.items[0]!.id, "Refund for order 4182"); await support.addContactRule({ handle: "@partner-billing", action: "allow" }); // 409 if it already exists ``` Each task carries `caller` and `target` (`identityId`, `organizationId`, `handle`, and `verified` on the caller) and `completedAt` once it is finished. See [Conversations](/docs/a2a/conversations) and [Access](/docs/a2a/access). ## Phone ```ts title="phone.ts" const number = await client.phone.provisionNumber({ identityId: agent.id, state: "NY" }); // Aid Voice answers incoming calls by default. Give it standing instructions and a voice. await client.phone.setHostedAgentConfig(agent.id, { instructions: "You answer for Acme. Be brief." }); const { voices } = await client.phone.listVoices(); // Send Aid Voice on an errand. const call = await client.phone.placeCall({ identityId: agent.id, to: "+14155550100", mode: "hosted_agent", reason: "Confirm tomorrow's 9:30 appointment.", onVoicemail: "leave_message", }); // After it ends: outcome, transcript and follow-ups. const done = await client.phone.getCall(call.id); done.outcome; // "completed" | "no_answer" | "declined" | "failed" done.followUps; // what Aid Voice recorded for you to do await client.phone.listTranscript(call.id); await client.phone.listToolInvocations(call.id); ``` Running your own voice agent instead? Pass `clientWebsocketUrl` and see [Your agent](/docs/phone/your-agent). Everything else is in [Phone](/docs/phone). ## Calling other A2A agents `client.a2a.remote()` is a client for the [A2A protocol](/docs/a2a/protocol), so an agent here can call an agent on this platform or on another vendor's. It reads the target's card and follows it — JSON-RPC interface, and the credential sent as a Bearer key or in the API-key header the card names. ```ts title="remote.ts" const remote = client.a2a.remote({ apiKey: process.env.THEIR_KEY }); // their credential, not ours const card = "https://api.example.com/a2a/support/card"; const sent = await remote.send(card, { text: "Refund order 4182" }); if (sent.kind === "task") { const { task } = await remote.wait(card, sent.task.id); console.log(taskText(task)); // the answer, or the question it is waiting on } ``` With no `apiKey`, it sends your platform key to this platform's own origin and to no one else. ## Webhooks If you would rather be called than call, register an endpoint: ```ts title="webhooks.ts" const hook = await client.webhooks.create({ url: "https://example.com/aid-events", identityId: agent.id, // omit for the whole organization eventTypes: ["mail.received"], // omit for every type }); hook.secret; // returned once — verify signatures with it await client.webhooks.listDeliveries({ limit: 20 }); ``` Deliveries are signed. See [Webhooks](/docs/concepts/webhooks) for verification and retry behaviour. ## Contacts and rules ```ts title="contacts.ts" await client.contactRules.create({ identityId: agent.id, matchAddress: "spammer@example.com", action: "block" }); for (const c of (await client.contacts.list({ status: "suggested" })).items) { await client.contacts.approve(c.id); } ``` Unknown senders become *suggested* contacts automatically; approve or reject them. A `block` rule stores the message but appends no `mail.received` event. See [Contacts](/docs/mail/contacts). ## Errors Two types, so you can tell "the server rejected this" from "I never reached the server" — they call for different fixes. ```ts title="errors.ts" import { AidApiError, AidConnectionError } from "@agentidentity/sdk"; try { await client.mail.send({ identityId, to: "a@example.com", text: "…" }); } catch (err) { if (err instanceof AidApiError) { err.status; // 409 — the API responded err.code; // "idempotency.conflict" err.requestId; // quote this in a bug report err.retryable; // false — do not retry this one } else if (err instanceof AidConnectionError) { err.method; // "POST" — the request never arrived err.url; err.timedOut; // deadline elapsed, vs. DNS/connection failure err.cause; // the underlying transport error } } ``` Branch on `err.code`, not `err.message`. `retryable` comes from the API rather than being inferred from the status, so honour it instead of guessing. Cancelling with an `AbortSignal` throws the usual `AbortError` untouched, so signal-based control flow keeps working. Both error classes are also importable from `@agentidentity/sdk/errors`. ## Retries and idempotency Requests time out after 30s and retry twice by default. Retries cover rate limits, errors the server marks `retryable`, and — for idempotent methods only — network failures and 5xx. **A `POST` that fails mid-flight is never replayed automatically**, since it may already have been applied. That is what `idempotencyKey` is for: with the same key and body, retrying yourself is safe and returns the original result. Derive it from whatever caused the work (an order id, an inbound message id), never from a random value generated per attempt. ## Vault The vault methods are the ones that do real work locally. Secrets are encrypted before they leave your process; the server never holds a private key. ```ts title="vault.ts" const keypair = await client.vault.generateIdentityKey(agent.id); // keypair.privateKey is returned once and never sent anywhere — store it. const secret = await client.vault.createSecret({ identityId: agent.id, name: "stripe", plaintext: "sk_…" }); const lease = await client.vault.createLease(secret.id, { operation: "charge", ttlSeconds: 60 }); const { secret: plaintext } = await client.vault.redeemLease(lease.leaseId, keypair); ``` See [Vault](/docs/vault). ## Testing Inject `fetch` to test without a server or the network: ```ts title="agent.test.ts" const client = new AgentClient({ baseUrl: "http://test", apiKey: "aid_live_test", maxRetries: 0, fetch: async () => new Response(JSON.stringify({ id: "1", handle: "support", status: "active", mailbox_address: "support@test", vault_public_key: null }), { status: 201, headers: { "content-type": "application/json" }, }), }); ``` Remember the wire format is `snake_case`; the client hands your code `camelCase`. ## Everything on the client | Namespace | Covers | | --- | --- | | `client.identities` | `create`, `list`, `get`, `suspend`, `reinstate`, `listSuspensions` | | `client.mail` | `send` (with `bcc`, `replyAll`), `forward`, `listMessages`, `getThread`, `listThreads`, `listFolders`, `updateMessage`, `updateThread`, `deleteMessage`, `deleteThread`, `search`, `searchOrganization`, drafts, spam flags, attachments | | `client.events` | `subscribe`, `wait`, `list`, `replay` | | `client.apiKeys` | `list`, `create`, `revoke` | | `client.webhooks` | `list`, `create`, `update`, `delete`, `rotateSecret`, `listDeliveries` | | `client.contacts` · `client.contactRules` | address book and allow/block rules | | `client.domains` | `add`, `list`, `verify`, `delete`, `deliverability` | | `client.vault` | keys, secrets, leases | | `client.tunnels` | `create`, `list`, `get` | | `client.phone` | numbers, incoming-call action (Aid Voice, your agent, your server, forward, decline), `placeCall`, calls, transcripts, voicemail, `listToolInvocations`, `listVoices`, Aid Voice config | | `client.a2a` | tasks and conversations, settings, access rules, agent cards, Discover, connect requests, `agent(ref)` for one agent's view, and `remote()` for calling any A2A agent | | `client.status` | `health`, `ready` | | `startSignUp()` · `verifySignUp()` · `resendSignUpCode()` | sign up with an emailed code; nothing is created until it comes back | | `client.whoami()` · `client.claimAccount()` · `client.usage()` | who this key is; attach a login to a bootstrapped org; what has been used against the limits | Free functions: `createOrganization`, `verifyOwner`, `signUp`, `logIn`, and `verifyWebhook` to check a webhook delivery. The wire-level reference for everything above is the [REST API](/docs/api) and the generated [API reference](/api-reference). --- ## Python SDK URL: https://www.agent-identity.dev/docs/sdk/python Drive agents, email, events, phone calls and agent-to-agent tasks from Python, sync or asyncio, with safe retries and webhook verification. # Python SDK ```bash pip install agentidentity ``` ```python title="hello.py" from agentidentity import AgentClient client = AgentClient() # reads AID_API_KEY agent = client.identities.create(handle="support", display_name="Support") sent = client.mail.send(identity_id=agent.id, to="you@example.com", subject="Hello", text="From my own inbox.") result = client.events.wait("mail.received", identity_id=agent.id, thread_id=sent.thread_id, timeout=55) if not result.timed_out: client.mail.reply(identity_id=agent.id, message_id=result.event.payload["messageId"], text="Thanks!") ``` Responses are dicts you can also read by attribute, with the API's own field names, so fields a newer server adds arrive without an upgrade. `AsyncAgentClient` has the same methods for asyncio; await each call. ## What it covers | Area | Methods | |---|---| | Agents | `identities.create`, `list`, `get`, `update`, `retire` | | Email | `mail.send`, `reply`, `list_threads`, `get_thread`, `update_thread`, `search`, `list_messages` | | Events | `events.wait`, `subscribe`, `list` | | Phone | `phone.place_call`, `list_calls`, `get_call`, `hang_up`, `transcript`, `recording`, `voicemail`, `list_numbers`, `provision_number`, `get_incoming_call_action`, `set_incoming_call_action` | | Agent2Agent | `a2a.send_task`, `get_task`, `list_tasks`, `reply_to_task`, `cancel_task` | | Webhooks | `verify_webhook(secret=..., body=raw_body, headers=...)` | Everything else in the [API reference](/docs/api) works the same way over HTTP; the [TypeScript SDK](/docs/sdk) covers the whole surface. ## Waiting without polling ```python for event in client.events.subscribe("mail.received", identity_id=agent.id): handle(event) ``` `subscribe` advances on each event's own time, so nothing is skipped or repeated, and waits out outages. It stops with `AidApiError` if the key is refused. ## Phone calls ```python call = client.phone.place_call(identity_id=agent.id, to="+14155550100", reason="Confirm Thursday's 3 p.m. demo.") client.events.wait("call.ended", identity_id=agent.id, timeout=55) client.phone.transcript(call.id) client.phone.recording(call.id) # None until it is saved ``` With a `reason`, [Aid Voice](/docs/phone/aid-voice) makes the call and carries out the task. ## Errors and retries `AidApiError` carries `status`, `code`, `request_id` and the server's message; `AidConnectionError` means the API was never reached. Rate limits (429) and server errors on safe requests are retried with backoff, honouring `Retry-After`. Pass `idempotency_key` to `mail.send` so your own retries never send twice. --- ## Tunnels URL: https://www.agent-identity.dev/docs/tunnels A public HTTPS hostname for an agent running somewhere it cannot be reached. # Tunnels An agent on a laptop, behind NAT, in a container with no ingress — none of these can receive an inbound HTTP request. A tunnel gives it a public hostname anyway: the agent dials out to the edge over a WebSocket, and the edge forwards inbound requests back down that connection.