> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ibubbl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Start typing

> Show the contact the typing bubble (the three dots) in this conversation. It lasts 10 seconds: call again every 5 seconds to keep it going. Sending a message clears it on the contact's phone; so does `DELETE`, or simply not calling again.

Typing shows only in a 1:1 iMessage conversation the contact has already messaged. Anywhere else the call answers `200` with `status: "ignored"` and an `ignored_reason`, and does nothing.

Typing is a signal to the contact, so it's refused for a blocked contact or one who opted out.

Counts toward the conversation-actions rate limit (300 a minute per account); calls closer than 3 seconds apart are answered without doing anything new.

### Start typing

**Method:** `POST` | **Endpoint:** `/v1/conversations/{conversation_id}/typing`

**Description:** Show the contact the typing bubble (the three animated dots) in an iMessage conversation, for example while your AI writes a reply. It lasts 10 seconds; call again every 5 seconds to keep it going. Sending a message clears it. See [Typing while your AI thinks](https://docs.ibubbl.com/guides/reactions-read-receipts-typing.md#typing-while-your-ai-thinks).

**Authentication:** API key in the `Authorization` header: `Authorization: Bearer ib_pk_<64 hex>`. Create keys in the dashboard under **Settings → API access**.

**Rate limit:** 300 conversation actions per 60 seconds per account, shared with tapbacks and mark read. Over that, `429 rate_limited` with a `Retry-After` header; see [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits). A refresh every 5 seconds is 12 calls a minute per conversation.

#### Request Body

None. The path parameter `conversation_id` is the conversation's ID (a UUID): `conversation_id` on every [message](https://docs.ibubbl.com/api-reference/messages/retrieve.md) and on [`message.inbound`](https://docs.ibubbl.com/webhooks/events).

#### How it behaves

* **It lasts 10 seconds.** `expires_at` says when it stops unless you call again. The dots appear on the phone within about a second.
* **Keep it going by calling every 5 seconds.** Calls less than 3 seconds apart are answered `started` without doing anything new, and keep the earlier `expires_at`.
* **Sending ends it.** When you [send a message](https://docs.ibubbl.com/api-reference/messages/send.md), it replaces the dots straight away. If you stop calling instead, or call [Stop typing](https://docs.ibubbl.com/api-reference/conversations/stop-typing.md), iBubbl stops typing, but the dots can stay on the contact's iPhone for up to about a minute.
* **iMessage, one-to-one only.** Where typing can't be shown, the call answers `200` with `status: "ignored"` and does nothing:

| `ignored_reason` | Meaning |
| - | - |
| `group` | A group conversation. |
| `not_imessage` | The contact is known not to use iMessage (an SMS or RCS contact). |
| `no_chat` | The contact hasn't messaged this line yet, so there's no chat to type in. |

* **Blocked and opted-out contacts are refused.** Typing is a signal to the contact, so a [blocked](https://docs.ibubbl.com/api-reference/contacts/block.md) contact gets `422 contact_blocked` and an opted-out one `422 contact_opted_out`.
* **No webhook** fires for typing.

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/typing \
  -H "Authorization: Bearer $IBUBBL_API_KEY"
```

```typescript TypeScript theme={null}
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";

const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/typing`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
// "started", or "ignored" with ignored_reason where typing can't be shown.
console.log(data.status, data.ignored_reason, data.expires_at);
```

```python Python theme={null}
import os

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

conversation_id = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d"

res = requests.post(
    f"https://api.ibubbl.com/v1/conversations/{conversation_id}/typing",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# "started", or "ignored" with ignored_reason where typing can't be shown.
print(data["status"], data["ignored_reason"], data["expires_at"])
```

```json 200 OK theme={null}
{
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "typing": true,
  "status": "started",
  "ignored_reason": null,
  "expires_at": "2026-11-11T16:04:49.293232Z"
}
```

```json 422 Unprocessable Entity theme={null}
{
  "detail": {
    "error": "contact_opted_out",
    "message": "This contact has opted out."
  }
}
```

#### Responses

**200 OK**

| Field | Description |
| - | - |
| `conversation_id` | The conversation. |
| `typing` | `true` when the dots are on after this call. |
| `status` | `started` (or kept going), or `ignored`. |
| `ignored_reason` | `group`, `not_imessage` or `no_chat` when ignored; otherwise `null`. |
| `expires_at` | When the dots stop unless you call again (UTC, 10 seconds after the call that started or extended them). `null` unless `started`. |

```json theme={null}
{
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "typing": false,
  "status": "ignored",
  "ignored_reason": "group",
  "expires_at": null
}
```

**401 Unauthorized**

```json theme={null}
{
  "detail": {
    "error": "unauthorized",
    "message": "Invalid or revoked API key"
  }
}
```

Other `message` values: `Authorization Bearer token required`, `API key required (use Authorization: Bearer ib_pk_…)`, `Invalid API key format`, `Customer account not found`.

**403 Forbidden**

```json theme={null}
{
  "detail": {
    "error": "forbidden",
    "message": "Account is suspended"
  }
}
```

Your iBubbl account is suspended or cancelled (`message` is then `Account is cancelled`). Every API-key endpoint returns this.

**404 Not Found**

```json theme={null}
{
  "detail": {
    "error": "not_found",
    "message": "Conversation not found"
  }
}
```

Also returned for a malformed ID and a conversation that belongs to another account.

**422 Unprocessable Entity**

```json theme={null}
{
  "detail": {
    "error": "contact_blocked",
    "message": "This contact is blocked."
  }
}
```

`contact_blocked` or `contact_opted_out`, in a one-to-one conversation. Nothing was shown. Group conversations are answered `ignored` instead.

**429 Too Many Requests**

```json theme={null}
{
  "detail": {
    "error": "rate_limited",
    "message": "Too many conversation actions; wait 12s (limit 300 a minute per account)."
  }
}
```

Wait the `Retry-After` header's seconds, then call again.

**503 Service Unavailable**

```json theme={null}
{
  "detail": {
    "error": "opt_out_check_unavailable",
    "message": "Couldn't check the contact's opt-out. Try again."
  }
}
```

iBubbl couldn't confirm the contact hasn't opted out, so it showed nothing. Retry after a short backoff.


## OpenAPI

````yaml POST /v1/conversations/{conversation_id}/typing
openapi: 3.1.0
info:
  title: iBubbl API
  version: v1
  description: >-
    The iBubbl REST API. Send iMessages, manage contacts, and subscribe to
    webhook events from your backend.


    Authenticate every request with a Bearer API key (`ib_pk_<hex>`) — generate
    one from your dashboard's API Access settings. See the
    [Authentication](/authentication) guide.
servers:
  - url: https://api.ibubbl.com
security:
  - BearerAuth: []
tags:
  - name: Messages
  - name: Contacts
  - name: Conversations
  - name: Webhooks
  - name: Inbound Events
paths:
  /v1/conversations/{conversation_id}/typing:
    post:
      tags:
        - Conversations
      summary: Start typing
      description: >-
        Show the contact the typing bubble (the three dots) in this
        conversation. It lasts 10 seconds: call again every 5 seconds to keep it
        going. Sending a message clears it on the contact's phone; so does
        `DELETE`, or simply not calling again.


        Typing shows only in a 1:1 iMessage conversation the contact has already
        messaged. Anywhere else the call answers `200` with `status: "ignored"`
        and an `ignored_reason`, and does nothing.


        Typing is a signal to the contact, so it's refused for a blocked contact
        or one who opted out.


        Counts toward the conversation-actions rate limit (300 a minute per
        account); calls closer than 3 seconds apart are answered without doing
        anything new.
      operationId: v1_start_typing_v1_conversations__conversation_id__typing_post
      parameters:
        - name: conversation_id
          in: path
          required: true
          schema:
            type: string
            title: Conversation Id
      responses:
        '200':
          description: Typing started (or ignored).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TypingStatus'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`forbidden`: your iBubbl account is suspended or cancelled.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: Conversation not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: '`contact_blocked` or `contact_opted_out`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: >-
            `rate_limited`: more than 300 conversation actions in 60 seconds
            from your account (all keys, and tapbacks, read and typing requests,
            count together; list requests have their own budget). Wait
            `Retry-After` seconds, then send the same request again.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '503':
          description: >-
            `opt_out_check_unavailable`: the contact's opt-out couldn't be
            checked, so typing wasn't shown. Retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    TypingStatus:
      properties:
        conversation_id:
          type: string
          title: Conversation Id
          description: The conversation.
        typing:
          type: boolean
          title: Typing
          description: Whether the typing bubble is on after this call.
        status:
          type: string
          enum:
            - started
            - stopped
            - ignored
          title: Status
          description: >-
            `started` (or kept going), `stopped`, or `ignored`: typing can't be
            shown here and nothing was done; see `ignored_reason`.
        ignored_reason:
          anyOf:
            - type: string
              enum:
                - group
                - not_imessage
                - no_chat
            - type: 'null'
          title: Ignored Reason
          description: >-
            `group` (not available in group conversations), `not_imessage` (the
            contact is on SMS or RCS), `no_chat` (the contact hasn't messaged
            this line yet). Null otherwise.
        expires_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Expires At
          description: >-
            When the bubble stops unless you call again (UTC), 10 seconds after
            the call. Null unless `started`.
      type: object
      required:
        - conversation_id
        - typing
        - status
        - ignored_reason
        - expires_at
      title: TypingStatus
      example:
        conversation_id: 3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d
        expires_at: '2026-11-11T15:04:15.123Z'
        status: started
        typing: true
    ApiError:
      properties:
        detail:
          allOf:
            - $ref: '#/components/schemas/ApiErrorDetail'
          description: >-
            Error envelope. Every `/v1` error response wraps a stable `error`
            code and a human-readable `message` in `detail`.
      type: object
      required:
        - detail
      title: ApiError
      example:
        detail:
          error: validation_error
          message: >-
            Exactly one of contact_id, to, chat_guid, or participant_phones is
            required
    ApiErrorDetail:
      properties:
        error:
          type: string
          title: Error
          description: >-
            Stable machine-readable error code. Use this for branching logic in
            your integration; the human-readable `message` is subject to change.
            See the Error Reference for the catalog.
          examples:
            - validation_error
        message:
          type: string
          title: Message
          description: Human-readable description of what went wrong.
          examples:
            - >-
              Exactly one of contact_id, to, chat_guid, or participant_phones is
              required
      type: object
      required:
        - error
        - message
      title: ApiErrorDetail
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ib_pk_<64 hex>
      description: >-
        Bearer API key. Format: `Authorization: Bearer ib_pk_<64 hex>` (70
        characters). Generate keys from Dashboard → Settings → API Access.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.