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

# Quickstart

> Create a key, send an iMessage to your own phone, check its status and receive the reply.

In about ten minutes you'll send a real iMessage from your iBubbl line to your own phone, check that it was delivered, and get your reply pushed to your server.

**Before you start**

* An iBubbl account with an **active line**. iBubbl sets up and runs the line for you; you don't connect a Mac or an Apple ID. [Sign up](https://ibubbl-dashboard.vercel.app/signup) if you haven't.
* **Your own mobile number**, to send the first messages to.

> [!WARNING]
>
> **Every request is live.** There is no sandbox or test mode, and no test line: each send delivers a real message from your line. Send only to your own phone while you test.

**1. Create an API key**

In the [dashboard](https://ibubbl-dashboard.vercel.app), open **Settings → API Access** and click **Create key**. Copy the key (`ib_pk_` followed by 64 hex characters); this is the only time the full key is shown.

Keep it in an environment variable rather than in your code. The examples on this page read it from `IBUBBL_API_KEY`:

```bash theme={null}
export IBUBBL_API_KEY="ib_pk_..."
```

See [Authentication](https://docs.ibubbl.com/authentication.md) for rotation and security practices.

**2. Send a message to your own phone**

Put your own mobile number in `to`.

```bash cURL theme={null}
# One key per message: generate it once, and reuse it if you retry.
IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST https://api.ibubbl.com/v1/messages \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"to\": \"+14155551234\",
    \"body\": \"Hello from the iBubbl API\",
    \"idempotency_key\": \"$IDEMPOTENCY_KEY\"
  }"
```

```typescript TypeScript theme={null}
import { randomUUID } from "node:crypto";

// One key per message: generate it once, and reuse it if you retry.
const idempotencyKey = randomUUID();

const res = await fetch("https://api.ibubbl.com/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "+14155551234",
    body: "Hello from the iBubbl API",
    idempotency_key: idempotencyKey,
  }),
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.id, data.status);
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

# One key per message: generate it once, and reuse it if you retry.
idempotency_key = str(uuid.uuid4())

res = requests.post(
    "https://api.ibubbl.com/v1/messages",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "to": "+14155551234",
        "body": "Hello from the iBubbl API",
        "idempotency_key": idempotency_key,
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["id"], data["status"])
```

The response is `202 Accepted`:

```json theme={null}
{
  "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "status": "pending",
  "to": "+14155551234",
  "body": "Hello from the iBubbl API",
  "contact_id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
  "conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "conversation_kind": "one_to_one",
  "chat_guid": null,
  "channel": null,
  "sent_at": "2026-11-10T15:04:10.112Z",
  "held_until": null,
  "held_reason": null
}
```

Save the `id`; you'll use it in the next step.

> [!NOTE]
>
> **`202` means accepted, not delivered.** iBubbl has taken the message and will send it from your line; it hasn't reached the phone yet. Track it with the next step or with [webhooks](https://docs.ibubbl.com/webhooks/events.md).

> [!NOTE]
>
> **`idempotency_key` is required.** Use a new one for each message, and send the **same** key and the same payload again if a request times out or fails with a `5xx`: iBubbl returns the original message instead of sending twice. See [Idempotency](https://docs.ibubbl.com/idempotency.md).

> [!NOTE]
>
> **`to` must be in E.164 format**: `+`, the country code, then the number, with no spaces or punctuation (`+14155551234`). Anything else returns `422 validation_error`.

**3. Check the status**

Fetch the message by its `id`:

```bash cURL theme={null}
curl https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b \
  -H "Authorization: Bearer $IBUBBL_API_KEY"
```

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

const res = await fetch(`https://api.ibubbl.com/v1/messages/${messageId}`, {
  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}`);
console.log(data.status, data.channel);
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

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

res = requests.get(
    f"https://api.ibubbl.com/v1/messages/{message_id}",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["status"], data["channel"])
```

Once your phone has it, `status` is `delivered` (or `read`, if you have read receipts on), and `channel` says how it went out: `imessage`, or `sms` when your number couldn't be reached on iMessage.

```json theme={null}
{
  "id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "status": "delivered",
  "channel": "imessage",
  "sent_at": "2026-11-10T15:04:12.381Z",
  "held_until": null,
  "held_reason": null,
  "...": "..."
}
```

> [!NOTE]
>
> **Outside sending hours, the message waits.** iBubbl only sends during each contact's [sending hours](https://docs.ibubbl.com/concepts/working-hours.md) (by default Monday to Saturday 9 AM–8 PM, Sunday 12–8 PM, in the contact's timezone). If you're outside them, the message stays `pending` with `held_reason: "working_hours"`, and `held_until` says when it will go out. That's expected, not an error.

In production, don't poll: subscribe to webhooks, as in the next step.

**4. Receive a reply**

Register an HTTPS endpoint you control that logs the request body, subscribed to `message.inbound`. To test from your laptop, expose a local server with a tunnel such as `cloudflared tunnel --url http://localhost:8000` or `ngrok http 8000`, and use the HTTPS URL it prints.

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/webhooks \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-endpoint.example.com/ibubbl-webhooks",
    "event_types": ["message.inbound"]
  }'
```

```typescript TypeScript theme={null}
const res = await fetch("https://api.ibubbl.com/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-endpoint.example.com/ibubbl-webhooks",
    event_types: ["message.inbound"],
  }),
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.id, data.secret); // store the secret now
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

