Install
$ agentstack add skill-inkbox-ai-inkbox-inkbox-ts ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Inkbox TypeScript SDK
API-first communication infrastructure for AI agents — email, phone, encrypted vault, and identities.
Install & Init
npm install @inkbox/sdk
Requires Node.js ≥ 22. ESM module — no context manager needed:
import { Inkbox } from "@inkbox/sdk";
const inkbox = new Inkbox({ apiKey: "ApiKey_..." });
Constructor options: { apiKey: string, baseUrl?: string, timeoutMs?: number }
Core Model
Inkbox (admin-only client)
├── .createIdentity(handle) → Promise
├── .getIdentity(handle) → Promise
├── .listIdentities() → Promise
├── .mailboxes → MailboxesResource
├── .phoneNumbers → PhoneNumbersResource
├── .texts → TextsResource
├── .imessages → IMessagesResource
├── .imessageContactRules → IMessageContactRulesResource
├── .mailIdentityContactRules → MailIdentityContactRulesResource (keyed by agentHandle)
├── .phoneIdentityContactRules → PhoneIdentityContactRulesResource (keyed by agentHandle)
├── .signingKeys → SigningKeysResource (per-identity: createOrRotate/getStatus)
├── .mailContactRules → MailContactRulesResource (DEPRECATED — per-mailbox)
├── .phoneContactRules → PhoneContactRulesResource (DEPRECATED — per-number)
├── .smsOptIns → SmsOptInsResource
├── .contacts → ContactsResource (.access, .vcards)
├── .notes → NotesResource (.access)
├── .vault → VaultResource
├── .whoami() → Promise
└── .createSigningKey() → Promise (DEPRECATED — org-level; use .signingKeys)
AgentIdentity (identity-scoped helper)
├── .mailbox → IdentityMailbox | null
├── .phoneNumber → IdentityPhoneNumber | null
├── .mailFilterMode / .phoneFilterMode → FilterMode
├── .getCredentials() → Promise (requires vault unlocked)
├── .listAccess() → Promise
├── .grantAccess(viewerId|null) → Promise
├── .revokeAccess(viewerId) → Promise
├── .listMailContactRules() / .createMailContactRule(...) / .get/.update/.delete
├── .listPhoneContactRules() / .createPhoneContactRule(...) / ... (requires phone number)
├── .getSigningKeyStatus() / .createSigningKey()
├── mail methods (requires assigned mailbox)
├── phone methods (requires assigned phone number)
└── text methods (requires assigned phone number)
An identity must have a channel assigned before you can use mail/phone methods. If not assigned, an InkboxError is thrown.
Agent Signup
For the full agent self-signup flow (register, verify, check status, restrictions, and direct API examples), read the shared reference:
> See: skills/inkbox-agent-self-signup/SKILL.md
TypeScript SDK methods: Inkbox.signup({...}), Inkbox.verifySignup(apiKey, {...}), Inkbox.resendSignupVerification(apiKey), Inkbox.getSignupStatus(apiKey).
Identities
const 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(); // cascades: mailbox + tunnel + phone-number release
Channel Management
// Identity is created with a mailbox AND tunnel atomically — both are on the response
console.log(identity.emailAddress); // e.g. "sales-agent@inkboxmail.com"
console.log(identity.tunnel?.publicHost); // e.g. "sales-agent.inkboxwire.com"
// Phone numbers are still opt-in
const phone = await identity.provisionPhoneNumber({ type: "toll_free" });
console.log(phone.number); // e.g. "+18005551234"
// Release the phone number (vendor + local)
await identity.releasePhoneNumber();
Mailboxes and tunnels are not separately linkable — they are 1:1 with their owning identity. Use inkbox.createIdentity() to provision both; use identity.delete() to remove both (cascade).
Identity Visibility
Controls which other agent identities can see an identity in API responses. Humans and admins always see every identity.
const rules = await identity.listAccess(); // IdentityAccess[]
// One wildcard row (viewerIdentityId === null → every active identity sees it),
// explicit per-viewer rows, or [] (no agent can see it).
await identity.grantAccess(viewer.id); // grant one viewer identity
await identity.grantAccess(null); // reset to org-wide wildcard
await identity.revokeAccess(viewer.id); // revoke one viewer (keyed by viewer UUID)
Granting a viewer against an already-wildcard target raises RedundantContactAccessGrantError (409); revoking a non-existent grant raises InkboxAPIError (404).
Send
const sent = await identity.sendEmail({
to: ["user@example.com"],
subject: "Hello",
bodyText: "Hi there!", // plain text (optional)
bodyHtml: "Hi there!", // 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: "",
}],
});
Read
// 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: string[] = [];
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}`);
}
Thread Folders
Threads carry a folder field: inbox, spam, archive, or blocked (server-assigned, never client-set).
import { ThreadFolder } from "@inkbox/sdk";
// thread.folder / threadDetail.folder is always one of the four values above.
Low-level folder listing / per-thread updates (list({ folder }), listFolders(email), update(..., { folder })) live on ThreadsResource. Passing folder: "blocked" to update throws before the HTTP call.
Phone
// Place outbound call — stream audio via WebSocket
const call = await identity.placeCall({
toNumber: "+15551234567",
clientWebsocketUrl: "wss://your-agent.example.com/ws",
});
console.log(call.status);
console.log(call.rateLimit.callsRemaining);
// 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"
}
Text Messages (SMS/MMS)
Outbound SMS limits and gates (current):
- Allowed only from local numbers, not toll-free.
- 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 returns
429 sender_rate_limited. - New local numbers need ~10-15 min for 10DLC carrier propagation.
identity.phoneNumber.smsStatusisSmsStatus.PENDINGuntil ready; sends in this window return409 sender_sms_pending. - Recipient must have texted
STARTto any number in the org. Unknown →403 recipient_not_opted_in.STOP→403 recipient_opted_out. Inspect / override consent state viainkbox.smsOptIns(see below). - 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.
Customer-managed 10DLC brands/campaigns lift the default per-number cap to the carrier-assigned tier. Toll-free SMS sending is still coming soon.
// Send SMS/MMS from this identity's phone number.
// Returns a queued TextMessage; final delivery state arrives via any
// webhook subscription on the sender's phone number whose eventTypes
// include the text.* lifecycle events.
const sent = await identity.sendText({
to: "+15551234567",
text: "Hello from Inkbox",
});
console.log(sent.id, sent.deliveryStatus); // "queued"
// Group MMS beta: pass an array of recipients plus optional media URLs.
const group = await identity.sendText({
to: ["+15551234567", "+15557654321"],
text: "Hello group",
mediaUrls: ["https://example.com/photo.jpg"],
});
console.log(group.conversationId, group.recipients);
// Reply to an existing conversation by UUID. Do not pass `to` with this form.
const reply = await identity.sendText({
conversationId: group.conversationId,
text: "Following up in the same conversation.",
});
// List text messages (offset pagination)
const texts = await identity.listTexts({ limit: 20, offset: 0 });
for (const t of texts) {
console.log(t.id, t.direction, t.remotePhoneNumber, t.text, t.isRead);
}
// Filter by read state
const unread = await identity.listTexts({ isRead: false });
// Get a single text message
const text = await identity.getText("text-uuid");
console.log(text.type); // "sms" or "mms"
if (text.media) { // MMS media attachments (temporary signed URLs)
for (const m of text.media) {
console.log(m.contentType, m.size, m.url);
}
}
// List one-to-one conversation summaries; opt into groups explicitly.
const convos = await identity.listTextConversations({ limit: 20, includeGroups: true });
for (const c of convos) {
console.log(c.id, c.participants, c.latestHasMedia, c.latestText);
}
// Get messages in a specific conversation by remote number or conversation UUID.
const msgs = await identity.getTextConversation("+15551234567", { limit: 50 });
// Mark a text as read (identity convenience method)
await identity.markTextRead("text-uuid");
// Mark all messages in a conversation as read
const readResult = await identity.markTextConversationRead("+15551234567");
console.log(readResult.updatedCount);
// Admin-only: search, update, delete
const results = await inkbox.texts.search(phone.id, { q: "invoice", limit: 20 });
await inkbox.texts.update(phone.id, "text-uuid", { status: "deleted" });
iMessage
iMessage works differently from SMS: there is no per-identity iMessage number. Recipients connect to an agent identity through a small shared pool of numbers — they ask the triage line to connect them to @agent_handle, and that creates an assignment between that one recipient and the identity. Everything agent-facing is keyed by conversationId / remoteNumber; the shared local number is never exposed, and there is no cold outreach — you can only message recipients who connected first.
Discover the router (triage) line at runtime — it can change, so never hardcode it:
const triage = await inkbox.imessages.getTriageNumber();
console.log(triage.number, triage.connectCommand); // "+1646...", "connect @your-handle"
// Humans connect by texting that command to that number.
Reachability is opt-in per identity (imessageEnabled, default false):
const identity = await inkbox.createIdentity("my-agent", { imessageEnabled: true });
// or toggle later
await identity.update({ imessageEnabled: true });
// admin-only: flip contact-rule mode (default "blacklist")
await identity.update({ imessageFilterMode: "whitelist" });
console.log(identity.imessageEnabled, identity.imessageFilterMode);
Messaging (identity convenience methods; inkbox.imessages is the org-level resource with the same operations plus agentIdentityId / isBlocked filters):
// Send to a connected recipient, or reply into a conversation by UUID.
const sent = await identity.sendIMessage({ to: "+15551234567", text: "Hello over iMessage" });
const reply = await identity.sendIMessage({
conversationId: sent.conversationId,
text: "With style",
sendStyle: "slam", // IMessageSendStyle: confetti, lasers, slam, ...
});
console.log(sent.service, sent.status); // "imessage", "queued"
// List messages / conversations
const msgs = await identity.listIMessages({ limit: 20, isRead: false });
const convos = await identity.listIMessageConversations({ limit: 20 });
const convo = await identity.getIMessageConversation(sent.conversationId);
// assignmentStatus tells you whether the recipient is still connected:
// anything other than "active" means sends/reactions will be refused
// until they reconnect through triage.
console.log(convo.assignmentStatus);
// Who is actively connected to this identity right now (paginated)?
const connections = await identity.listIMessageAssignments({ limit: 20 });
for (const a of connections) {
console.log(a.remoteNumber, a.status, a.createdAt);
}
// Tapback reactions. Sends accept the classic six (love, like, dislike,
// laugh, emphasize, question); inbound can also be "custom" with the
// literal emoji in customEmoji.
await identity.sendIMessageReaction({ messageId: msgs[0].id, reaction: "like" });
// Live tapbacks come back on message reads, oldest first.
for (const r of msgs[0].reactions ?? []) {
console.log(r.direction, r.reaction, r.customEmoji);
}
// Read receipts + typing indicator
await identity.markIMessageConversationRead(sent.conversationId);
await identity.sendIMessageTyping(sent.conversationId);
// Media: upload bytes (max 10 MiB), then send the returned URL (one per message)
const upload = await identity.uploadIMessageMedia({
content: await readFile("photo.jpg"),
filename: "photo.jpg",
contentType: "image/jpeg",
});
await identity.sendIMessage({ to: "+15551234567", mediaUrls: [upload.mediaUrl] });
Contact rules are scoped to the identity (not a phone number) because pool numbers are shared infrastructure:
import { IMessageRuleAction } from "@inkbox/sdk";
const rule = await inkbox.imessageContactRules.create("my-agent", {
action: IMessageRuleAction.BLOCK,
matchTarget: "+15559999999",
});
const rules = await inkbox.imessageContactRules.list("my-agent");
await inkbox.imessageContactRules.update("my-agent", rule.id, { status: "paused" }); // admin-only
await inkbox.imessageContactRules.delete("my-agent", rule.id); // admin-only
const allRules = await inkbox.imessageContactRules.listAll(); // admin-only, org-wide
Inbound messages and reactions arrive via identity-owned webhook subscriptions — see Webhooks below.
SMS Opt-Ins
Per-recipient SMS consent state, keyed by (your org, recipient number). The registry is updated automatically when recipients text START / STOP to any of your numbers (source: "sms"). Reads are admin-only; writes are admin-only and require your org to be on its own active, customer-managed 10DLC campaign (Inkbox-default-campaign orgs share consent state and get 409 customer_campaign_required on writes — source: "api" writes record an audit event).
import { SmsOptInStatus } from "@inkbox/sdk";
// List your org's consent rows, newest-updated first (server caps limit at 200)
const rows = await inkbox.smsOptIns.list({ limit: 50 });
const optedOut = await inkbox.smsOptIns.list({ status: SmsOptInStatus.OPTED_OUT });
// Look up one recipient — 404 → InkboxA
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [inkbox-ai](https://github.com/inkbox-ai)
- **Source:** [inkbox-ai/inkbox](https://github.com/inkbox-ai/inkbox)
- **License:** MIT
- **Homepage:** https://inkbox.ai/docs
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.