# Auto-replies & office hours

Source: https://jointextkit.com/docs/auto-reply
Index: https://jointextkit.com/llms.txt

Each number can answer inbound texts automatically: an away message, an
out-of-office, or a first-touch acknowledgement.

## Configuration

Auto-replies are set in the console, not through the API. Open
[Console → Numbers](/console/numbers), expand **Auto-reply** under a number,
and save. Each number has its own settings, and only a number that is
currently on your account can have one.

| Setting | Notes |
| --- | --- |
| `enabled` | Turns the reply on. An enabled auto-reply needs a message; saving it with a blank one is refused. |
| `message` | The reply text, at most 320 characters. Anything longer is cut at 320. |
| `officeHours.tz` | An IANA time zone such as `America/New_York`. The console fills in your browser's zone. |
| `officeHours.days` | The days you are open, as numbers: 0 is Sunday, 6 is Saturday. The console starts with Monday to Friday. |
| `officeHours.start`, `officeHours.end` | Opening and closing time as 24-hour `HH:MM`. The console starts with `09:00` and `17:00`. |
| `officeHours.mode` | `always` or `after_hours`, described below. |

- `always`: reply to every eligible inbound. The hours are ignored. This is
  also what you get with no office hours at all.
- `after_hours`: the out-of-office pattern: reply only OUTSIDE the hours.
  Ticking "Only outside office hours" in the console saves this mode.

### How office hours are read

You are open when the current day, in your time zone, is one of `days` and
the time is at or after `start` and before `end`. A day that is not listed
is closed all day, so with Monday to Friday hours a Saturday text always gets
the reply. The window cannot cross midnight: if `end` is earlier than
`start` you are never open, and `after_hours` replies around the clock.

### Screening and the audit log

The text is checked against the content policy when you save an enabled
auto-reply, and a blocked text is refused with the reason. It is checked once,
at save time, not on every reply. Each save is recorded in the
[audit log](/docs/audit-logs) as `settings.updated` with target type
`auto_reply`: whether it is enabled and which hours mode it uses, never the
text.

## Guardrails (not configurable)

These are checked in this order on every inbound text:

- STOP / START / HELP keywords always win; the auto-reply never answers them.
  That covers every [keyword](/docs/opt-out#keywords) and a plain-English
  request to stop. Those messages get the keyword reply instead, or none.
- Verification codes are never answered. A text counts as one when it is only
  a 4 to 8 digit number, when it has a 4 to 8 digit number together with one
  of the words code, verification, verify, otp, passcode or pin, or when it
  contains a code in the form `G-123456`.
- Opted-out counterparties are never texted.
- One auto-reply per conversation per 4 hours, claimed atomically; two
  simultaneous inbound messages cannot double-send.

The 4-hour window belongs to the [conversation](/docs/inbox): one of your
numbers and one other party. The same person texting two of your numbers can
get a reply from each. The window is claimed before the reply is sent, so a
reply that fails at the carrier still uses it.

## The reply as a message

An auto-reply is an ordinary outbound message from the number that received
the text. It appears in the inbox thread and in `GET /v1/messages`.

- It is not billed. Its `price` is 0 with the line "Automatic reply (not
  billed)".
- It does not count against your daily or monthly message quota.
- It has no `sent_by`.

## Events

The inbound text emits `message.received` first. The reply then emits
`message.sent` with two extra fields:

```json
{
  "type": "message.sent",
  "data": {
    "message_id": "msg_a1B2c3D4e5F6g7H8",
    "to": "+14155550132",
    "from": "+15005550100",
    "body": "Thanks for texting! We are closed and will reply in the morning.",
    "auto_reply": true,
    "kind": "auto_reply"
  }
}
```

STOP and HELP confirmations carry `auto_reply: true` as well, with `kind`
set to `keyword_reply`. Check `kind` to tell the two apart. On a live
number the event also has a `carrier_reference`.

If a live reply cannot be handed to the carrier, the message is stored as
`failed` and you get `message.failed` with `code` `carrier_rejected`, a `reason`, the
`failure` object, and the same `auto_reply` and `kind` fields. Event
delivery is covered in [Webhooks](/docs/webhooks).

## Testing

Works identically in sandbox: simulate an inbound with
`POST /v1/test/inbound` and the reply appears in the thread with an
`auto_reply: true` marker on its event.

```bash
curl -X POST https://api.jointextkit.com/v1/test/inbound \
  -H "Authorization: Bearer ssms_sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "from": "+14155550132", "body": "Are you open?" }'
```

The call returns `201` with the inbound message, not the reply. Find the
reply in `GET /v1/events` or in `GET /v1/messages?number=%2B14155550132`.
A sandbox reply is recorded with status `sent` and `test: true`, and is
never transmitted. Send a second text within 4 hours and no reply comes back:
that is the cooldown. The endpoint takes test keys only; a live key gets
`403 test_mode_only`. Neither SDK has a call for it or for auto-reply
settings.