res = requests.post(
    "https://api.ibubbl.com/v1/webhooks",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "url": "https://your-endpoint.example.com/ibubbl-webhooks",
        "event_types": ["message.inbound"],
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["id"], data["secret"])  # store the secret now
```

The `201 Created` response includes the signing `secret` (`whsec_…`). It is shown only once: store it, because you'll need it to verify signatures.

Now reply to the message on your phone. Your endpoint receives a `POST` like this:

```json theme={null}
{
  "id": "evt_8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
  "type": "message.inbound",
  "created_at": "2026-11-10T15:06:12.456Z",
  "data": {
    "message_id": "5b6c7d8e-9f0a-1b2c-3d4e-5f6a7b8c9d0e",
    "from_number": "+14155551234",
    "to_number": "+14155550100",
    "body": "Got it!",
    "received_at": "2026-11-10T15:06:12.456Z",
    "attachments": [],
    "reply_to_message_id": null,
    "conversation_kind": "one_to_one",
    "sender_phone": "+14155551234",
    "participants": [
      {"phone": "+14155551234", "first_name": null, "last_name": null}
    ]
  }
}
```

Return any `2xx` within 10 seconds, or iBubbl retries the delivery.

> [!WARNING]
>
> **Before you go live:** verify the `X-iBubbl-Signature` header on every request ([Signature verification](https://docs.ibubbl.com/guides/webhook-signatures.md)), de-duplicate on the event `id`, and only ever send automatically in response to `message.inbound`. Sending in response to `message.sent` or another status event of your own messages makes a loop.

Subscriptions can't be edited. When you're done testing, revoke this one with [`DELETE /v1/webhooks/{subscription_id}`](https://docs.ibubbl.com/api-reference/webhooks/revoke) and create your production subscription.

## Next steps

> [card] [Webhook events](https://docs.ibubbl.com/webhooks/events.md)
>
> Every event type and its payload.

> [card] [Send messages](https://docs.ibubbl.com/guides/send-messages.md)
>
> All four ways to address a message, including groups.

> [card] [Receive messages](https://docs.ibubbl.com/guides/receive-messages.md)
>
> Replies, attachments, opt-outs, and an auto-reply that can't loop.

> [card] [Errors](https://docs.ibubbl.com/errors.md)
>
> Every error code, and which ones to retry.

> [card] [Build with AI](https://docs.ibubbl.com/build-with-ai.md)
>
> Hand these docs and a ready-made system prompt to your coding agent.


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