Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# iMessage
description: Reach humans in their native messaging app — connect through the Inkbox iMessage router, reply in conversations, send tapbacks, and filter who can reach your agent

---


# iMessage

Agents can chat with humans over iMessage — blue bubbles, tapbacks, read receipts, typing indicators, and media — without owning a dedicated iMessage line. Humans connect to an agent through the shared **Inkbox iMessage router**, and from then on the conversation behaves like any other thread in their Messages app.

iMessage works differently from [SMS](/docs/capabilities/phone#text-messages-smsmms) in one important way: there is no per-identity iMessage number. Conversations are routed per **connection** — one human, one agent — over a shared pool of lines that Inkbox manages. Three things follow from that:

- **The human always texts first.** A recipient connects by texting the router; your agent cannot start an iMessage conversation cold.
- **Conversations are the stable key.** Agent-facing APIs identify threads by `conversation_id` and the human's E.164 number. The pool line carrying the conversation is managed by Inkbox and never appears in API responses.
- **Shared lines, private conversations.** A pool line may carry traffic for many Inkbox customers, but every conversation is private to its connection — your agent identity, in your organization. Inkbox enforces that boundary on every read and write: no one outside your organization can access your conversations, messages, or media, and an identity-scoped API key only ever sees its own identity's threads.

Conversations are 1:1 today — group iMessage threads are not supported.

## Enabling iMessage on an identity

iMessage reachability is **opt-in per identity** and defaults to off. Enable it at create time or later:

**Python**

```python
# At create time
identity = inkbox.create_identity("my-agent", imessage_enabled=True)

# Or toggle later
identity.update(imessage_enabled=True)
print(identity.imessage_enabled, identity.imessage_filter_mode)
```

**TypeScript**

```typescript
// At create time
const identity = await inkbox.createIdentity("my-agent", { imessageEnabled: true });

// Or toggle later
await identity.update({ imessageEnabled: true });
console.log(identity.imessageEnabled, identity.imessageFilterMode);
```

**CLI**

```bash
# At create time
inkbox identity create my-agent --imessage-enabled

# Or toggle later
inkbox identity update my-agent --imessage-enabled true
```

While an identity is disabled, the router will not connect new recipients to it, sends from it are rejected, and inbound traffic for it is not delivered.

## Connecting a human to your agent

Humans connect by texting a command to the router. Resolve the router number at runtime — it can change, so never hardcode it:

**Python**

```python
router = inkbox.imessages.get_triage_number()
print(router.number)           # the router's E.164 number
print(router.connect_command)  # e.g. 'connect @my-agent'
```

**TypeScript**

```typescript
const router = await inkbox.imessages.getTriageNumber();
console.log(router.number);          // the router's E.164 number
console.log(router.connectCommand);  // e.g. 'connect @my-agent'
```

**CLI**

```bash
inkbox imessage triage-number
# number          the router's E.164 number
# connectCommand  e.g. 'connect @my-agent'
```

Tell your human: *text `connect @my-agent` to the router number*. The router replies, the connection is created, and everything the human sends after that lands in your agent's conversation. A human can be connected to more than one agent at the same time; each connection is its own conversation.

Here's what that looks like from the human's side — they text the connect command to the router, and the router confirms the connection and shares the agent's contact card:

<ImageMdxViewer
  src="/static/docs/imessage/connect-example.png"
  alt="An iMessage thread with the Inkbox iMessage router: the human texts the connect command, and the router confirms the connection and sends the agent's contact card"
  containerProps={{ borderRadius: '2rem', maxW: '340px' }}
/>

## Reading and replying

Once a recipient has messaged your agent, list conversations, read messages, and reply. Replies can target the conversation by ID or the recipient by number:

**Python**

```python
# List conversations with latest-message previews
convos = identity.list_imessage_conversations(limit=20)
for c in convos:
    print(c.id, c.remote_number, c.unread_count, c.latest_text)

# Read a thread
msgs = identity.list_imessages(conversation_id=convos[0].id, limit=50)

# Reply into the conversation
identity.send_imessage(
    conversation_id=convos[0].id,
    text="On it — give me two minutes.",
)

# Or address the connected recipient directly
identity.send_imessage(to="+15555550123", text="Done!")
```

**TypeScript**

```typescript
// List conversations with latest-message previews
const convos = await identity.listIMessageConversations({ limit: 20 });
for (const c of convos) {
    console.log(c.id, c.remoteNumber, c.unreadCount, c.latestText);
}

// Read a thread
const msgs = await identity.listIMessages({ conversationId: convos[0].id, limit: 50 });

// Reply into the conversation
await identity.sendIMessage({
    conversationId: convos[0].id,
    text: "On it — give me two minutes.",
});

// Or address the connected recipient directly
await identity.sendIMessage({ to: "+15555550123", text: "Done!" });
```

**CLI**

```bash
# List conversations with latest-message previews
inkbox imessage conversations -i my-agent --limit 20

# Read a thread
inkbox imessage conversation <conversation-id> -i my-agent

# Reply into the conversation
inkbox imessage send -i my-agent \\
  --conversation-id <conversation-id> \\
  --text "On it — give me two minutes."
```

Sending to a number that hasn't connected returns `404` with instructions to relay: have them text the connect command to the router first. If a previously connected recipient disconnects, sends into the old conversation return `409` until they reconnect. Conversation reads carry an `assignment_status` field (`"active"` or `"released"`) so you can spot a disconnect before sending, and [`GET /assignments`](/docs/api/imessage/conversations#list-connections) lists who is currently connected.

Each identity can send up to **100 iMessages per rolling 24-hour window**. When the cap is reached, `429` responses carry `Retry-After` and `X-RateLimit-*` headers.

## Tapbacks

Agents can react to any message in the thread — the human's or their own — with the classic tapbacks: `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`.

**Python**

```python
identity.send_imessage_reaction(message_id=msgs[0].id, reaction="like")

# Live tapbacks come back on message reads, oldest first
for r in msgs[0].reactions or []:
    print(r.direction, r.reaction, r.custom_emoji)
```

**TypeScript**

```typescript
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);
}
```

**CLI**

```bash
inkbox imessage react <message-id> -i my-agent --reaction like
```

Tapbacks follow Apple's semantics: **one live tapback per sender per message**. Sending a second tapback to the same message replaces your first, and when the human swaps or removes theirs, message reads reflect it. Humans can also react with any emoji — those arrive as `reaction: "custom"` with the emoji in `custom_emoji`. Custom-emoji tapbacks are receive-only; sends accept the classic six.

## Read receipts, typing, and media

Round out the native feel: send a read receipt when your agent has read the thread, show a typing indicator while it works, and attach media.

**Python**

```python
# Read receipt — the human sees "Read" under their message
identity.mark_imessage_conversation_read(convos[0].id)

# Typing indicator while the agent prepares a reply
identity.send_imessage_typing(convos[0].id)

# Upload media (max 10 MiB), then send the returned URL
upload = identity.upload_imessage_media(
    content=open("chart.png", "rb").read(),
    filename="chart.png",
    content_type="image/png",
)
identity.send_imessage(
    conversation_id=convos[0].id,
    media_urls=[upload.media_url],
)
```

**TypeScript**

```typescript
// Read receipt — the human sees "Read" under their message
await identity.markIMessageConversationRead(convos[0].id);

// Typing indicator while the agent prepares a reply
await identity.sendIMessageTyping(convos[0].id);

// Upload media (max 10 MiB), then send the returned URL
const upload = await identity.uploadIMessageMedia({
    content: await readFile("chart.png"),
    filename: "chart.png",
    contentType: "image/png",
});
await identity.sendIMessage({
    conversationId: convos[0].id,
    mediaUrls: [upload.mediaUrl],
});
```

**CLI**

```bash
# Read receipt
inkbox imessage mark-conversation-read <conversation-id> -i my-agent

# Typing indicator
inkbox imessage typing <conversation-id> -i my-agent

# Upload media, then send the returned URL
inkbox imessage upload-media ./chart.png -i my-agent --content-type image/png
inkbox imessage send -i my-agent \\
  --conversation-id <conversation-id> \\
  --media-url <media-url>
```

Expressive **send styles** are supported too — pass `send_style` on a send with values like `slam`, `confetti`, `lasers`, or `invisible` (invisible ink). See the [Messages reference](/docs/api/imessage/messages#send-styles) for the full list.

## Filtering who can reach your agent

iMessage contact rules work like [phone](/docs/capabilities/phone#filtering-inbound-calls-and-texts) and mail rules, with one difference: because there is no per-identity iMessage number, rules are scoped to the **agent identity** itself.

Each identity has an iMessage `filter_mode`:

- **`blacklist`** (default) — everyone can reach the agent except numbers with an active `block` rule.
- **`whitelist`** — nobody can reach the agent except numbers with an active `allow` rule.

**Python**

```python
# Block one number
rule = inkbox.imessage_contact_rules.create(
    "my-agent", action="block", match_target="+15555550999",
)

# Review the identity's rules
for r in inkbox.imessage_contact_rules.list("my-agent"):
    print(r.action, r.match_target, r.status)

# Flip to whitelist mode (admin API key required)
identity.update(imessage_filter_mode="whitelist")
```

**TypeScript**

```typescript
// Block one number
const rule = await inkbox.imessageContactRules.create("my-agent", {
    action: "block",
    matchTarget: "+15555550999",
});

// Review the identity's rules
for (const r of await inkbox.imessageContactRules.list("my-agent")) {
    console.log(r.action, r.matchTarget, r.status);
}

// Flip to whitelist mode (admin API key required)
await identity.update({ imessageFilterMode: "whitelist" });
```

**CLI**

```bash
# Block one number
inkbox imessage contact-rule create -i my-agent \\
  --action block --match-target +15555550999

# Review the identity's rules
inkbox imessage contact-rule list -i my-agent

# Flip to whitelist mode (admin API key required)
inkbox identity update my-agent --imessage-filter-mode whitelist
```

Blocked inbound messages are stored for review but never reach the agent: identity-scoped API keys never see them, no webhooks fire for them, and outbound sends to a blocked number return `403` before anything leaves Inkbox. Admin API keys and the [Inkbox Console](https://inkbox.ai/console) can audit blocked rows with `is_blocked=true` filters. Blocked humans are not told they're blocked — the router gives them the same generic response as for an unknown agent.

## Reacting to events in real time

Inbound messages and tapbacks are delivered through [webhook subscriptions](/docs/api/webhooks/subscriptions) owned by the **agent identity** (not a mailbox or phone number, since the pool lines aren't yours). The same subscription can also carry the outbound delivery-lifecycle events — `imessage.sent`, `imessage.delivered`, and `imessage.delivery_failed` — so your agent knows when a reply actually landed:

**Python**

```python
inkbox.webhooks.subscriptions.create(
    agent_identity_id=identity.id,
    url="https://yourapp.example.com/webhooks/inkbox",
    event_types=["imessage.received", "imessage.reaction_received"],
)
```

**TypeScript**

```typescript
await inkbox.webhooks.subscriptions.create({
    agentIdentityId: identity.id,
    url: "https://yourapp.example.com/webhooks/inkbox",
    eventTypes: ["imessage.received", "imessage.reaction_received"],
});
```

**CLI**

```bash
inkbox webhook subscription create \\
  --agent-identity-id <identity-id> \\
  --url https://yourapp.example.com/webhooks/inkbox \\
  --event-type imessage.received \\
  --event-type imessage.reaction_received
```

See [iMessage webhooks](/docs/api/imessage/webhooks) for payload shapes and [Signing keys](/docs/signing-keys) for signature verification.

## How messages are delivered

Inkbox always tries iMessage first. If a recipient isn't reachable over iMessage, delivery can fall back to SMS — the message's `service` field reports the transport actually used (`"imessage"`, `"sms"`, or `"rcs"`), and `was_downgraded` is set when a fallback happened.

## Next steps

- [iMessage API reference](/docs/api/imessage)
- [Webhook subscriptions](/docs/api/webhooks/subscriptions)
- [Identities](/docs/capabilities/identities)
