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

# Working Hours & Scheduling

> How iBubbl respects working hours for outbound sends.

iBubbl never sends an iMessage outside of the recipient's working hours. This protects your sender reputation and respects contact preferences — late-night blasts are the fastest way to get a phone number flagged.

The one exception is an [after-hours auto-reply](https://docs.ibubbl.com/concepts/after-hours-reply.md): a reply to someone who texted you in the last 20 minutes can go out straight away.

## How it works

Every outbound message — sent from the dashboard, `POST /v1/messages`, a workflow, a scheduled send or an integration — waits for the recipient's working hours before it leaves your device:

* `POST /v1/messages` accepts the send straight away (`202 Accepted`) with `status: "pending"`, whatever the time.
* If the **recipient's local time** is inside your working window, the message goes out within seconds.
* If it is outside, the message stays `pending` and goes out automatically when the window next opens. There is no separate status or webhook for this wait; a [`message.sent`](https://docs.ibubbl.com/webhooks/events) webhook fires when it goes out.

You don't need to do anything to opt in. The behavior is on by default for every account.

## Configuring working hours

Working hours are set in the dashboard at **Settings → Sending Hours**. Defaults for new accounts:

| Day | Sends allowed |
| - | - |
| Monday – Saturday | 09:00 – 20:00 |
| Sunday | 12:00 – 20:00 |

The end time is exclusive: with the default window, sends pause at 8:00 PM. The window is applied in each **contact's** timezone (see below), so there is no timezone setting on the window itself.

These are only your **sending** hours. When people can book meetings with you is a separate setting, **Settings → Booking Hours**, in your own time zone.

You can configure different windows per day of the week, or close specific days entirely. The dashboard is the only way to manage these settings — there's no API surface for them today.

## Per-contact timezone

The local time check uses the **contact's timezone**, not your account's. iBubbl determines a contact's timezone in this order:

1. **Explicit override** — if you set the `timezone` field on the contact (via [`POST /v1/contacts`](https://docs.ibubbl.com/api-reference/contacts/upsert) or the dashboard).
2. **Area-code inference** — for North American numbers, iBubbl infers a default timezone from the area code (e.g., `+1415...` → `America/Los_Angeles`).
3. **Account timezone** — the timezone in your dashboard's **Settings → Notifications**, which starts as your browser's timezone.
4. **`America/New_York`** — if none of the above is set.

To set a contact's timezone yourself, pass it on contact upsert **before you send to them**:

```bash cURL theme={null}
curl -X POST https://api.ibubbl.com/v1/contacts \
  -H "Authorization: Bearer $IBUBBL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+14155551234",
    "timezone": "America/New_York"
  }'
```

```typescript TypeScript theme={null}
const res = await fetch("https://api.ibubbl.com/v1/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phone: "+14155551234",
    timezone: "America/New_York",
  }),
  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.timezone);
```

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

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

res = requests.post(
    "https://api.ibubbl.com/v1/contacts",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "phone": "+14155551234",
        "timezone": "America/New_York",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["id"], data["timezone"])
```

Once a contact has a timezone, an upsert never changes it. That includes the inferred one: the area-code guess is saved on the contact when their first message is processed, so a `timezone` sent on a later upsert is ignored. Upsert first, then send.

The `timezone` must be one of the supported zones (US, Canada, and a few international ones); anything else returns `422 invalid_timezone`.

## Detecting a held send

A message waiting for working hours is `pending`, like one that is about to go out. [`GET /v1/messages/{id}`](https://docs.ibubbl.com/api-reference/messages/retrieve) tells them apart with two fields:

```json theme={null}
{
  "id": "4a5b6c7d-8e9f-0a1b-2c3d-4e5f6a7b8c9d",
  "status": "pending",
  "to": "+14155551234",
  "held_reason": "working_hours",
  "held_until": "2026-10-08T16:00:00+00:00"
}
```

* `held_reason` — why it is waiting right now: `working_hours` (outside the contact's working hours) or `hourly_limit` (your line has messaged 30 new contacts in the last 60 minutes; the message stays `pending` and goes out by itself when a slot frees — see [Limits and queues](https://docs.ibubbl.com/concepts/limits.md)).
* `held_until` — when it is **expected** to go out. For `working_hours`, the contact's next opening: it is recomputed within about 10 seconds when your hours change, and it is `null` if no sending day is enabled. For `hourly_limit`, an estimate of when a slot frees, or `null` if there is none.

Both are `null` when the message is not being held. A held message is first marked within about 10 seconds of being accepted, so a `GET` immediately after `POST` may not show the hold yet. A [`message.sent`](https://docs.ibubbl.com/webhooks/events) webhook fires when it goes out.

## Capacity holds (`status: "queued"`)

Separately from working hours, a one-to-one send can be accepted but parked. The response is still `202 Accepted`, with `status: "queued"`. Today the only reason is `line_offline`: your line's Mac is offline. The reason is in the `failed_reason` field of the [`message.queued`](https://docs.ibubbl.com/webhooks/events) webhook, not in the API response. (`hourly_cap_hit` is a legacy reason that no current line uses; the hourly new-contact limit holds messages as `pending` instead.)

iBubbl re-checks queued messages every 30 seconds and sends them once the Mac is back. A message still queued 60–65 minutes after it was accepted is dropped and moves to `status: "expired"`, and a [`message.failed`](https://docs.ibubbl.com/webhooks/events#message-failed) webhook fires with `status: "expired"` and `failed_reason: "queue_expired"`. Send it again (a new `idempotency_key`, or the same one: an expired message's key is released) if it still matters.

A `pending` message works differently: if its line has been offline for over an hour, it is not sent late. It moves to `status: "failed"` with `failed_reason: "agent_offline"`, and a [`message.failed`](https://docs.ibubbl.com/webhooks/events) webhook fires.

The daily limits do **not** queue: a send over 50 new contacts or 1,000 messages a day fails immediately with a `422`. See [Limits and queues](https://docs.ibubbl.com/concepts/limits.md) for every limit and [Errors](https://docs.ibubbl.com/errors.md) for the codes.


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