31 releases
| new 0.5.14 | Aug 7, 2026 |
|---|---|
| 0.5.13 | Aug 5, 2026 |
| 0.5.9 | Jul 31, 2026 |
| 0.4.26 | Jul 18, 2026 |
| 0.4.14 | Jun 29, 2026 |
#45 in Email
47 downloads per month
1MB
21K
SLoC
inkbox — Rust SDK
Rust SDK for the Inkbox API — email, SMS/MMS, iMessage, voice, contacts, notes, an encrypted vault, and inbound tunnels for AI agents.
This crate is a faithful port of the Python (inkbox on PyPI) and
TypeScript (@inkbox/sdk on npm) SDKs. The public surface is
blocking (built on reqwest::blocking), matching the synchronous Python/TS
APIs. JSON field names, enum values, request bodies, query params, and paths
match the other SDKs exactly — they all speak to the same server.
Install
[dependencies]
inkbox = "0.5"
The tunnels data-plane runtime is behind an optional feature:
[dependencies]
inkbox = { version = "0.5", features = ["tunnels-runtime"] }
Quickstart
use inkbox::Inkbox;
fn main() -> inkbox::Result<()> {
// The client is handed out as an `Arc<Inkbox>` (the per-identity facade
// and the tunnels resource hold a back-reference to it).
let inkbox = Inkbox::new("ApiKey_...")?;
// Create an agent identity (atomically provisions a mailbox + tunnel).
let identity = inkbox.create_identity("support-bot")?;
// Send an email from the identity's mailbox.
identity.send_email(
&["customer@example.com".into()],
"Hello!",
Some("Hi there"),
None, None, None, None, None, false,
)?;
// Read the inbox.
for msg in identity.iter_emails(None, None)? {
println!("{:?}", msg.subject);
}
Ok(())
}
Voice AI authority
Voice AI calls inherit the identity's saved authority when no per-call override
is supplied. An explicit ContactScoped override always downscopes the call.
An explicit Yolo override requires an admin credential unless the saved
authority is already Yolo. Changing the saved default requires an admin API
key:
use inkbox::phone::{CallOrigin, HostedAgentAuthorityMode};
let call = identity.place_hosted_call(
"+15551234567",
CallOrigin::DedicatedNumber,
"Coordinate the appointment and send confirmations.",
)?;
identity.set_hosted_agent_authority_mode(
HostedAgentAuthorityMode::Yolo,
)?;
let scoped_call = identity.place_hosted_call_with_authority(
"+15551234567",
CallOrigin::DedicatedNumber,
"Confirm only this caller's appointment.",
HostedAgentAuthorityMode::ContactScoped,
)?;
Advanced construction
use inkbox::Inkbox;
let inkbox = Inkbox::builder("ApiKey_...")
.base_url("https://inkbox.ai") // self-hosting / tests (HTTPS, or localhost over HTTP)
.timeout_secs(30.0)
.vault_key("my-Vault-key-01!") // unlock the vault at construction
.build()?;
Construction from the environment
// Resolves api_key / base_url / vault_key from the matching env var
// (INKBOX_API_KEY / INKBOX_BASE_URL / INKBOX_VAULT_KEY), then ~/.inkbox/config
// (`key = value` lines). Handy for background/agent processes that don't
// inherit the shell's env. Errors if no API key is found.
let inkbox = Inkbox::from_env()?;
Vault credentials
let inkbox = Inkbox::builder("ApiKey_...").vault_key("my-Vault-key-01!").build()?;
let identity = inkbox.get_identity("support-bot")?;
for login in identity.credentials()?.list_logins() {
println!("{}", login.name);
}
Surface
Org-level accessors on Inkbox mirror the Python @property names:
| Domain | Accessor |
|---|---|
mailboxes(), messages(), threads(), mail_identity_contact_rules(), mail_contact_rules() (deprecated), domains() |
|
| Phone | calls(), phone_numbers(), texts(), incoming_call_action(), phone_identity_contact_rules(), phone_contact_rules() (deprecated), sms_opt_ins() |
| iMessage | imessages(), imessage_contact_rules() |
| Vault / data | vault(), contacts(), notes() |
| Agent-to-agent discovery and history | a2a() |
| Org | api_keys(), identities(), signing_keys(), tunnels(), webhooks() |
Contact rules and webhook signing keys are keyed by agent identity, addressed
by agent_handle. Use mail_identity_contact_rules() /
phone_identity_contact_rules() (per-identity list/get/create/update/
delete plus an org-wide list_all) and signing_keys()
(create_or_rotate(handle) / get_status(handle)). The legacy per-mailbox /
per-number mail_contact_rules() / phone_contact_rules() accessors and the
org-level create_signing_key() are deprecated bridges that remain for
back-compat.
The per-identity facade AgentIdentity (from create_identity / get_identity)
exposes channel-scoped convenience methods: send_email, forward_email,
iter_emails, place_call, send_text, send_imessage, credentials,
create_secret, set_totp, the identity-keyed contact-rule helpers
(list_mail_contact_rules, create_phone_contact_rule, ...), create_signing_key,
and more.
Mailbox imports
use std::time::Duration;
use inkbox::mail::MailImportFormat;
let imports = inkbox.mailboxes().imports();
let created = imports.create(
"agent@inkboxmail.com",
MailImportFormat::Auto,
Some(&["old-address@example.com".to_string()]),
true,
)?;
imports.upload(&created.upload, "./archive.mbox")?;
imports.start("agent@inkboxmail.com", &created.job.id.to_string())?;
let job = imports.wait(
"agent@inkboxmail.com",
&created.job.id.to_string(),
Some(Duration::from_secs(3600)),
Some(Duration::from_secs(5)),
)?;
Imports support MBOX and EML files, or a ZIP holding either (a Gmail Takeout ZIP
imports as-is); ZIP entries that are not mail, including nested archives, are
ignored. wait returns completed, failed, and cancelled jobs, and reports a
local wall-clock timeout as InkboxError::Timeout without cancelling the job.
Counters are cumulative and never go backwards, but they can sit unchanged while
a large message is processed and do not represent a percentage. Jobs run one at
a time per organization and share overall import capacity, so a long queued
stretch is normal. Unsafe imported content may be rejected in
messages_rejected_unsafe.
Upload targets expire after 5 minutes; call refresh_upload_target and upload
again if one expires, or cancel the job so the mailbox is not held by an
upload that never landed. Other limits: 1 GiB per upload, 50 MiB per message,
100,000 messages and 20 original addresses per job, 65,000 entries per ZIP, 20
import jobs per organization per 24 hours (InkboxError::MailImportQuotaExceeded
carries the Retry-After value), and one in-flight import per mailbox.
Dedicated iMessage numbers
List or claim organization-owned dedicated numbers through the iMessage resource:
use inkbox::imessage::IMessageNumberType;
let available = inkbox.imessages().list_numbers()?;
let number = inkbox
.imessages()
.claim_number(IMessageNumberType::DedicatedOutbound, "setup-support-number-v1")?;
assert!(number.can_start_conversation());
A number can also be claimed and attached atomically while creating an identity:
use inkbox::identities::Unset;
use inkbox::imessage::IMessageNumberType;
let identity = inkbox.create_identity_with_imessage_number(
"support-bot",
None,
Unset::Omit,
Some(true),
None,
Unset::Omit,
None,
None,
None,
Some(IMessageNumberType::DedicatedInbound),
)?;
let number = identity.imessage_number().expect("dedicated number");
assert_eq!(number.r#type, IMessageNumberType::DedicatedInbound);
For an existing identity, update_with_imessage_number can attach an already
owned number by id, move back to shared iMessage service with an explicit null,
or claim and attach a new number by type. Claims require a stable 1–255 character
idempotency key; reuse the same key after an ambiguous result. Dedicated outbound
numbers are the only number type that can start a new conversation.
Dedicated outbound identities can also start group conversations. Scalar sends
remain on send_imessage; groups use send_imessage_group, and later replies
use the returned conversation id with send_imessage:
use inkbox::imessage::IMessageSendStyle;
let recipients = vec!["+15551234567".to_string(), "+15557654321".to_string()];
let group_media = vec!["https://example.com/group-photo.jpg".to_string()];
let group = identity.send_imessage_group(
&recipients,
Some("Welcome to the group!"),
Some(&group_media),
Some(IMessageSendStyle::Confetti),
)?;
let reply_media = vec!["https://example.com/follow-up.jpg".to_string()];
identity.send_imessage(
None,
Some(&group.conversation_id),
Some("Following up in the same conversation."),
Some(&reply_media),
Some(IMessageSendStyle::Lasers),
)?;
let conversations = identity.list_imessage_conversations_with_groups(
50,
0,
None,
true,
)?;
println!("{:?}", conversations[0].group_creation_status);
Group creation and conversation-id replies accept the same 13
IMessageSendStyle values as one-to-one sends, with or without the media URL.
List methods exclude groups by default for backwards compatibility. Group
messages expose is_group, a best-known participants snapshot, and
per-recipient delivery state; assignment and one-to-one remote fields are
optional. group_creation_status is Creating, NotCreated, or Ready. A
rejected initial creation keeps the same local conversation at NotCreated;
send again with that conversation id to retry. Success binds the remote thread
and changes the status to Ready.
send_imessage_reaction supports inbound one-to-one and group messages by
message id. The sendable named reactions are love, like, dislike,
laugh, emphasize, question, and eyes; arbitrary custom emoji remain
inbound-only. Group read receipts and typing indicators remain unsupported.
Static (no-client) helpers for the public agent-signup flow live on Inkbox:
Inkbox::signup, verify_signup, resend_signup_verification,
get_signup_status.
Use Inkbox::signup_with for additive options.
Set inkbox::agent_signup::AgentSignupOptions::harness to the current runtime
so a claimed response can return matching plugin guidance in message.
inkbox::agent_signup::AgentSignupOptions::invitation_token accepts an
exact-origin share URL or raw token. inkbox::extract_a2a_invitation_token
uses the production site, while
inkbox::extract_a2a_invitation_token_with_base_url accepts an explicit site.
Only the token is sent during signup. Use this option for invitation-assisted
signup and omit it when you do not have an A2A connection invitation. Signup
and verification responses expose an optional invitation summary; the existing
positional signup is unchanged.
Raw and extracted tokens must match a2ai_ followed by 43 URL-safe characters.
Share links require HTTPS, except for configured localhost/127.0.0.1 URLs.
Review an invitation before signup without an API key using
Inkbox::preview_a2a_invitation. An existing claimed identity can accept it
with inkbox.a2a().accept_invitation(...). Organization credentials can manage
the issuer lifecycle with create_invitation, list_invitations,
get_invitation, and revoke_invitation on the same A2A resource.
Agent-to-agent discovery and history
Rust exposes identity-scoped A2A task, context, and message history. Receiver configuration and the standard protocol client are currently available in the Python and TypeScript SDKs.
use inkbox::a2a::{
A2AContextListOptions, A2ADirectoryListOptions, A2AHistoryDirection,
A2AMessageListOptions, A2ATaskListOptions,
};
let public_agents = inkbox.a2a().public_directory(&A2ADirectoryListOptions {
q: Some("research".to_string()),
limit: Some(25),
..Default::default()
})?;
let organization_agents = inkbox.a2a().organization_directory(
&A2ADirectoryListOptions::default(),
)?;
let tasks = identity.a2a_tasks(&A2ATaskListOptions {
direction: Some(A2AHistoryDirection::Both),
worker_handle: Some("researcher".to_string()),
q: Some("quarterly report".to_string()),
limit: Some(25),
..Default::default()
})?;
let messages = identity.a2a_messages(&A2AMessageListOptions {
direction: Some(A2AHistoryDirection::Outbound),
worker_handle: Some("researcher".to_string()),
q: Some("revenue".to_string()),
limit: Some(25),
..Default::default()
})?;
let contexts = identity.a2a_contexts(&A2AContextListOptions {
direction: Some(A2AHistoryDirection::Both),
limit: Some(25),
..Default::default()
})?;
let renamed = identity.a2a_update_context(
contexts.items[0].id,
"Quarterly Research Review",
)?;
println!("{:?}", tasks.next_cursor);
println!("{} {}", public_agents.items.len(), organization_agents.items.len());
println!("{:?}", contexts.next_cursor);
for message in messages.items {
println!("{} {} {:?}", message.task_id, message.task_state, message.parts);
}
New contexts immediately expose the persisted name New A2A Session. That
exact default may be replaced asynchronously with a short name based on the
first task message. Either participant can rename the shared context at any
time; a non-default name is not replaced by automatic naming. The
context's top-level caller and target remain the original opener and recipient,
while each nested task's participants identify that task's direction. Tasks in
both directions may run concurrently. Rust exposes the persisted ledger and
rename operation; it does not provide the outbound protocol client.
Task keyword filtering returns tasks containing a matching message. Message
filtering returns individual matching messages with task, context, requester,
and worker provenance. Search covers string and numeric content values from
text and data parts, excludes metadata, and is newest-first rather than
relevance-ranked. role is the message author (caller or agent),
independent of task direction. Cursors are opaque; pass next_cursor with the
same filters to fetch the next page.
The webhooks::types module includes A2AWebhookPayload and its typed event
discriminator for all four A2A task-lifecycle events.
Crypto
The vault uses Argon2id key derivation and AES-256-GCM envelope encryption,
with the exact parameters of the Python/TS SDKs, so secrets are
cross-SDK-interoperable. TOTP (SHA-1/256/512) and webhook HMAC-SHA256
signature verification (signing_keys::verify_webhook) are implemented in
pure Rust.
Features
tunnels-runtime— the local TLS-terminating HTTP/2 reverse-proxy data plane for inbound tunnels (pulls intokio,rustls(ring),h2). Bring a tunnel online withinkbox.tunnels().connect(name, forward_to), orconnect_with_status(name, forward_to, on_status)to observe"connecting"/"connected"/"reconnecting"/"closed". The control-plane tunnels surface (inkbox.tunnels(): list / get / update / sign_csr) is always available without this feature.
Status
The Rust SDK implements the REST resources documented above, vault crypto +
TOTP, webhook verification, and the tunnels control plane. Invitation-assisted
signup, unauthenticated preview, claimed-agent acceptance, and organization-side
invitation management are supported. The tunnels data-plane runtime
(tunnels-runtime feature) is implemented end-to-end:
- Edge HTTP: TLS h2 dial,
/_system/hello, parked intake pool, body materialization, URL-forward,/_system/response/{id}, PING keepalive, jittered reconnect. - WebSocket upgrade bridge and raw-TCP passthrough bridge over h2 extended CONNECT, with local upstream WS handshake and rustls TLS termination.
- Passthrough bootstrap: EC P-256 keypair + PKCS#10 CSR signing + cert-chain persistence.
115 unit/integration tests cover the wire codecs, crypto, CSR, URL-forward, and
runtime lifecycle. The passthrough data plane has been validated end-to-end
against a live edge (TLS-terminated HTTP plus a real-time call media
WebSocket). See src/tunnels/client/.
License
MIT
Dependencies
~7–26MB
~338K SLoC