How to send appointment reminders by SMS from an AI agent
Before an AI agent texts a reminder, record the person's consent to appointment texts and register the use case (a 10DLC campaign or toll-free verification). Then keep the agent's wording to the registered samples by filling a fixed template, schedule each text for a sensible hour in the appointment's time zone, send it with one idempotency key per reminder, and route replies back through an sms.received webhook with STOP already handled.
Unitpost ·
An agent that books appointments can also remind people about them. The text itself is short. What goes wrong is everything around it: a reminder to someone who never agreed to texts, wording that doesn't match what carriers approved, a text at 5:40 in the morning, two copies after a retry, and a STOP reply the model tries to answer.
Each of those has a fix that lives in code, not in the prompt.
Record consent before the first reminder
The CTIA Messaging Principles, the wireless industry's guidelines for business texting, put appointment reminders in the informational category. The person gives you their number and agrees to texts for that purpose, and that express consent comes before the first text. It covers reminders. It doesn't cover promotions.
Unitpost doesn't make that check for you on a reminder. A transactional text to a number you've never stored still goes out, because only marketing needs consent on record. So the agent's send tool has to ask. Record the opt-in where it happens, on your booking form:
POST /contacts/{id}/sms-consentwithaction: "opt_in",purpose: "transactional"and asourcesuch asweb_form.occurred_atfor when they agreed (backdate imported consent to the real moment; a future time is refused),evidence_urlpointing at your own record, anddisclosure_versionfor the checkbox wording they saw.
Then make the send tool read GET /contacts/{id}/sms-consent first and refuse when consented_at is empty or status isn't active. The model never decides whether someone agreed. Your tool does.
Unitpost's appointment reminder recipe words the checkbox "Send me Acme SMS appointment reminders.", leaves it unchecked by default, and states how often texts come.
Only people who booked
A reminder goes to someone who made an appointment and agreed to texts about it. An agent never texts a number taken from a web page or a bought list. Unitpost's Acceptable Use Policy prohibits both on every plan.
Register the use case and keep the agent inside it
US carriers treat every business text as application-to-person traffic, so the number needs a 10DLC campaign or toll-free verification before it sends. Unitpost's appointment reminder recipe registers a transactional program: ACCOUNT_NOTIFICATION on 10DLC, APPOINTMENT for toll-free, with no embedded links and no embedded phone numbers. Its sample messages read:
Acme: reminder: your appointment is tomorrow at 10:00. Reply STOP to opt out.
Acme: your appointment is in 1 hour. Reply STOP to opt out.
Acme: your appointment is confirmed. Reply STOP to opt out.What the agent sends has to look like those. Unitpost's SMS Messaging Policy says it directly: what you send must look like the sample messages and use case you registered. A reminder that grows a "book your next cleaning and save 10%" line, or a link the registration said it wouldn't carry, is no longer the program carriers approved.
10DLC rules for AI-generated text messages covers what drift looks like and what to do when the use case really changes.
Should the agent use a template or write the text?
Use a template for the reminder. POST /sms takes the finished body (up to 1,600 characters), so keep the wording in your code and let the agent supply only the fields: the date, the time, perhaps the location. Your code renders the text, so every reminder matches the registered sample, starts with your business name and ends with the opt-out line.
Free text belongs in replies, inside a conversation the person started. Even there, keep the agent on the subject of the appointment.
Two details a model tends to get wrong:
- Opt-out words. CANCEL and END are opt-out keywords on Unitpost numbers, just like STOP. "Reply CANCEL to cancel your appointment" opts the person out of every text you send, reminders included. Ask for a reply that isn't a keyword, such as 1 to confirm and 2 to reschedule.
- Characters. One emoji or curly quote switches a text from GSM-7 to UCS-2, which cuts a segment from 160 characters to 70, and texts are billed per segment. Check each template with the SMS segment calculator.
Send it at a sensible hour in the recipient's time zone
Unitpost holds marketing texts for the recipient's local window, 8:00 to 21:00, narrower where a law or operator code sets it. A reminder is transactional, and transactional texts are never held: a reminder scheduled for 5:40 a.m. goes out at 5:40 a.m. Picking the hour is your job.
Use the appointment's time zone, meaning the clinic's or the shop's. Not the server's, and not a guess from the area code: someone with a number from another state still has the appointment where it is. Unitpost's quiet-hours check maps US area codes to zones and, when a code spans more than one, falls back to the hours that are inside the window in every US zone. That fallback protects marketing sends. It doesn't pick a time for your reminder.
scheduled_at makes you commit. It takes ISO 8601 with Z or an offset, such as 2026-10-27T08:00:00-07:00, and a time with no zone is refused with a 422. Compute the instant in your code from the appointment record. Don't ask the model to do offset arithmetic, least of all across a daylight saving change.
Use one idempotency key per reminder
Agents retry. A timeout, an ambiguous error or a second pass over the same task produces a second call. POST /sms takes an Idempotency-Key header, and the MCP sms_send tool takes idempotency_key. The rules:
- Same key and same body within 24 hours: the original message comes back, and no second text goes out.
- Same key with a different body:
409 invalid_idempotent_request. - Same key while the first request is still in flight:
409 concurrent_idempotent_requests. - A refused send releases the key, so the corrected request can reuse it.
Derive the key from the appointment and the reminder slot, for example appt_8812-reminder-24h, never from the attempt. The SDKs generate a key when you pass none, but that only covers the SDK's own network retries. An agent calling the tool again is a new call with a new key.
The 409 on a changed body matters with a model in the loop. If the agent rewords the reminder on a retry, the same key refuses it instead of sending a second, different text. That is the protection working. Don't answer it with a fresh key.
The window is 24 hours, and reminders are often scheduled days ahead. Store the returned sms_… id on the appointment and check it before scheduling another. When the appointment moves, change the text you already have: PATCH /sms/{id} with a new scheduled_at to move it, or { "cancel": true } to drop it (sms_update in MCP).
Route replies back to the agent
US toll-free, 10DLC and Canadian long-code numbers take replies. Alphanumeric Sender IDs don't. Subscribe a webhook to sms.received and each reply arrives with from, to (your number), body, keyword, auto_replied, looks_like_opt_out, in_reply_to (the reminder they answered, when the carrier says which) and received_at.
In the handler, before anything reaches the model:
- Verify the webhook signature on the raw body.
- Drop the event when
keywordisOPT_OUTorauto_repliedis true. STOP is already recorded and confirmed, and HELP already has its answer. - When
looks_like_opt_outis true ("please stop texting me"), send it to a person, not the agent. - Otherwise load the conversation with
GET /sms?participant=<number>, hand it to the agent, and answer withsms.sendand nofrom, so the answer leaves from the number they texted.
A "1" becomes a confirmed appointment in your system. A "2" starts a reschedule. If the person opts out between booking and the reminder, the scheduled text is checked again when it's due, ends suppressed without a charge, and sms.suppressed fires.
How to handle STOP and HELP when an AI agent sends texts has the handler code and the opt-out rules in full.