> For the complete documentation index, see [llms.txt](https://developers.mobile-text-alerts.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.mobile-text-alerts.com/appointment-reminders-with-confirmation-replies.md).

# Appointment Reminders with confirmation replies

Learn how to build an appointment reminder workflow with the Mobile Text Alerts API

This guide shows how to build a complete appointment reminder workflow with the Mobile Text Alerts API. This includes scheduling a personalized reminder ahead of time, letting the recipient respond by replying, and keeping your appointment records in sync with the user response. This use case uses multiple API endpoints, this guide shows how to use them together in one worked example.

## Appointment reminder workflow overview

The following is the typical appointment reminder workflow:

Schedule a personalized reminder ahead of an appointment <i class="fa-arrow-right">:arrow-right:</i> The recipient can reply to confirm, change or cancel the appointment <i class="fa-arrow-right">:arrow-right:</i> Update your system for any appointment changes

**Implementation steps:**

1. An appointment is created in your system.
2. You schedule a reminder message to send ahead of the appointment. This is personalized with the recipient's name, time, and location.
3. Mobile Text Alerts sends the reminder message at the scheduled time (in the recipient's local time).
4. The recipient replies — to confirm, cancel, or ask to reschedule.
5. Your webhook endpoint receives this reply and then updates the appointment record in your system.
6. If the appointment time changes or is cancelled before it is sent, then the scheduled reminder is updated or cancelled.

```mermaid
sequenceDiagram
    participant App as Your Application
    participant MTA as Mobile Text Alerts
    participant User as Recipient

    App->>MTA: POST /send (scheduledDate, properties, externalId, tags)
    MTA-->>App: messageId / scheduledMessageId
    Note over MTA: Waits until scheduledDate
    MTA->>User: Reminder SMS delivered
    User->>MTA: Replies ("C", "R", "cancel"...)
    MTA->>App: message-reply webhook (fromNumber, message, externalId, tags)
    App->>App: Update appointment status
    opt Appointment time changes
        App->>MTA: PATCH /scheduled/{scheduledMessageId}
    end
    opt Appointment cancelled
        App->>MTA: Cancel scheduled reminder
    end
```

This user guide walks through these steps, including the request fields and endpoints. It covers `scheduledDate`, `properties`, the [`message-reply` webhook](/tutorials/webhooks.md#message-reply), and the [Scheduled Messages](/api-reference/scheduled-messages.md) endpoints.

### Prerequisites

Before building this workflow you need to have:

* Completed the steps in [Getting Started](/getting-started/get-an-api-key.md) and have a valid API key.
* A verified sending number. Trial accounts and unverified numbers can only send templated content, which will limit how much you can personalize a reminder.
* A webhook endpoint that is already registered and receiving [`message-reply`](/tutorials/webhooks.md#message-reply) events. See [Setting up your first webhook](/tutorials/webhooks/setting-up-your-first-webhook.md) if this has not been configured yet.

## Store appointment data

Mobile Text Alerts holds the message and schedule, but the appointment itself is your data. At minimum, keep a record per appointment with the following fields:

<table><thead><tr><th width="220">Field</th><th>Purpose</th></tr></thead><tbody><tr><td><code>appointmentId</code></td><td>Your own internal identifier for the appointment.</td></tr><tr><td><code>subscriberNumber</code></td><td>The recipient's phone number, in E.164 format.</td></tr><tr><td><code>appointmentDateTime</code></td><td>The appointment date and time.</td></tr><tr><td><code>recipientTimezone</code></td><td>The recipient's local timezone (IANA name, e.g. <code>America/Chicago</code>), used to compute the correct offset for <code>scheduledDate</code>.</td></tr><tr><td><code>externalId</code></td><td>The identifier you sent on the reminder's <code>/send</code> request — typically derived from <code>appointmentId</code>. Used to match an inbound reply back to this appointment.</td></tr><tr><td><code>messageId</code><strong>*</strong></td><td>The <code>messageId</code> returned by <code>/send</code> when the reminder was created.</td></tr><tr><td><code>scheduledMessageId</code><strong>*</strong></td><td>The scheduled message's own ID, returned when the reminder is created as a future send. Needed to later update or cancel the reminder before it fires.</td></tr><tr><td><code>status</code></td><td>This is the confirmation state, one of the following: <code>pending</code>, <code>confirmed</code>, <code>cancelled</code>, or <code>reschedule_requested</code>.</td></tr></tbody></table>

{% hint style="info" %}
**\*Note** `messageId` and `scheduledMessageId` serve different purposes:

* `messageId` identifies the message record for reconciliation and delivery status
* `scheduledMessageId` is used to update or delete the pending send itself
  {% endhint %}

## Schedule the reminder

Reminder messages are sent with a [`POST /send`](/api-reference/send.md#post-send) request, using the `scheduledDate` field as the scheduled delivery time.

### Timezone handling

The `scheduledDate` field is an [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) string with an explicit UTC offset. For example, `"20260615T143000-0500"` represents 2:30 PM in a UTC-5 timezone. There is no separate timezone parameter. The offset in the string is the timezone.

Compute the **offset for the recipient's local reminder delivery time**.

Example scenarios with an offset:

* For a reminder scheduled for 9:00 AM in `America/Chicago`, `scheduledDate` needs the `America/Chicago` offset for that delivery date, not your application server's offset.
* Daylight saving transitions mean the correct offset for a June delivery (`-0500` for Central Daylight Time) can differ from the offset in effect in December (`-0600` for Central Standard Time). Always resolve the offset against the target delivery date using your timezone library, not a hardcoded value.

**Code example:**

The following resolves the reminder's local delivery time to `scheduledDate` and computes the offset for that delivery date.

{% tabs %}
{% tab title="Node" %}

```javascript
import { DateTime } from "luxon";

function toScheduledDate(reminderLocalDateTime, timezone) {
  const dt = DateTime.fromISO(reminderLocalDateTime, { zone: timezone });
  // e.g. "20260615T143000-0500"
  return dt.toFormat("yyyyMMdd'T'HHmmssZZ").replace(":", "");
}

const scheduledDate = toScheduledDate("2026-06-15T14:30:00", "America/Chicago");
```

{% endtab %}

{% tab title="Python" %}

```python
from zoneinfo import ZoneInfo
from datetime import datetime

def to_scheduled_date(reminder_local_dt: datetime, timezone: str) -> str:
    """Attach the correct offset for the reminder delivery time and format for MTA."""
    tz = ZoneInfo(timezone)
    localized = reminder_local_dt.replace(tzinfo=tz)
    # e.g. "20260615T143000-0500"
    return localized.strftime("%Y%m%dT%H%M%S%z")

scheduled_date = to_scheduled_date(
    datetime(2026, 6, 15, 14, 30, 0),
    "America/Chicago",
)
```

{% endtab %}
{% endtabs %}

## Send the scheduled reminder

When sending the reminder message you need to set the `externalId` field to your own appointment identifier so it comes back on any webhook related to this message. Then store the `messageId` Mobile Text Alerts returns in response.

{% hint style="info" %}
**Use `tags` to carry extra context**

The `tags` field is a general purpose key/value map of the outbound message and is echoed back on any related webhook event, including the reply. Unlike `externalId`, which is a single string, `tags` can hold multiple fields. This is useful for passing metadata your reply handler needs without an extra database lookup, such as `appointmentId`, `locationId`, or `providerId`.
{% endhint %}

Scheduling a message also creates a scheduled message record you can look up, update, or delete later — see [#change-or-cancel-a-reminder-message](#change-or-cancel-a-reminder-message "mention") below.

### Personalize the reminder

The `properties` field maps each recipient number to a set of Liquid template variables referenced in `message` with `{{variableName}}` syntax. Each subscriber gets their own set of values in the same request, which is what makes it possible to send one templated reminder to many recipients with different appointment details.

The following example uses `firstName`, `appointmentTime`, `providerName`, and `location`, but you can create variables that suit your use case.

{% tabs %}
{% tab title="Node" %}

```javascript
const response = await fetch("https://api.mobile-text-alerts.com/v3/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MTA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subscribers: ["+13175551111"],
    message:
      "Hi {{firstName}}, this is a reminder for your appointment with " +
      "{{providerName}} on {{appointmentTime}} at {{location}}. " +
      "Reply C to confirm, R to reschedule, or X to cancel.",
    properties: {
      "+13175551111": {
        firstName: "Bob",
        providerName: "Dr. Alvarez",
        appointmentTime: "Tue, Jun 16 at 2:30 PM",
        location: "Downtown Clinic",
      },
    },
    scheduledDate: "20260615T143000-0500",
    externalId: `appt-482910`,
    tags: { appointmentId: "482910" },
  }),
});

const { data } = await response.json();
// Persist data.messageId (and the scheduledMessageId once you retrieve it, see below)
```

{% endtab %}

{% tab title="Python" %}

```python
import os
import requests

response = requests.post(
    "https://api.mobile-text-alerts.com/v3/send",
    headers={
        "Authorization": f"Bearer {os.environ['MTA_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "subscribers": ["+13175551111"],
        "message": (
            "Hi {{firstName}}, this is a reminder for your appointment with "
            "{{providerName}} on {{appointmentTime}} at {{location}}. "
            "Reply C to confirm, R to reschedule, or X to cancel."
        ),
        "properties": {
            "+13175551111": {
                "firstName": "Bob",
                "providerName": "Dr. Alvarez",
                "appointmentTime": "Tue, Jun 16 at 2:30 PM",
                "location": "Downtown Clinic",
            }
        },
        "scheduledDate": "20260615T143000-0500",
        "externalId": "appt-482910",
        "tags": {"appointmentId": "482910"},
    },
)

data = response.json()["data"]
# Persist data["messageId"] (and the scheduledMessageId once you retrieve it, see below)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**Message Character Limits**

Because `properties` values vary in length per recipient, the same template can render to different message lengths depending on the data. SMS messages are limited to 160 characters before they split into an additional segment (each segment billed as an additional credit). A long provider name or location string could push some messages into a second segment while others stay in one. To avoid this, keep variable values concise if consistent segment count matters for your use case.
{% endhint %}

If your reminder content is fixed and only the variables change, such as a standardized clinic reminder format, consider defining it as a reusable message template with `templateId` instead of inlining `message` on every request.

See [Send an SMS Message](/tutorials/message-sending/send-an-sms-message.md#properties-field-for-custom-variables-in-messages) to learn more.

## Handle the message reply

### How reply attribution works in SMS

It is important to note that SMS is not a threaded medium. There is no concept of a "conversation ID" that a phone enforces, instead replies simply appear in chronological order in the recipient's default messaging app, alongside every other message from that same number. When a subscriber replies, Mobile Text Alerts can only attribute that reply to **the most recent outbound message sent to that number**, because that is the only attribution the channel itself provides. This is exactly what `externalId` and `tags` deliver on the `message-reply` payload: MTA echoes back the identifying data from that last outbound send.

A subscriber could have more than one appointment reminder outstanding at the same time, such as two appointments in the same week. If they reply, the reply payload alone won't tell you *which* appointment "confirm" refers to if your message copy doesn't make it unambiguous. So the distinguishing detail has to live in the message text itself:

**Example message:**

```
Hi Bob, reminder: your appointment with Dr. Alvarez is Tue 6/16 at 2:30 PM.
Reply C to confirm this Tue 3pm appointment, R to reschedule, or X to cancel.
```

Referencing a specific day/time in the reply prompt, as shown above, gives the recipient (and your own support team, if they ever look at the thread) an unambiguous anchor — even though your backend will still resolve the reply to a specific appointment via `externalId`/`tags` rather than by parsing the message text.

### Read the webhook payload

When the recipient replies, Mobile Text Alerts posts a `message-reply` event to your configured webhook endpoint.

Relevant fields:

<table><thead><tr><th width="160">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>fromNumber</code></td><td>The subscriber's number the reply came from.</td></tr><tr><td><code>message</code></td><td>The full text content of the reply.</td></tr><tr><td><code>externalId</code></td><td>The identifier you set on the original outbound reminder — use this to look up the appointment.</td></tr><tr><td><code>tags</code></td><td>The tags object you set on the original outbound reminder.</td></tr><tr><td><code>replyId</code> / <code>messageId</code></td><td>MTA's internal identifier for this inbound reply.</td></tr><tr><td><code>timestamp</code></td><td>UTC ISO 8601 timestamp of the reply.</td></tr></tbody></table>

You need a handler that resolves a reply to the appointment and updates its status based on the recipient's response.

**Example:**

{% tabs %}
{% tab title="Node" %}

```javascript
import express from "express";
const app = express();
app.use(express.json());

app.post("/webhooks/mobile-text-alerts", async (req, res) => {
  const { externalId, tags, fromNumber, message } = req.body;

  // Look up the appointment via externalId (or tags.appointmentId)
  const appointment = await db.appointments.findByExternalId(externalId);
  if (!appointment) {
    // Nothing matched — acknowledge anyway so MTA doesn't retry
    return res.sendStatus(200);
  }

  const normalized = message.trim().toUpperCase();

  if (["C", "CONFIRM", "Y", "YES"].includes(normalized)) {
    await db.appointments.updateStatus(appointment.id, "confirmed");
  } else if (["X", "CANCEL", "N", "NO"].includes(normalized)) {
    await db.appointments.updateStatus(appointment.id, "cancelled");
    // Also cancel the underlying scheduled reminder if it hasn't fired yet
    // (guards against a duplicate/late reminder — see below)
  } else if (["R", "RESCHEDULE"].includes(normalized)) {
    await db.appointments.updateStatus(appointment.id, "reschedule_requested");
    // Kick off your own reschedule flow (agent follow-up, booking link, etc.)
  } else {
    // Unrecognized reply — route to a human, or send a clarifying auto-reply
  }

  res.sendStatus(200);
});
```

{% endtab %}

{% tab title="Python" %}

```python
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/mobile-text-alerts")
def handle_reply():
    payload = request.get_json()
    external_id = payload.get("externalId")
    from_number = payload.get("fromNumber")
    message = payload.get("message", "").strip().upper()

    appointment = db.appointments.find_by_external_id(external_id)
    if appointment is None:
        # Nothing matched — acknowledge anyway so MTA doesn't retry
        return "", 200

    if message in ("C", "CONFIRM", "Y", "YES"):
        db.appointments.update_status(appointment.id, "confirmed")
    elif message in ("X", "CANCEL", "N", "NO"):
        db.appointments.update_status(appointment.id, "cancelled")
        # Also cancel the underlying scheduled reminder if it hasn't fired yet
        # (guards against a duplicate/late reminder — see below)
    elif message in ("R", "RESCHEDULE"):
        db.appointments.update_status(appointment.id, "reschedule_requested")
        # Kick off your own reschedule flow (agent follow-up, booking link, etc.)
    else:
        # Unrecognized reply — route to a human, or send a clarifying auto-reply
        pass

    return "", 200
```

{% endtab %}
{% endtabs %}

#### **Webhook-driven handling vs. Automated Replies**

Handle confirmations through the `message-reply` webhook, as shown above, rather than with [Automated Replies](/api-reference/automated-replies.md). A confirmation may need to update a record in your system. Automated Replies work well for static information that does not depend on application state, such as a keyword that always responds with clinic hours or a cancellation policy. Anything that needs to change an appointment record belongs in a webhook handler. Note that both Automated Replies and webhook-driven replies can be used on the same number.

## Change or cancel a reminder message

Appointments can be updated or cancelled after the reminder has already been scheduled. Because the reminder was created as a scheduled send, you can look it up to update or cancel it — no need to cancel and recreate.

### View outstanding scheduled sends

Use the [`GET /scheduled`](/api-reference/scheduled-messages.md#get-scheduled) endpoint to list all scheduled messages on your account, or call [`GET /scheduled/{scheduledMessageId}`](/api-reference/scheduled-messages.md#get-scheduled-scheduledmessageid) if you have the `scheduledMessageId` for an appointment.

### Update a reminder when an appointment time has changed

If the appointment time changes, update the existing scheduled message with [`PATCH /scheduled/{scheduledMessageId}`](/api-reference/scheduled-messages.md#patch-scheduled-scheduledmessageid) rather than deleting and re-creating it. This preserves the same scheduled message record and avoids a window where no reminder is scheduled at all.

{% tabs %}
{% tab title="Node" %}

```javascript
await fetch(
  `https://api.mobile-text-alerts.com/v3/scheduled/${scheduledMessageId}`,
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.MTA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      // New scheduledDate computed for the recipient's timezone,
      // as described in Scheduling the reminder above
      scheduledDate: "20260618T100000-0500",
      message:
        "Hi {{firstName}}, your appointment with {{providerName}} has moved " +
        "to {{appointmentTime}} at {{location}}. Reply C to confirm, R to " +
        "reschedule, or X to cancel.",
    }),
  }
);
```

{% endtab %}

{% tab title="Python" %}

```python
requests.patch(
    f"https://api.mobile-text-alerts.com/v3/scheduled/{scheduled_message_id}",
    headers={
        "Authorization": f"Bearer {os.environ['MTA_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        # New scheduledDate computed for the recipient's timezone,
        # as described in Scheduling the reminder above
        "scheduledDate": "20260618T100000-0500",
        "message": (
            "Hi {{firstName}}, your appointment with {{providerName}} has moved "
            "to {{appointmentTime}} at {{location}}. Reply C to confirm, R to "
            "reschedule, or X to cancel."
        ),
    },
)
```

{% endtab %}
{% endtabs %}

#### Cancel a reminder message

If the appointment is cancelled outright, cancel the individual scheduled message so the reminder never goes out. Use [`POST /scheduled/{scheduledMessageId}`](/api-reference/scheduled-messages.md#post-scheduled-scheduledmessageid) for the endpoint and request details.

Update your own appointment record's status to `cancelled` in the same code path, so a webhook reply event arriving after the fact (see below) has a consistent state to check against.

#### Time between cancellation and delivery messages

Sometimes there is a narrow window where an appointment is cancelled *after* the reminder has already been handed off for delivery but *before* your cancellation request reaches Mobile Text Alerts. In this case the recipient gets a reminder for an appointment that no longer exists. This scenario can't be fully eliminated, but you can contain it:

* Check appointment status before acting on a `message-reply`. If a reminder fires late for an already-cancelled appointment and the recipient replies "C", your webhook handler should see the appointment is already `cancelled` and avoid flipping it back to `confirmed`.
* Where the workflow allows it, cancel the scheduled reminder as early as possible in your cancellation flow, rather than as a final step, to minimize the race window.

### Recurring reminders

For standing repeating appointments you can use the `repeat` field on the original `/send` request instead of scheduling each occurrence individually.

`repeat` takes boolean flags for each day of the week, a `type` of `week` or `month`, and a `frequency`:

```
CUSTOM = -1
NEVER = 0
DAILY = 1
WEEKLY = 2
BIWEEKLY = 3
MONTHLY = 4
ANNUALLY = 5
```

See [Repeat scheduled message sends](/tutorials/message-sending/send-an-sms-message.md#repeat-scheduled-message-sends) for the full field reference and behavior details.

## Learn More

* [Send an SMS Message](/tutorials/message-sending/send-an-sms-message.md) — this includes the full reference for `properties`, `scheduledDate`, and `repeat`.
* [Receive and Reply to Messages](/tutorials/message-sending/receive-and-reply-to-messages.md) — the general pattern for two-way messaging this guide builds on.
* [Scheduled Messages](/api-reference/scheduled-messages.md) — full API endpoint reference for listing, updating, and deleting scheduled sends.
* [Webhooks](/tutorials/webhooks.md) — see this page for event types and payload reference, including `message-reply`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://developers.mobile-text-alerts.com/appointment-reminders-with-confirmation-replies.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
