---
title: "How to Send SMS Approvals From an AI Agent and Get the Reply Back"
url: "https://textbolt.com/blog/ai-agent-human-approval-sms/"
date: "2026-08-19T07:24:34-05:00"
modified: "2026-08-27T07:31:36-05:00"
type: "Article"
resource: "https://textbolt.com/blog/ai-agent-human-approval-sms/"
timestamp: "2026-08-27T07:31:36-05:00"
author:
  name: "Rakesh Patel"
categories:
  - "SMS Alerts"
word_count: 3441
reading_time: "18 min read"
summary: "Every AI agent doing unattended work eventually hits a decision it should not make alone. An unusual refund, an ambiguous instruction, a payment above a risk threshold. The agent stops and waits fo..."
description: "Learn how AI agents can send human approval requests by SMS, receive replies by email, and safely handle timeouts without building a webhook."
keywords: "AI Agent Human Approval SMS, SMS Alerts"
language: "en"
schema_type: "Article"
related_posts:
  - title: "From Offshore Email to a Phone Ashore: A Dockside Test Plan"
    url: "https://textbolt.com/blog/send-text-from-boat-offshore/"
  - title: "How to Send SMS Alerts for Public Safety"
    url: "https://textbolt.com/blog/sms-alerts-for-public-safety/"
  - title: "Proof of Notification: The Record Your Insurer and Your Lawyer Will Ask For"
    url: "https://textbolt.com/blog/proof-of-notification/"
---

# How to Send SMS Approvals From an AI Agent and Get the Reply Back

_Published: August 19, 2026_  
_Author: Rakesh Patel_  

