> ## 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.

# Add a tapback

> Send one of the six iMessage tapbacks (`love`, `like`, `dislike`, `laugh`, `emphasize`, `question`) on a message you received. It goes out over iMessage, or RCS for an Android contact; it can't go out by SMS.

The tapback is accepted as `pending`. When it reaches the contact's phone, `message.reaction.added` fires with `direction: "outbound"`. If it can't be sent, `message.reaction.failed` fires with a `failed_reason`.

One tapback per message, as in iMessage: a different type replaces your previous one (`message.reaction.removed` fires for it if it was sent). Sending the type that's already on the message answers `200` with that reaction and sends nothing.

Counts toward the conversation-actions rate limit (300 a minute per account).

### Add a tapback

**Method:** `POST` | **Endpoint:** `/v1/messages/{message_id}/reactions`

**Description:** React to a message a contact sent you with one of the six iMessage tapbacks: ❤️ `love`, 👍 `like`, 👎 `dislike`, 😂 `laugh`, ‼️ `emphasize` or ❓ `question`. It goes out over iMessage, or over RCS to an Android contact. It can't go out by SMS. See [Tapbacks, read receipts and typing](https://docs.ibubbl.com/guides/reactions-read-receipts-typing.md).

**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 (tapbacks, mark read and typing together, across all your API keys). Over that, you get `429 rate_limited` with a `Retry-After` header; see [Rate limits](https://docs.ibubbl.com/concepts/limits.md#rate-limits).

#### Request Body

| Field | Type | Required | Description |
| - | - | - | - |
| `type` | string | Yes | `love`, `like`, `dislike`, `laugh`, `emphasize` or `question`. Lowercase. Custom emoji tapbacks can't be sent. |

The path parameter `message_id` is the ID of a message you **received**: the `message_id` of a [`message.inbound`](https://docs.ibubbl.com/webhooks/events) event, or the `id` of an inbound message from [List messages](https://docs.ibubbl.com/api-reference/messages/list.md). Fields not listed above are rejected with `422 validation_error`.

#### How it behaves

* **It's accepted first, sent second.** The answer is `201` with `status: "pending"`. When the tapback reaches the contact's phone, [`message.reaction.added`](https://docs.ibubbl.com/webhooks/events) fires with `direction: "outbound"`. If it can't be sent, [`message.reaction.failed`](https://docs.ibubbl.com/webhooks/events) fires with a `failed_reason`, usually within seconds and always within 10 minutes.
* **One tapback per message.** As in iMessage, a different type **replaces** your previous tapback on that message: the new one is `201`, and the old one ends as `removed`. If the old one had reached the phone, `message.reaction.removed` fires for it. Tapbacks sent from the dashboard count as yours too.
* **It's safe to call twice.** Sending the type that's already on the message answers `200` with that same reaction and sends nothing.
* **Groups work.** A tapback on a message in a group conversation goes to the group.
* **Blocked and opted-out contacts get none.** In a one-to-one conversation, a [blocked](https://docs.ibubbl.com/api-reference/contacts/block.md) contact gets `422 contact_blocked` and an [opted-out](https://docs.ibubbl.com/concepts/suppression.md) one `422 contact_opted_out`, as for a send.
* **Not over SMS.** If the message came in by SMS, or the conversation's latest messages are going out by SMS, the answer is `422 sms_not_supported`. If iBubbl can't tell yet, it's accepted, and a tapback the line then can't send fires `message.reaction.failed` with `failed_reason: "sms_target"`.

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/reactions \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "love"}'
```

```typescript TypeScript theme={null}
const messageId = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b";

const res = await fetch(`https://api.ibubbl.com/v1/messages/${messageId}/reactions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ type: "love" }),
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
// 201: a new tapback; 200: that tapback was already on the message.
console.log(res.status, data.id, data.status);
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

message_id = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"

res = requests.post(
    f"https://api.ibubbl.com/v1/messages/{message_id}/reactions",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"type": "love"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# 201: a new tapback; 200: that tapback was already on the message.
print(res.status_code, data["id"], data["status"])
```

```json 201 Created theme={null}
{
  "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
  "message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "type": "love",
  "emoji": "❤️",
  "direction": "outbound",
  "status": "pending",
  "failed_reason": null,
  "created_at": "2026-11-11T16:03:25.403057Z",
  "removed_at": null
}
```

```json 422 Unprocessable Entity theme={null}
{
  "detail": {
    "error": "sms_not_supported",
    "message": "This conversation is on SMS, where tapbacks can't be sent. They work over iMessage and RCS."
  }
}
```

#### Responses

**201 Created**

The tapback is accepted and on its way.

| Field | Description |
| - | - |
| `id` | The reaction's ID: the `reaction_id` in `message.reaction.*` events, and the ID to [remove it](https://docs.ibubbl.com/api-reference/messages/remove-reaction.md) with. |
| `message_id` | The message the tapback is on. |
| `conversation_id` | That message's conversation. |
| `type` / `emoji` | The tapback, as its name and as an emoji. |
| `direction` | Always `outbound`: a tapback you sent. |
| `status` | `pending` (accepted, not on the phone yet), `sent` (on the phone; `message.reaction.added` has fired), `failed` (never sent; see `failed_reason`) or `removed` (you removed it, or a newer tapback replaced it). |
| `failed_reason` | Why a `failed` tapback wasn't sent: `sms_target`, `target_not_found`, `bb_rejected`, `messages_error`, `expired`, `retract_rejected` or `target_unsent`. Otherwise `null`. See [`message.reaction.failed`](https://docs.ibubbl.com/webhooks/events). |
| `created_at` / `removed_at` | When you sent it, and when it was removed or replaced (UTC; `removed_at` is `null` while it's active). |

```json theme={null}
{
  "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
  "message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "type": "love",
  "emoji": "❤️",
  "direction": "outbound",
  "status": "pending",
  "failed_reason": null,
  "created_at": "2026-11-11T16:03:25.403057Z",
  "removed_at": null
}
```

**200 OK**

That type is already on the message. The body is the existing reaction (its `status` may already be `sent`); nothing new is sent.

**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": "Message not found"
  }
}
```

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

**422 Unprocessable Entity**

```json theme={null}
{
  "detail": {
    "error": "invalid_target",
    "message": "Only messages you received can get a tapback."
  }
}
```

Nothing was sent. The `error` is one of:

* `validation_error`: no `type`, a `type` that isn't one of the six, or an unknown field.
* `invalid_target`: the message is one you sent. Tapbacks go only on messages you received.
* `contact_blocked`: a one-to-one conversation with a contact you [blocked](https://docs.ibubbl.com/api-reference/contacts/block.md).
* `contact_opted_out`: a one-to-one conversation with a contact who opted out.
* `sms_not_supported`: the message came in by SMS, or the conversation is going out by SMS.
* `target_unsent` / `target_partly_unsent`: the contact unsent the message, or part of it.

**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 send the same request 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 sent nothing. Retry after a short backoff.


## OpenAPI

````yaml POST /v1/messages/{message_id}/reactions
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/messages/{message_id}/reactions:
    post:
      tags:
        - Messages
      summary: Add a tapback
      description: >-
        Send one of the six iMessage tapbacks (`love`, `like`, `dislike`,
        `laugh`, `emphasize`, `question`) on a message you received. It goes out
        over iMessage, or RCS for an Android contact; it can't go out by SMS.


        The tapback is accepted as `pending`. When it reaches the contact's
        phone, `message.reaction.added` fires with `direction: "outbound"`. If
        it can't be sent, `message.reaction.failed` fires with a
        `failed_reason`.


        One tapback per message, as in iMessage: a different type replaces your
        previous one (`message.reaction.removed` fires for it if it was sent).
        Sending the type that's already on the message answers `200` with that
        reaction and sends nothing.


        Counts toward the conversation-actions rate limit (300 a minute per
        account).
      operationId: v1_add_reaction_v1_messages__message_id__reactions_post
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
            title: Message Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReactionCreate'
      responses:
        '200':
          description: That tapback is already on the message; nothing sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Reaction'
        '201':
          description: Tapback accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Reaction'
        '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: Message (or reaction) not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '422':
          description: >-
            `validation_error` (bad body, unknown `type`), `invalid_target` (a
            message you sent), `contact_blocked`, `contact_opted_out`,
            `sms_not_supported` (the message came in by SMS, or the conversation
            is going out by SMS), `target_unsent` or `target_partly_unsent` (the
            contact unsent the message, or part of it).
          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 nothing was sent. Retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    ReactionCreate:
      properties:
        type:
          type: string
          enum:
            - love
            - like
            - dislike
            - laugh
            - emphasize
            - question
          title: Type
          description: >-
            One of the six iMessage tapbacks: `love`, `like`, `dislike`,
            `laugh`, `emphasize`, `question`. Custom emoji tapbacks can't be
            sent. Fields not listed here are rejected with `422
            validation_error`.
          examples:
            - love
      additionalProperties: false
      type: object
      required:
        - type
      title: ReactionCreate
      example:
        type: love
    Reaction:
      properties:
        id:
          type: string
          title: Id
          description: Reaction ID. The `reaction_id` in `message.reaction.*` events.
        message_id:
          type: string
          title: Message Id
          description: The message the tapback is on.
        conversation_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Conversation Id
          description: The conversation that message is in.
        type:
          type: string
          title: Type
          description: '`love`, `like`, `dislike`, `laugh`, `emphasize` or `question`.'
        emoji:
          type: string
          title: Emoji
          description: The tapback as an emoji, e.g. `❤️`.
        direction:
          type: string
          enum:
            - outbound
          const: outbound
          title: Direction
          description: 'Always `outbound`: a tapback you sent.'
        status:
          type: string
          enum:
            - pending
            - sent
            - failed
            - removed
          title: Status
          description: >-
            `pending`: accepted, not yet on the contact's phone. `sent`:
            delivered to Messages (`message.reaction.added` fires then).
            `failed`: never sent, see `failed_reason` (`message.reaction.failed`
            fires). `removed`: you removed it, or a newer tapback of yours on
            the same message replaced it.
        failed_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Failed Reason
          description: >-
            Why a `failed` tapback wasn't sent: `sms_target`,
            `target_not_found`, `bb_rejected`, `messages_error`, `expired`,
            `retract_rejected` or `target_unsent`. Null otherwise.
        created_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Created At
          description: When you sent the tapback (UTC).
        removed_at:
          anyOf:
            - type: string
            - type: 'null'
          title: Removed At
          description: When it was removed or replaced (UTC); null while active.
      type: object
      required:
        - id
        - message_id
        - conversation_id
        - type
        - emoji
        - direction
        - status
        - failed_reason
        - created_at
        - removed_at
      title: Reaction
      example:
        conversation_id: 3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d
        created_at: '2026-11-11T15:04:05.123Z'
        direction: outbound
        emoji: ❤️
        id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d
        message_id: 8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b
        status: pending
        type: love
    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.