{
  "markdown": "# Inkbox\n\nhttps://inkbox.ai\n\n[![PyPI](https://img.shields.io/pypi/v/inkbox)](https://pypi.org/project/inkbox/)\n[![npm](https://img.shields.io/npm/v/@inkbox/sdk)](https://www.npmjs.com/package/@inkbox/sdk)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nAPI-first communication infrastructure for AI agents — email (with custom sending domains), phone, identities, encrypted vault (login credentials, API keys, key pairs, SSH keys, OTP, etc.), and tunnels (expose a local server at a public URL via outbound HTTP/2).\n\n| Package | Language | Install |\n|---|---|---|\n| [`inkbox`](./sdk/python/) | Python ≥ 3.11 | `pip install inkbox` |\n| [`@inkbox/sdk`](./sdk/typescript/) | TypeScript / Node ≥ 22 | `npm install @inkbox/sdk` |\n| [`@inkbox/cli`](./cli/) | CLI / Node ≥ 22 | `npm install -g @inkbox/cli` |\n\n## Cursor plugin\n\n**Status:** Pre-release and under repository review; not yet submitted to or\navailable in the Cursor Marketplace.\n\nThis repository includes an MCP-only Cursor plugin package that connects to\n`https://inkbox.ai/mcp/cursor`. After Marketplace publication, install\n**Inkbox** from Cursor's **Customize** view, enable the Inkbox MCP server, and\nselect **Connect** to authorize an organization and identity in the browser. No\nAPI key, client secret, local process, or bundled Cursor skills are required.\n\n---\n\n## Quick Start\n\nGet an API key from the [Inkbox Console](https://inkbox.ai/console), then:\n\n### Python\n\n```python\nfrom inkbox import Inkbox\n\nwith Inkbox(api_key=\"ApiKey_...\") as inkbox:\n    # Create an agent identity with a linked mailbox\n    identity = inkbox.create_identity(\"my-agent\", display_name=\"My Agent\")\n    identity.provision_phone_number()  # provisions a local number\n\n    # Send an email\n    identity.send_email(\n        to=[\"user@example.com\"],\n        subject=\"Hello\",\n        body_text=\"Hi from my agent!\",\n    )\n\n    # List recent emails\n    for msg in identity.iter_emails():\n        print(msg.subject, msg.from_address)\n\n    # Place a phone call\n    call = identity.place_call(to_number=\"+15551234567\")\n\n    # Send a text message (SMS/MMS); pass a list for group MMS.\n    identity.send_text(to=\"+15551234567\", text=\"Hi from my agent!\")\n\n    # Read text messages\n    for t in identity.list_texts():\n        print(t.remote_phone_number, t.text)\n\n    # Reply over iMessage (identity must be iMessage-enabled and the\n    # recipient connected to it via the shared triage line first)\n    identity.send_imessage(to=\"+15551234567\", text=\"Hi over iMessage!\")\n```\n\n### TypeScript\n\n```typescript\nimport { Inkbox } from \"@inkbox/sdk\";\n\nconst inkbox = new Inkbox({ apiKey: \"ApiKey_...\" });\n\n// Create an agent identity with a linked mailbox\nconst identity = await inkbox.createIdentity(\"my-agent\", { displayName: \"My Agent\" });\nconst phone = await identity.provisionPhoneNumber(); // provisions a local number\n\n// Send an email\nawait identity.sendEmail({\n  to: [\"user@example.com\"],\n  subject: \"Hello\",\n  bodyText: \"Hi from my agent!\",\n});\n\n// List recent emails\nfor await (const msg of identity.iterEmails()) {\n  console.log(msg.subject, msg.fromAddress);\n}\n\n// Place a phone call\nconst call = await identity.placeCall({ toNumber: \"+15551234567\" });\n\n// Send a text message (SMS/MMS); pass an array for group MMS.\nawait identity.sendText({ to: \"+15551234567\", text: \"Hi from my agent!\" });\n\n// Read text messages\nconst texts = await identity.listTexts();\nfor (const t of texts) {\n  console.log(t.remotePhoneNumber, t.text);\n}\n\n// Reply over iMessage (identity must be iMessage-enabled and the\n// recipient connected to it via the shared triage line first)\nawait identity.sendIMessage({ to: \"+15551234567\", text: \"Hi over iMessage!\" });\n```\n\n### CLI\n\n```bash\n# Create an agent identity (mailbox is created automatically)\ninkbox identity create my-agent\n\n# Send an email\ninkbox email send -i my-agent \\\n  --to user@example.com \\\n  --subject \"Hello\" \\\n  --body-text \"Hi from my agent!\"\n\n# List recent emails\ninkbox email list -i my-agent --limit 10\n\n# Save an incomplete draft, then list it with its current generation\ninkbox email drafts create -i my-agent --subject \"Work in progress\" \\\n  --idempotency-key draft-create-2026-08-19-1\ninkbox email drafts list -i my-agent\n\n# Place a phone call\ninkbox phone call -i my-agent --to +15551234567\n\n# Or call over an existing shared iMessage connection (no dedicated number)\ninkbox phone call -i my-agent --to +15551234567 \\\n  --origination shared_imessage_number\n\n# Override one Voice AI call to use broader authority. This requires an admin\n# API key unless the identity's saved authority is already yolo.\ninkbox phone call -i my-agent --to +15551234567 \\\n  --hosted --reason \"Coordinate the appointment and send confirmations.\" \\\n  --authority-mode yolo\n\n# Send a text message (SMS/MMS; comma-separate --to for groups)\ninkbox text send -i my-agent --to +15551234567 --text \"Hi from my agent!\"\n\n# Reply over iMessage (identity must be iMessage-enabled and the\n# recipient connected to it via the shared triage line first)\ninkbox imessage send -i my-agent --to +15551234567 --text \"Hi over iMessage!\"\n\n# Read text messages\ninkbox text list -i my-agent\n\n# Initialize vault (first time only — requires INKBOX_VAULT_KEY)\ninkbox vault init --vault-key \"my-vault-key\"\n\n# Manage vault secrets\ninkbox vault create --name \"CRM Login\" --type login --username bot@crm.com --password s3cret\ninkbox vault secrets\ninkbox vault get <secret-id>\n```\n\n### Voice AI voice discovery\n\nDiscover the voice catalog for your organization, including the default voice,\navailability, and optional preview URLs. No identity ID is needed:\n\n| Client | Catalog |\n|---|---|\n| Python | `inkbox.hosted_agent.list_voices()` |\n| TypeScript | `await inkbox.hostedAgent.listVoices()` |\n| Rust | `client.hosted_agent().list_voices()?` |\n| CLI | `inkbox phone hosted-agent voices --json` |\n\nThe response contains `voices` and `default_voice` (`defaultVoice` in\nTypeScript and CLI JSON). Each voice has `id`, `name`, `description`,\n`available`, and optional `preview_url` (`previewUrl`). Unavailable voices\nremain in the catalog; choose an entry with `available: true` and pass its\nstring ID to the existing hosted-agent config setter. Setters replace the\nvoice and instructions, so preserve the saved instructions when changing only\nthe voice. Discovery requires an API that supports the catalog endpoint;\nexisting string-based voice selection does not require this new method.\n\n### A2A discovery and history\n\nEach identity can inspect work it received, work it requested, or both without\nduplicating task records. Task lists are newest-first and cursor-paginated.\nKeyword search matches string and numeric content values from `text` and\n`data` parts; message metadata is not searched, and results remain\nnewest-first. A message's `role` is its author (`caller` or `agent`),\nindependent of task direction.\n\nEnabled same-organization identities can call each other without contact rules.\nPublic agents accept enabled callers that allow public egress. Private\ncross-organization calls require the requester to allow the worker outbound and\nthe worker to allow the requester inbound; explicit blocks always win.\n\nSearch enabled agents in your organization or agents that opted into the public\ndirectory. Results include typed Agent Cards and opaque cursor pagination:\n\n```python\npublic_agents = inkbox.a2a.public_directory(q=\"research\", limit=25)\norganization_agents = inkbox.a2a.organization_directory(q=\"support\")\n```\n\nAn A2A context is a shared collaboration between its original two\nparticipants. Either participant can start another task in that context, and\ntasks in both directions may run concurrently. The context's top-level caller\nand target remain the original opener and recipient; each task's caller and\ntarget identify that task's direction. New contexts start as `New A2A Session`;\nthat exact default may be replaced with a short name based on the first task\nmessage. Either participant can rename the persisted value at any time.\n\n```python\nidentity = inkbox.get_identity(\"coordinator\")\n\npage = identity.a2a_tasks(\n    direction=\"both\",\n    worker_handle=\"researcher\",\n    q=\"quarterly summary\",\n)\nfor task in page.items:\n    print(task.id, task.state)\n\nfor message in identity.iter_a2a_messages(\n    direction=\"outbound\",\n    worker_handle=\"researcher\",\n    q=\"revenue\",\n):\n    print(message.task_id, message.task_state, message.role, message.parts)\n\nfor context in identity.a2a_contexts(direction=\"both\").items:\n    print(context.name, context.id)\n\nidentity.a2a_update_context(\n    \"context-uuid\",\n    name=\"Quarterly Research Review\",\n)\n```\n\n```bash\ninkbox a2a directory --public --query research --limit 25\ninkbox a2a directory --query support\ninkbox a2a tasks -i coordinator --direction both --worker researcher\ninkbox a2a messages -i coordinator --direction outbound \\\n  --worker researcher --query revenue --json\n```\n\nTask detail includes current state and message history.\n\n### Tunnels (Python)\n\n```python\n# Bring a local server online at https://my-app.inkboxwire.com.\n# Outbound HTTP/2 only — no inbound port to open. POSIX only.\nlistener = inkbox.tunnels.connect(name=\"my-app\", forward_to=\"http://127.0.0.1:8080\")\nprint(listener.public_url)\nprint(listener.status, listener.is_connected, listener.last_connected_at)\nlistener.wait()\n```\n\n### Mailbox Imports\n\nImport MBOX and EML files, or a ZIP holding either (a Gmail Takeout ZIP imports\nas-is), through `mailboxes.imports`. Create a job, upload directly with the\nreturned target, start it, then wait for any terminal state (`completed`,\n`failed`, or `cancelled`). Entries in a ZIP that are not mail, including nested\narchives, are ignored. Imported content that is unsafe may be rejected and\ncounted separately.\n\n```bash\ninkbox mailbox imports run agent@inkboxmail.com ./archive.mbox \\\n  --original-address old-address@example.com\n```\n\nUse `--no-wait` to return after queueing, or `inkbox mailbox imports wait\n<email> <job-id>` to resume watching later. Processing counters are cumulative\nand never go backwards, but they can sit unchanged while a large message is\nprocessed, and they do not yield a percentage. Jobs run one at a time per\norganization and share overall import capacity, so a long `queued` stretch is\nnormal rather than a stall.\n\nLimits: 1 GiB per upload, 50 MiB per message, 100,000 messages and 20\n`original_addresses` per job, 65,000 entries per ZIP, and 20 import jobs per\norganization per 24 hours. Upload targets expire after 5 minutes and can be\nre-issued; a job whose upload never arrives is cancelled after 24 hours.\n\n### Email Drafts\n\nPython, TypeScript, Rust, and the CLI support creating, listing, reading,\nupdating, duplicating, deleting, and sending saved drafts. Drafts may be\nincomplete. Each draft response includes a `generation`; pass that exact value\nto the next mutation so edits from another client are not overwritten.\n\nDrafts share the mailbox's standard Drafts folder, so changes made through the\nSDK or CLI are visible in connected mail clients and vice versa. Attachment\n`part_index` / `partIndex` values belong to the generation that returned them;\nrefresh the draft before using an index after any edit.\n\nA successful send returns the sent message and removes the draft. Retrying send\nwith the same draft ID and exact generation may return that same sent message.\nHTTP 409 responses carry a structured `error`: refresh before retrying\n`draft_generation_conflict`, retry the same generation for\n`draft_send_in_progress`. Do not resend `draft_delivery_uncertain`; after\nchecking sent mail, duplicate or delete that draft instead.\n\n### Tunnels (TypeScript)\n\n```typescript\nimport { connect } from \"@inkbox/sdk/tunnels/connect\";\n\nconst listener = await connect(inkbox, {\n  name: \"my-app\",\n  forwardTo: \"http://127.0.0.1:8080\",\n});\nconsole.log(listener.publicUrl);\nconsole.log(listener.status, listener.isConnected, listener.lastConnectedAt);\nawait listener.wait();\n```\n\n### Tunnels (Rust)\n\nThe blocking Rust runtime is behind the `tunnels-runtime` feature. Run it on a\ncaller-owned thread and retain a cloneable local status handle:\n\n```rust\nuse inkbox::tunnels::client::TunnelStatusHandle;\n\nlet status = TunnelStatusHandle::new();\nlet runtime_status = status.clone();\nlet client = inkbox.clone();\nlet tunnel_thread = std::thread::spawn(move || {\n    client.tunnels().connect_with_status(\n        \"my-app\",\n        \"http://127.0.0.1:8080\",\n        runtime_status.callback(),\n    )\n});\nprintln!(\"{:?} {}\", status.status(), status.is_connected());\n\n// Join when shutdown is expected so startup/runtime errors are surfaced.\nif let Err(error) = tunnel_thread.join().expect(\"tunnel thread panicked\") {\n    eprintln!(\"tunnel stopped: {error}\");\n}\n```\n\nPython and TypeScript also accept an in-process callable (Fetch handler in TS,\nASGI app in Python) instead of a `forward_to` URL. All three runtimes reconnect\nafter transient connection failures. See [`skills/inkbox-tunnels/`](./skills/inkbox-tunnels/)\nfor the full reference.\n\n**Redeploys are graceful.** When the tunnel service redeploys, a long-running listener reconnects make-before-break: it stands up a fresh connection before closing the draining one, so short HTTP requests see no gap. In-progress WebSocket and passthrough-TCP sessions cannot migrate across a redeploy — they end with a typed `server_draining` close and the third-party peer reconnects onto the new task. Write handlers to reconnect idempotently.\n\n### Outbound SMS — current limits\n\n- Outbound SMS works only from **local** numbers (not toll-free).\n- **100 recipient sends per phone number per rolling 24h.** A 3-recipient group message counts as 3 recipient sends. A single accepted send may push usage past the cap; the next capped send fails with `429 sender_rate_limited`.\n- A new local number waits **~10-15 minutes** for the 10DLC campaign to propagate at the carrier; until then `phone_number.sms_status` (Python) / `phoneNumber.smsStatus` (TS) is `\"pending\"` and sends fail with `409 sender_sms_pending`.\n- Recipients must text **`START`** to any number in your organization to opt in. Unknown recipients fail with `403 recipient_not_opted_in`; opt-outs (`STOP`) return `403 recipient_opted_out`.\n- **Beta:** Group MMS and conversation sends are beta. Some carriers may reject group chats or MMS from 10DLC numbers even when the sender is ready and recipients have opted in.\n\nCustomer-managed 10DLC brands and campaigns lift the default per-number cap to the carrier-assigned tier. Toll-free SMS sending is still coming soon.\n\n---\n\n## Agent Signup\n\nAgents can self-register without a pre-existing API key. The flow provisions a mailbox, identity, and API key in one call:\n\n### Python\n\n```python\nimport os\nfrom inkbox import Inkbox\n\n# 1. Sign up (no API key needed)\nresult = Inkbox.signup(\n    human_email=\"john@example.com\",\n    note_to_human=\"Hey John, this is your agent signing up!\",\n    display_name=\"My Agent\",          # optional\n    agent_handle=\"my-agent\",          # optional\n    email_local_part=\"my.agent\",      # optional\n    invitation_token=os.getenv(\"INKBOX_A2A_INVITATION\"),  # optional link or raw token\n)\napi_key = result.api_key  # save this — shown only once\nprint(result.message)     # authoritative delivery/acceptance outcome\n\n# 2. Verify only when signup did not already accept and claim the invitation\nalready_claimed = (\n    result.invitation is not None and result.invitation.status == \"accepted\"\n) or result.claim_status == \"agent_claimed\"\nif not already_claimed:\n    Inkbox.verify_signup(api_key, verification_code=\"483921\")\n\n# 3. Use the API key\nwith Inkbox(api_key=api_key) as inkbox:\n    identity = inkbox.get_identity(result.agent_handle)\n    identity.send_email(to=[\"john@example.com\"], subject=\"Hello!\", body_text=\"I'm set up.\")\n```\n\n### TypeScript\n\n```typescript\nimport { Inkbox } from \"@inkbox/sdk\";\n\n// 1. Sign up (no API key needed)\nconst result = await Inkbox.signup({\n  humanEmail: \"john@example.com\",\n  noteToHuman: \"Hey John, this is your agent signing up!\",\n  displayName: \"My Agent\",      // optional\n  agentHandle: \"my-agent\",      // optional\n  emailLocalPart: \"my.agent\",   // optional\n  invitationToken: process.env.INKBOX_A2A_INVITATION, // optional link or raw token\n});\nconst apiKey = result.apiKey; // save this — shown only once\nconsole.log(result.message);  // authoritative delivery/acceptance outcome\n\n// 2. Verify only when signup did not already accept and claim the invitation\nconst alreadyClaimed = result.invitation?.status === \"accepted\"\n  || result.claimStatus === \"agent_claimed\";\nif (!alreadyClaimed) {\n  await Inkbox.verifySignup(apiKey, { verificationCode: \"483921\" });\n}\n\n// 3. Use the API key\nconst inkbox = new Inkbox({ apiKey });\nconst identity = await inkbox.getIdentity(result.agentHandle);\nawait identity.sendEmail({ to: [\"john@example.com\"], subject: \"Hello!\", bodyText: \"I'm set up.\" });\n```\n\n### CLI\n\n```bash\n# 1. Sign up (no --api-key needed)\ninkbox signup create --human-email john@example.com \\\n  --note-to-human \"Hey John, this is your agent signing up!\" \\\n  --display-name \"My Agent\" \\\n  --agent-handle my-agent \\\n  --email-local-part my.agent\n# When invited, use --invitation-prompt, --invitation-stdin, or\n# INKBOX_A2A_INVITATION (the legacy token-named environment variable remains supported).\n\n# 2. Verify only if signup did not report an accepted/claimed invitation\ninkbox signup verify --code 483921\n\n# 3. Check status\ninkbox signup status\n```\n\nUse an admin-scoped API key to create and manage invitations through\n`inkbox.a2a_invitations` (Python), `inkbox.a2aInvitations` (TypeScript), or\n`inkbox a2a invites`. Acceptance is agent-only and automatically makes the\nbilateral A2A contact rules needed for the invited peer bundle.\n\n---\n\n## What's in this repo\n\n| Directory | Description |\n|---|---|\n| [`sdk/python/`](./sdk/python/) | Python SDK (`inkbox`) |\n| [`sdk/typescript/`](./sdk/typescript/) | TypeScript SDK (`@inkbox/sdk`) |\n| [`cli/`](./cli/) | CLI (`@inkbox/cli`) |\n| [`skills/inkbox-python/`](./skills/inkbox-python/) | Python agent skill for Claude Code and other coding agents |\n| [`skills/inkbox-ts/`](./skills/inkbox-ts/) | TypeScript agent skill for Claude Code and other coding agents |\n| [`skills/inkbox-onboarding/`](./skills/inkbox-onboarding/) | Language-agnostic identity and channel onboarding skill |\n| [`skills/inkbox-tunnels/`](./skills/inkbox-tunnels/) | Tunnels skill — bring a local server online at a public Inkbox URL |\n| [`examples/use-inkbox-browser-use/`](./examples/use-inkbox-browser-use/) | Inkbox + Browser Use — give your agent an email, phone, and vault |\n| [`examples/use-inkbox-kernel/`](./examples/use-inkbox-kernel/) | Inkbox + Kernel — give your agent an email and browser |\n| [`examples/use-inkbox-cli/`](./examples/use-inkbox-cli/) | Shell script examples for CLI automation and CI pipelines |\n| [`examples/use-inkbox-vault/`](./examples/use-inkbox-vault/) | Vault TOTP example — create credentials with one-time codes |\n| [`examples/use-inkbox-signup/`](./examples/use-inkbox-signup/) | Agent self-signup — register without an API key, verify, send welcome email |\n| [`examples/use-inkbox-webhook/`](./examples/use-inkbox-webhook/) | Inbound email webhook — tunnel + signature verification + auto-reply |\n\n---\n\n## Agent Skills\n\nLoad the Inkbox skills into your coding agent so it automatically knows how to use the SDK.\n\n### Claude Code (plugin)\n\n```\n/plugin marketplace add inkbox-ai/inkbox   # <github-owner>/<repo>\n/plugin install inkbox@inkbox              # <plugin-name>@<marketplace-name>\n/reload-plugins\n```\n\nThe plugin loads the Inkbox skills and connects the remote MCP server. Sign in when prompted to authorize an identity.\n\nThe plugin's version tracks the SDK and CLI release it documents, so `/plugin update` pulls the skills that match the version you're running.\n\n### Codex (plugin)\n\n```bash\ncodex plugin marketplace add inkbox-ai/inkbox\n```\n\nThen install `inkbox` from the Codex plugin UI. See the [Codex plugin docs](https://developers.openai.com/codex/plugins/build) for current installation options.\n\n### Any Agent (individual skills)\n\n```bash\nnpx skills add inkbox-ai/inkbox/skills\n```\n\nSee [`skills/README.md`](./skills/README.md) for details.\n\n---\n\n## Documentation\n\n- [Inkbox Docs](https://inkbox.ai/docs)\n- [API Reference](https://inkbox.ai/docs/api-reference)\n- [Console](https://inkbox.ai/console)\n\n## Releasing\n\nMaintainers: see [RELEASING.md](./RELEASING.md) for the lockstep version-bump and per-registry publish steps (PyPI, npm, crates.io).\n\n## License\n\nMIT\n",
  "bytes": 20541,
  "sha": "b731c701cb78fc4f9dadc59dbfac11382bf14270eb29f1a8874e921f0d5b8ae7",
  "repo_slug": "inkbox-ai/inkbox",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_inkbox_ai_inkbox_inkbox_3cf94126/readme"
}