Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Send and receive emails and phone calls via Inkbox agent identities. Use when the user wants to check inbox messages, list unread email, view a thread, search mailbox contents, draft/send an email, place an outbound phone call, list call history, retrieve call transcripts, manage vault credentials, or create/set up an Inkbox identity.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 234% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 433% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 186% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 181% | 0% |
API-first communication infrastructure for AI agents — email, phone, encrypted vault, and identities.
INKBOX_API_KEY — Inkbox API keynode on PATH (Node.js 18+)INKBOX_AGENT_HANDLE is optional; use it when already configured, otherwise ask the user which identity handle to use or createDo not assume @inkbox/sdk is already installed in the skill folder.
When the SDK is missing, prefer a temporary disposable Node directory over modifying the workspace or skill folder. Use a flow like:
npm init -ynpm install @inkbox/sdk.mjs script therenodeOnly install dependencies into the skill folder or workspace if the user explicitly asks.
Use .mjs scripts with standard ESM imports. Avoid relying on tsx --eval or top-level-await snippets that may be runtime-fragile.
bashnpm install @inkbox/sdk
Requires Node.js ≥ 18. ESM module — no context manager needed:
jsimport { Inkbox } from "@inkbox/sdk"; const inkbox = new Inkbox({ apiKey: process.env.INKBOX_API_KEY });
Constructor options: { apiKey: string, baseUrl?: string, timeoutMs?: number }
Inkbox (org-level client)
├── .createIdentity(handle) → Promise<AgentIdentity>
├── .getIdentity(handle) → Promise<AgentIdentity>
├── .listIdentities() → Promise<AgentIdentitySummary[]>
├── .mailboxes → MailboxesResource
├── .phoneNumbers → PhoneNumbersResource
├── .vault → VaultResource
└── .createSigningKey() → Promise<SigningKey>
AgentIdentity (identity-scoped helper)
├── .mailbox → IdentityMailbox | null
├── .phoneNumber → IdentityPhoneNumber | null
├── .getCredentials() → Promise<Credentials> (requires vault unlocked)
├── mail methods (requires assigned mailbox)
└── phone methods (requires assigned phone number)An identity must have a channel assigned before you can use mail/phone methods. If not assigned, an InkboxAPIError is thrown.
jsconst identity = await inkbox.createIdentity("sales-agent"); const identity = await inkbox.getIdentity("sales-agent"); const identities = await inkbox.listIdentities(); // AgentIdentitySummary[] await identity.update({ newHandle: "new-name" }); // rename await identity.update({ status: "paused" }); // or "active" await identity.refresh(); // re-fetch from API, updates cached channels await identity.delete(); // unlinks channels
If INKBOX_AGENT_HANDLE is not configured, ask the user for the handle to use.
After creating a new identity:
skills.entries.<skill>.env.INKBOX_AGENT_HANDLEskills.entries.<skill>.apiKey with a SecretRef to INKBOX_API_KEYjs// Create and auto-link new channels const mailbox = await identity.createMailbox({ displayName: "Sales Agent" }); const phone = await identity.provisionPhoneNumber({ type: "toll_free" }); // or type: "local", state: "NY" console.log(mailbox.emailAddress); // e.g. "abc-xyz@inkboxmail.com" console.log(phone.number); // e.g. "+18005551234" // Link existing channels await identity.assignMailbox("mailbox-uuid"); await identity.assignPhoneNumber("phone-number-uuid"); // Unlink without deleting await identity.unlinkMailbox(); await identity.unlinkPhoneNumber();
Before sending, confirm recipients, subject, and body with the user.
jsconst sent = await identity.sendEmail({ to: ["user@example.com"], subject: "Hello", bodyText: "Hi there!", // plain text (optional) bodyHtml: "<p>Hi there!</p>", // HTML (optional) cc: ["cc@example.com"], // optional bcc: ["bcc@example.com"], // optional inReplyToMessageId: sent.id, // for threaded replies attachments: [{ // optional filename: "report.pdf", contentType: "application/pdf", contentBase64: "<base64>", }], });
js// Iterate all messages — auto-paginated async generator for await (const msg of identity.iterEmails()) { console.log(msg.subject, msg.fromAddress, msg.isRead); } // Filter by direction for await (const msg of identity.iterEmails({ direction: "inbound" })) { // or "outbound" ... } // Unread only (client-side filtered) for await (const msg of identity.iterUnreadEmails()) { ... } // Mark as read const ids = []; for await (const msg of identity.iterUnreadEmails()) ids.push(msg.id); await identity.markEmailsRead(ids); // Get full thread (oldest-first) const thread = await identity.getThread(msg.threadId); for (const m of thread.messages) { console.log(`[${m.fromAddress}] ${m.subject}`); }
js// Org-level mailbox search const results = await inkbox.mailboxes.search(identity.mailbox.emailAddress, { q: "invoice", limit: 20, });
This operation requires the identity to already have a mailbox provisioned.
js// Place outbound call — stream audio via WebSocket const call = await identity.placeCall({ toNumber: "+15167251294", clientWebsocketUrl: "wss://your-agent.example.com/ws", }); console.log(call.status); console.log(call.rateLimit.callsRemaining); // rolling 24h budget // List calls (offset pagination) const calls = await identity.listCalls({ limit: 10, offset: 0 }); for (const c of calls) { console.log(c.id, c.direction, c.remotePhoneNumber, c.status); } // Transcript segments (ordered by seq) const segments = await identity.listTranscripts(calls[0].id); for (const t of segments) { console.log(`[${t.party}] ${t.text}`); // party: "local" or "remote" }
Always confirm before placing a call.
Encrypted credential vault with client-side Argon2id key derivation and AES-256-GCM encryption. The server never sees plaintext secrets. Requires hash-wasm (included as a dependency).
jsimport type { LoginPayload, APIKeyPayload, SSHKeyPayload, OtherPayload } from "@inkbox/sdk"; // Unlock with a vault key — derives key via Argon2id, decrypts all secrets const unlocked = await inkbox.vault.unlock("my-Vault-key-01!"); // Optionally filter to secrets an agent identity has access to const unlocked = await inkbox.vault.unlock("my-Vault-key-01!", { identityId: "agent-uuid" }); // All decrypted secrets from the unlock bundle for (const secret of unlocked.secrets) { console.log(secret.name, secret.secretType); console.log(secret.payload); // LoginPayload, APIKeyPayload, SSHKeyPayload, or OtherPayload } // Fetch and decrypt a single secret by ID const secret = await unlocked.getSecret("secret-uuid"); const login = secret.payload as LoginPayload; console.log(login.username, login.password);
js// Create a login secret (secretType inferred from payload shape) await unlocked.createSecret({ name: "AWS Production", description: "Production IAM user", payload: { password: "s3cret", username: "admin", url: "https://aws.amazon.com" }, }); // Create an API key secret await unlocked.createSecret({ name: "GitHub PAT", payload: { apiKey: "ghp_xxx" }, }); // Create an SSH key secret await unlocked.createSecret({ name: "Deploy Key", payload: { privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----..." }, }); // Create a freeform secret await unlocked.createSecret({ name: "Misc", payload: { data: "any freeform content" }, }); // Update name/description and/or re-encrypt payload await unlocked.updateSecret("secret-uuid", { name: "New Name" }); await unlocked.updateSecret("secret-uuid", { payload: { password: "new", username: "new" }, }); // Delete await unlocked.deleteSecret("secret-uuid");
jsconst info = await inkbox.vault.info(); // VaultInfo const keys = await inkbox.vault.listKeys(); // VaultKey[] const keys = await inkbox.vault.listKeys({ keyType: "recovery" }); // filter by type const secrets = await inkbox.vault.listSecrets(); // VaultSecret[] (metadata only) const secrets = await inkbox.vault.listSecrets({ secretType: "login" }); // filter by type await inkbox.vault.deleteSecret("secret-uuid"); // delete without unlocking
| Type | Interface | Fields | |------|-----------|--------| | login | LoginPayload | password, username?, email?, url?, notes?, totp? | | api_key | APIKeyPayload | apiKey, endpoint?, notes? | | key_pair | KeyPairPayload | accessKey, secretKey, endpoint?, notes? | | ssh_key | SSHKeyPayload | privateKey, publicKey?, fingerprint?, passphrase?, notes? | | other | OtherPayload | data |
secretType is immutable after creation. To change it, delete and recreate.
Agent-facing credential access — typed, identity-scoped. The vault stays as the admin surface; identity.getCredentials() is the agent runtime surface.
jsimport type { Credentials } from "@inkbox/sdk"; // Unlock the vault first (stores state on the client) await inkbox.vault.unlock("my-Vault-key-01!"); const identity = await inkbox.getIdentity("support-bot"); const creds = await identity.getCredentials(); // Discovery — returns DecryptedVaultSecret[] with name/metadata const allCreds = creds.list(); const logins = creds.listLogins(); const apiKeys = creds.listApiKeys(); const sshKeys = creds.listSshKeys(); // Access by UUID — returns typed payload directly const login = creds.getLogin("secret-uuid"); // → LoginPayload const apiKey = creds.getApiKey("secret-uuid"); // → APIKeyPayload const sshKey = creds.getSshKey("secret-uuid"); // → SSHKeyPayload // Generic access — returns DecryptedVaultSecret const secret = creds.get("secret-uuid");
inkbox.vault.unlock() first — throws InkboxAPIError if vault is not unlockedidentity.refresh() to clear the cacheget* throws Error if not found, TypeError if wrong secret typeTOTP secrets are stored inside LoginPayload.totp in the encrypted vault. Codes are generated client-side — no server call needed.
jsimport { parseTotpUri } from "@inkbox/sdk"; import type { LoginPayload } from "@inkbox/sdk"; // Create a login with TOTP const secret = await identity.createSecret({ name: "GitHub", payload: { username: "user@example.com", password: "s3cret", totp: parseTotpUri("otpauth://totp/GitHub:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"), } satisfies LoginPayload, }); // Generate TOTP code const code = await identity.getTotpCode(secret.id); console.log(code.code); // e.g. "482901" console.log(code.secondsRemaining); // e.g. 17 // Add/replace TOTP on existing login await identity.setTotp(secretId, "otpauth://totp/...?secret=..."); // Remove TOTP await identity.removeTotp(secretId);
jsconst unlocked = await inkbox.vault.unlock("my-Vault-key-01!"); // Same methods available on UnlockedVault await unlocked.setTotp(secretId, totpConfigOrUri); await unlocked.removeTotp(secretId); const code = await unlocked.getTotpCode(secretId);
| Field | Type | Description | |---|---|---| | code | string | The OTP code (e.g. "482901") | | periodStart | number | Unix timestamp when the code became valid | | periodEnd | number | Unix timestamp when the code expires | | secondsRemaining | number | Seconds until expiry |
inkbox.mailboxes)jsconst mailboxes = await inkbox.mailboxes.list(); const mailbox = await inkbox.mailboxes.get("abc@inkboxmail.com"); const mb = await inkbox.mailboxes.create({ agentHandle: "support", displayName: "Support Inbox" }); await inkbox.mailboxes.update(mb.emailAddress, { displayName: "New Name" }); await inkbox.mailboxes.update(mb.emailAddress, { webhookUrl: "https://example.com/hook" }); await inkbox.mailboxes.update(mb.emailAddress, { webhookUrl: null }); // remove webhook const results = await inkbox.mailboxes.search(mb.emailAddress, { q: "invoice", limit: 20 }); await inkbox.mailboxes.delete(mb.emailAddress);
inkbox.phoneNumbers)jsconst numbers = await inkbox.phoneNumbers.list(); const number = await inkbox.phoneNumbers.get("phone-number-uuid"); const num = await inkbox.phoneNumbers.provision({ agentHandle: "my-agent", type: "toll_free" }); const local = await inkbox.phoneNumbers.provision({ agentHandle: "my-agent", type: "local", state: "NY" }); await inkbox.phoneNumbers.update(num.id, { incomingCallAction: "webhook", // "webhook", "auto_accept", or "auto_reject" incomingCallWebhookUrl: "https://...", }); await inkbox.phoneNumbers.update(num.id, { incomingCallAction: "auto_accept", clientWebsocketUrl: "wss://...", }); const hits = await inkbox.phoneNumbers.searchTranscripts(num.id, { q: "refund", party: "remote", limit: 50 }); await inkbox.phoneNumbers.release(num.id);
Webhooks are configured directly on the mailbox or phone number — no separate registration.
jsimport { verifyWebhook } from "@inkbox/sdk"; // Rotate signing key (plaintext returned once — save it) const key = await inkbox.createSigningKey(); // Verify an incoming webhook request const valid = verifyWebhook({ payload: req.body, // Buffer or string headers: req.headers as Record<string, string>, secret: "whsec_...", });
Headers checked: x-inkbox-signature, x-inkbox-request-id, x-inkbox-timestamp. Algorithm: HMAC-SHA256 over "{requestId}.{timestamp}.{body}".
jsimport { InkboxAPIError } from "@inkbox/sdk"; try { const identity = await inkbox.getIdentity("unknown"); } catch (e) { if (e instanceof InkboxAPIError) { console.log(e.statusCode); // HTTP status (e.g. 404) console.log(e.detail); // message from API } }
401 Unauthorized, tell the user the API key was rejected and ask them to verify or rotate INKBOX_API_KEYINKBOX_AGENT_HANDLE is missing, ask the user which identity to use or create one firstiterEmails() / iterUnreadEmails() return AsyncGenerator<Message> — use for await...oflistCalls() returns Promise<PhoneCall[]> — offset pagination, not a generatorfield: nullnew Inkbox({...}) is all that's requiredasync and return Promises — always await themthreadId)inReplyToMessageId+15551234567)listCalls can be passed to listTranscriptsOther measured skills in the registry, with their headline benchmark lift.