![How AI Agents Can Get Human Approval by SMS](https://wp.textbolt.com/wp-content/uploads/2026/08/How-AI-Agents-Can-Get-Human-Approval-by-SMS-1-convert.io_-1024x576.webp)

Every AI agent doing unattended work eventually hits a decision it should not make alone. An unusual refund, an ambiguous instruction, a payment above a risk threshold. The agent stops and waits for a human.

The waiting is the problem. Slack gets muted at night. Email reaches an inbox rather than a person, and rarely before morning. So the agent either blocks and stalls the queue, or proceeds and causes the exact incident the check existed to prevent.

Most teams building agents hit this, and few have solved it, because a real paging path looks like a week of infrastructure for a check that fires four times a month.

There is a shorter path. If your agent can send an email, it can send an approval request by SMS, and the reply lands back in the same inbox the agent already reads. An [email to SMS service](https://textbolt.com/solutions/email-to-text-service/) such as TextBolt handles the conversion in both directions.

## What Happens When Your AI Agent Needs a Human to Say Yes

Say your agent is processing invoices at 2 AM. It hits one for $48,000 from a vendor added eleven days ago. That is exactly the kind of payment nobody wants an agent making on its own.

So the agent needs to ask someone. In code, that asks two steps: send the question, then wait for the answer.

```
decision_id = request_approval("Pay $48,000 to Vendor #2231 (onboarded 11d ago)?")
verdict = await_reply(decision_id, timeout=900, default="DENY")

if verdict == "APPROVE":
    release_payment()
else:
    queue_for_morning_review()
```

These two functions are not from a library. You write them, and this post shows you how. Here is what each one does.

- **Line 1 sends the question:** request_approval emails your TextBolt address, which turns that email into a text message on your on-call person’s phone. It hands back a decision_id, a short random code that gets printed in the text. You need it because if two approvals go out at once, a reply saying “APPROVE” is useless unless you know which question it answers.
- **Line 2 waits for the answer:** The person replies to the text. That reply comes back as an email in the same inbox your agent is already watching. await_reply checks that inbox until it finds a reply carrying the matching ID, then returns whatever verdict it read.

**timeout=900 is fifteen minutes.** After that, the agent stops waiting.

**default=”DENY” is what happens when nobody answers.** This is the part most people get wrong. Your on-call person may be asleep, may have a dead phone, may just miss it. So the function is written to always return something. If no reply arrives in fifteen minutes, it returns “DENY” and the invoice goes to the morning queue.

That default is the whole point of the design. You are not building something that gets an answer, because you cannot guarantee one. You are building something that does the safe thing when the answer never comes. So the default is always the reversible choice: hold it, deny it, pause it. Never pay it.

Those eight lines are the entire pattern: email to SMS to email. No webhook receiver, no public endpoint, no phone number provisioning, no callback handling. You can build it in an afternoon.

It does have a ceiling, and the last section shows you where it is. Volume, sub-second decisions, and messaging customers rather than your own staff are the three lines that push you toward a real SMS API. Many teams never reach any of them.

## Why AI Agents Fail to Reach Your User Phone

If the pattern is that short, why does almost nobody have it? Because the obvious alternatives each fail somewhere different.

- **Slack is not a paging system.** It is an attention market that closes at night, and Do Not Disturb exists precisely so it cannot wake anyone.
- **Email is not synchronous.** It reaches an inbox reliably and a human unreliably, about four hours later.
- **Building real SMS is a project.** Buy a number, register an A2P 10DLC brand and campaign, a review measured in days rather than minutes, and host a public HTTPS endpoint for inbound webhooks. Then add auth on that endpoint, retry logic, idempotency, and compliance.

All of that is justified at scale. None of it is justified for a check that fires four times a month.

So the check gets built as a Slack message, and the 2:14 AM problem stays unsolved.

## What an Escalation Path Actually Needs

Strip the tooling away and an escalation path needs three properties.

- **It reaches a human on the device they actually respond to.** At 2 AM that device is a phone, and the format that gets through is a text.
- **The reply comes back into the agent’s context.** A notification with no return path is an FYI, not an escalation. That is the difference between line 1 and line 2 of the snippet, and line 2 is the hard one.
- **It requires no infrastructure you host.** No inbound endpoint to secure, monitor, and keep alive for a low frequency feature.

| **Option** | **Reaches a human** | **Reply gets back to agent** | **You host** | **Setup** | **Verdict** |
|---|---|---|---|---|---|
| **Slack ping** | Only if awake and at a desk. Muted at night | Needs a bot and an events endpoint | Bot host or receiver | Hours | Fine for daytime. Cannot wake anyone |
| **Plain email** | Hours later, if at all | Same inbox, polled | Nothing | Minutes | Free and easy, but not an escalation |
| **SMS API** (Twilio-class) | Seconds | Webhook to a public endpoint | HTTPS receiver, retries, idempotency | Days: number, registration, endpoint | Correct at scale. Overbuilt for four pages a month |
| **Email to SMS gateway** (TextBolt) | Seconds, to a phone* | Same inbox, polled | Nothing | An afternoon, plus one-time 10DLC registration | Both directions, no endpoint. Poll latency is the tradeoff |

*Provider dependent. Latency is covered honestly further down, with numbers you can measure yourself.

The email to SMS platform like TextBolt works because agents already send email trivially, since every language ships an SMTP client. TextBolt turns an email addressed to **+1XXXXXXXXXX@sendemailtotext.com into a text, then threads the human’s SMS reply back into the sending inbox as a normal email.

That address is the one config line. Your agent’s inbox becomes the callback channel you did not have to build, which is what makes **await_reply on line 2 possible at all.

Sending the text is the easy part, and most tutorials stop there. Getting the reply back into your running agent is where this gets interesting, and that is what the rest of this post covers.

## How the Email to SMS Approval Loop Works

Agents already send email trivially. Every runtime ships an SMTP client. An email addressed to **+1XXXXXXXXXX@sendemailtotext.com** becomes a standard text on a compliant carrier route.

The recipient texts back. The reply arrives as a threaded email reply in the same mailbox the agent sent from. The agent’s inbox becomes the callback channel you did not have to build.

### Sending the Approval Request From Python

Sending is the easy half. The [send text with no SDK](https://textbolt.com/blog/send-text-no-sdk/) breakdown covers the gateway mechanics if you want the longer version.

```
import os, secrets, smtplib
from email.message import EmailMessage

SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.gmail.com")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ["SMTP_USER"]        <em>

# a dedicated inbox the agent owns</em>
SMTP_PASS = os.environ["SMTP_PASS"]        <em>

# dedicated mailbox credential; see auth note</em>
ONCALL    = os.environ["ONCALL_ADDRESS"]   <em>

# +15551234567@sendemailtotext.com</em>

def request_approval(summary: str, options=("APPROVE", "DENY")) -> str:
    decision_id = secrets.token_hex(8).upper()   <em>

# 64 random bits; also enforce uniqueness in your state store</em>
    body = f"[{decision_id}] {summary[:55]} Reply exactly: {decision_id} {' or '.join(options)}"
    msg = EmailMessage()
    msg["From"], msg["To"] = SMTP_USER, ONCALL
    msg["Subject"] = f"AGENT {decision_id}"      <em>

# subject + body become the SMS</em>
    msg.set_content(body)
    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
        s.starttls()
        s.login(SMTP_USER, SMTP_PASS)
        s.send_message(msg)
    return decision_id
```

The human receives something like:

AGENT 4D91A7C20F83B612 [4D91A7C20F83B612]
Case 2231 is held for review. Take ownership?
Reply exactly: 4D91A7C20F83B612 APPROVE or DENY

### Reading the Reply Back Into the Agent

This is the half that turns a notification into an escalation, so it gets the real code.

```
import imaplib, email, time

IMAP_HOST     = os.environ.get("IMAP_HOST", "imap.gmail.com")
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "20"))   <em>

# your added latency floor</em>

def await_reply(decision_id: str, timeout: int = 900,
                default: str = "DENY", vocab=("APPROVE", "DENY")) -> str:
    """Block until the on-call human replies, or return `default` on timeout.
    Only ever returns a value from `vocab` or `default`, never raw text."""
    pattern  = re.compile(rf"b{re.escape(decision_id)}s+({'|'.join(vocab)})b", re.I)
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        with imaplib.IMAP4_SSL(IMAP_HOST) as imap:
            imap.login(SMTP_USER, SMTP_PASS)
            imap.select("INBOX")
            _, data = imap.search(None, "(UNSEEN)")
            for num in data[0].split():
                <em>

# BODY.PEEK reads without marking seen, so unrelated mail is not consumed</em>
                _, raw = imap.fetch(num, "(BODY.PEEK[])")
                msg = email.message_from_bytes(raw[0][1])
                if ONCALL.split("@")[0] not in (msg.get("From") or ""):
                    continue                        <em>

# sender check, see the injection section</em>
                match = pattern.search(_plain_text(msg))
                if match:
                    imap.store(num, "+FLAGS", "\Seen")
                    return match.group(1).upper()
        time.sleep(POLL_INTERVAL)
    return default          <em>

# nobody answered, do the pre-decided safe thing</em>

def _plain_text(msg) -> str:
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                return part.get_payload(decode=True).decode(errors="replace")
        return ""
    return (msg.get_payload(decode=True) or b"").decode(errors="replace")
```

Five knobs decide how this behaves in production.

| **Setting** | **Default** | **What it controls** |
|---|---|---|
| POLL_INTERVAL | 20s | Added latency floor. Median is half the interval, worst case is the interval |
| timeout | 900s | How long the agent waits before acting alone |
| default | DENY | What acting alone means. Always the reversible option |
| vocab | APPROVE, DENY | The only strings that can reach your control flow |
| ONCALL_ADDRESS | none | The entire integration. Changing the on-call human is changing this string |

Human reaction time dominates end to end latency and is unbounded. The poll interval is the only part you control, so measure gateway delivery yourself rather than trusting a published figure. The repo ships a timing harness for it.

### Running the Same Loop in Other Languages

Python is the common case for agents, so it gets the full treatment above. The Node version of both halves, plus one shot senders for Go and for shell, the second useful as a CI deploy gate, live in the companion repo.

### Wiring the Approval Loop Into LangChain

On a LangChain style runtime the whole loop collapses into one tool. Note what the model receives back, which is a word from the vocabulary and never raw inbound text.

```
from langchain_core.tools import tool

@tool
def ask_human(question: str) -> str:
    """Escalate a decision to the on-call human by SMS.
    Blocks until they reply, or returns the safe default on timeout."""
    decision_id = request_approval(question)
    return await_reply(decision_id, timeout=900, default="DENY")
```

Two prerequisites. Gmail and Microsoft 365 require an app password or OAuth for SMTP and IMAP from scripts, set up on a dedicated agent mailbox rather than a person’s account.

Second, the sending mailbox has to be registered as a sender before anything is delivered. Pointing arbitrary SMTP at the gateway fails quietly.

Your Agent Can Text a Human and Hear Back With TextBolt

Change the destination address and your agent’s email arrives as a text, with the reply threaded back to the same mailbox. Setup takes about 30 minutes, plus 10DLC approval before your first send.

 [Start Free Trial](https://my.textbolt.com/signup/)

## How to Design the Message So a Human Can Answer in Five Seconds

The transport is solved. What is left is the interface, and the interface is a groggy human and roughly 160 useful characters. Everything below follows from that constraint.

Here are the 5 important rules for you:

### 1. One Decision Per Message

“Approve refund AND update the vendor record?” is two decisions wearing one question mark. The person will answer one of them, or answer both with a single word that tells you nothing about which. Split them into two messages with two IDs. If that feels like too many texts, the real problem is that you are escalating things that do not need a human.

### 2. Front Load the Context That Changes the Answer

The reader gets one glance. Give them the amount, the counterparty, and the anomaly that triggered the check, in that order.

“$48,000, vendor is 11 days old” is the whole risk story. The invoice number, the internal transaction reference, and the name of the rule that fires are not. They belong in your logs, where someone can look them up if the answer turns out to be wrong.

The test is simple. If a detail would not change the verdict, it is taking up characters that something else needs.

### 3. Constrain the Reply Vocabulary and Print It in the Message

Tell the person exactly what to type. **Reply: A7F3 APPROVE or DENY.**

Not “let me know what you think,” and not “reply Y or N” unless Y and N are literally what your parser accepts.

Freeform replies are unparseable at 2 AM and dangerous at any hour. Unparseable because your regex has to guess at intent from a half-awake human. Dangerous because the moment you start guessing, you have built a system that sometimes approves things nobody approved.

Printing the vocabulary in the message costs you about fifteen characters and removes an entire class of failure.

### 4. Make the ID Unambiguous and Required

Generate random hex, print it in the message, and require it echoed back in the reply.

It does two jobs. First, it is your correlation key when two escalations are in flight, which is the normal case rather than the exotic one. Second, it is part of your spoofing resistance, because an attacker who does not know the ID cannot forge an approval for a decision they cannot name.

Match on ID and verb together. A bare “APPROVE” with no ID matches nothing and falls to the default.

### 5. Decide the Timeout Action Before You Ship

Then say it in the message when it is not obvious. “No reply in 15 min = held for morning review” turns silence into a defined outcome instead of an undefined one.

This matters more than it looks. Without it, the person receiving the text has no idea whether ignoring it is safe. With it, ignoring the text becomes a valid choice they made knowingly.

The default is always the reversible path: hold, deny, pause. Never “proceed.” If the reversible path is not acceptable for a given decision, that decision should not be gated behind a single text message.

## Common AI Agent SMS Approval Failures and How to Handle Them

Every rule above assumes things go to plan. Here is what happens when they do not.

- **Timeouts:** Somebody will sleep through it, routinely rather than rarely. The default resolves it, so the only question is which safe default. If none exists, add a second on-call address and a staggered timeout.
- **Freeform replies:** People text “yes go ahead” at 2 AM because that is how they talk. Strict parsing drops it to the default, which irritates them in the morning but keeps you safe. Loosen the regex consciously, never past a fixed vocabulary.
- **ID collisions:** Two escalations in flight and one distracted person is the normal case. Matching on ID plus verb means a bare APPROVE matches nothing and the timeout runs.
- **Prompt injection:** The reply is untrusted input and could say anything. Parse against your vocabulary, return the matched token or the default, discard the rest. The raw body never touches the model. Verify the From address and keep the mailbox unpublished.
- **Latency:** Human reaction time dominates everything. Your poll interval adds seconds to a variable measured in minutes.
- **Alert fatigue:** Six pages a night and nobody reads any of them. Fix your thresholds, not your pipe.
- **Consent:** Employment is not consent. Get on-call opt-in in writing, and send through a 10DLC-registered number.

## Four Limits of Human in the Loop Agent Escalation

Four things put you past the ceiling. All four are narrower than people assume.

- **Volume.** A few escalations a day is fine. Dozens an hour is where polling a single inbox stops being reasonable. Check your real number first, since most approval gates fire a few times a week.
- **Sub-second decisions.** Your floor is the poll interval, and no tuning gets under it. But the human takes minutes regardless, so sub-second matters only when a machine is waiting.
- **Rich payloads.** If someone needs images, forms, or multi-field input to decide, 160 characters is the wrong interface. The tell is a summary that will not fit.
- **Per-tenant provisioning.** Numbers per customer, opt-out state across thousands of recipients, localization by region. That is platform work.

## How TextBolt Closes the Human in the Loop

Inside those four limits, TextBolt runs the loop. Your agent sends email, the reply threads back to the same mailbox, and that return path is what **await_reply depends on.

- **The reply threads back to the sending mailbox.** The SMS reply arrives as a threaded email reply in the inbox the agent sent from. This is the property that varies most across providers, and it is the one the pattern cannot work without.
- **The agent gets its own account.** Multi-user messaging on Standard and above means a dedicated, unpublished mailbox is a security control rather than an upsell. A secondary on-call is just a second registered address.
- **Every send is logged with a delivery status.** When an approval times out, you can tell whether the text reached the phone or the person ignored it. No other part of this stack answers that.

The one setup cost is registration. Your sending address and business number are registered under 10DLC before the first message, which takes up to 48 hours. That step applies to every US business texting route, including the API you would otherwise build.

Plans start at $29 per month with 500 credits. A toll-free business number is $45 per year, and annual billing takes 20 percent off. Messages are text only, with no MMS, on 10DLC routes at up to 98% delivery rates.*

## Set Up Human in the Loop Agent Escalation With TextBolt

An agent that cannot reach a person will either stall the queue or decide on its own. Both come from the same missing piece, and neither is an outcome anybody chose.

Sending the text is the easy half. The reply landing back where your agent can read it is what turns a notification into an approval, and it is the property most email to SMS services do not have.

Your agent keeps its existing email code. Only the destination address changes, which is the same no SDK, no code change path teams already use to point monitoring alerts at a phone.

The repo has the Python implementation, the LangChain tool, the Go and shell senders, and a harness for timing your own round trip. Clone it, point the mailbox settings at an inbox you control, then register that inbox as a sender.

The free trial includes 10 credits, enough to run one real approval end to end. Setup takes about 30 minutes, plus 10DLC approval before your first send.

Still working out whether this pattern fits your volume? [Contact us](https://textbolt.com/contact/) and we will walk your escalation path with you before you build anything.

Test the Full Loop Before You Commit

The free trial includes credits to run a real approval end to end. Setup takes about 30 minutes, plus 10DLC approval before your first send.

 [Try TextBolt Free for 7-Day](https://my.textbolt.com/signup/)

## Frequently Asked Questions

**How do you get approval from a human in an AI workflow?**

Give the agent a blocking escalation step. Send a short, ID tagged decision to a phone, parse the reply against a fixed vocabulary, and resolve to a safe default on timeout. The loop above needs no webhook endpoint.

**Can an agent send an SMS from Python without an SMS API?**

Yes, if it can send email. Standard library smtplib addressed to a gateway address delivers the message as a text, and imaplib picks the reply back up from the same mailbox. No SDK required.

**Is polling an inbox reliable enough for production escalation?**

For low volume decisions where human reaction time dwarfs a 20 second poll interval, yes, provided every escalation has a timeout and a safe default. For high volume, sub second loops, or customer messaging, build on a real SMS API.

**Should the agent block while it waits, or checkpoint?**

Blocking is fine for a prototype. In production a fifteen minute wait means a held thread and a decision that vanishes if the process dies, so checkpoint and let the reply resume the run instead.


---

_View the original post at: [https://textbolt.com/blog/ai-agent-human-approval-sms/](https://textbolt.com/blog/ai-agent-human-approval-sms/)_  
_Served as markdown by [Third Audience](https://github.com/third-audience) v3.6.1.1_  
_Generated: 2026-08-27 12:31:37 UTC_  
