SMS for AI Agents: Send, Read Replies, Take Action
This guide turns any AI agent into something that texts real people and handles what they text back. It is three API calls: one to send, one to read, one to reply. It works the same from Hermes, OpenClaw, LangChain, a cron script, or Claude through the MCP server. If your agent can make an HTTP request, it can run a phone number.
The Pattern: Agent-Initiated Texting for AI Agents
Most SMS docs assume a human is typing. Agents are different: they run on schedules, they retry, and they need to read the reply hours later without a browser session. The shape that works is:
Prerequisites: an API key from the dashboard and a provisioned US local number. Every request below authenticates with Authorization: Bearer ac_live_...
1. Send a Text from Your Agent
One call. from takes the number ID or the E.164 string of a number you own, body is 1 to 1600 characters.
curl -X POST https://api.agentcall.co/v1/sms/send \
-H "Authorization: Bearer ac_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+12702468123",
"to": "+13145550142",
"body": "Hi Dana, invoice 1042 for the Maple St repaint, $4,200, due Friday. Pay here: https://buy.stripe.com/inv1042",
"idempotencyKey": "inv-1042-reminder-1"
}'{
"id": "msg_cmq47obtq004x",
"from": "+12702468123",
"to": "+13145550142",
"body": "Hi Dana, invoice 1042 ...",
"status": "queued",
"cost": 0.015,
"createdAt": "2026-08-09T14:02:11.000Z"
}Confirm it actually landed. The 201 means the carrier accepted the message, not that a handset showed it. Read the message back a moment later for the real outcome:
curl https://api.agentcall.co/v1/sms/msg_cmq47obtq004x \
-H "Authorization: Bearer ac_live_YOUR_API_KEY"
# { "status": "delivered", ... }
#
# status: queued -> sent -> delivered | failed
# queued handed to the carrier, no receipt yet
# sent carrier accepted it, handset delivery unconfirmed
# delivered confirmed on the recipient's handset
# failed carrier rejected it; see errorCode (40010 = number not
# registered for A2P texting yet)Always set idempotencyKey for automated sends. Agents retry. A replayed request with the same key returns 201 with the header X-AgentCall-Idempotency-Replayed: true and does not send or bill a second message. Derive the key from the work item, like inv-1042-reminder-1, not from a timestamp.
Rate limit: 60 sends per minute per account. Pricing: $0.015 per outbound text, $0.008 per inbound, no per-segment math. See pricing.
Over MCP the same call is the send_sms tool. For a text that should go out later or on a schedule, use create_schedule (POST /v1/numbers/:numberId/schedules) instead of running your own timer.
2. Read the Reply: Polling or Push
Every inbound text is stored against the number the moment it arrives. What varies is how your agent learns about it. There are two patterns, and picking the right one is mostly a question of how much infrastructure you want to run.
Option A: Poll the inbox (start here)
No public endpoint, no webhook verification, nothing to deploy. Your agent asks for new messages whenever it wants them. This is the right choice for scheduled workflows like invoice chasing, where a reply an hour later is fine.
curl "https://api.agentcall.co/v1/sms/inbox/num_cmpexgapz001r?since=2026-08-09T14:00:00Z&limit=20" \
-H "Authorization: Bearer ac_live_YOUR_API_KEY"
# {
# "data": [
# {
# "id": "msg_cmq48xk2m011a",
# "from": "+13145550142",
# "to": "+12702468123",
# "body": "Ah thanks, I'll get it out Thursday.",
# "otp": null,
# "receivedAt": "2026-08-09T14:38:27.000Z"
# }
# ],
# "hasMore": false
# }Note the path takes the number ID (the num_... string from GET /v1/numbers), not the phone number. Track the last receivedAt you processed and pass it as since on the next poll.
Option B: Relay mode (AgentCall pushes to your agent)
Set the number's smsMode to relay and AgentCall HMAC-signs and POSTs every inbound text to your HTTPS endpoint within seconds. AgentCall runs no LLM in this mode; your agent is the brain, AgentCall is the pipe. Use this when reply latency matters or your agent is already a server.
curl -X POST https://api.agentcall.co/v1/numbers/num_cmpexgapz001r/inbound-config \
-H "Authorization: Bearer ac_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "ai",
"systemPrompt": "You answer calls for Precision Painting. Take a message with the caller name and number.",
"smsMode": "relay",
"agentWebhook": {
"url": "https://agent.yourdomain.com/agentcall/sms",
"signingSecret": "a-random-secret-at-least-16-chars"
}
}'You do not have to build the relay: one command installs it
Most agents run on a laptop or a VPS with no public HTTPS endpoint, so the honest version of "point a webhook at your agent" used to be "write a webhook, host it somewhere reachable, then write the service that drains it and replies." All of that is now open source and set up by one command.
git clone https://github.com/Kintupercy/agentcall-hermes-bridge.git
cd agentcall-hermes-bridge && npm install && npx wrangler login
./bootstrap.sh --install-consumer \
--number-id num_cmpexgapz001r \
--allow +15551234567That creates the always-on HTTPS endpoint your agent lacks, generates and installs the signing secrets on both sides, installs a service next to your agent that restarts forever, puts your number into relay mode, and runs a signed self-test. It refuses to print READY unless that self-test passes. It defaults to a hosted subdomain, so you need no domain and no DNS, and --dry-run prints every command before anything is created.
If you run Hermes, you write nothing at all: the official adapter is detected automatically. For any other agent you write one script that takes the text on stdin and prints the reply on stdout.
agentcall_sms_consumer.py preflight # config, bridge, API key, your agent. Sends nothing.
agentcall_sms_consumer.py selftest # a signed synthetic text through the real loop. Texts nobody.
agentcall_sms_consumer.py verify --number +15551234567
# the real one: you text the number, this watches it landTexts are never lost in transit. Each one is claimed rather than deleted, and acknowledged only once your agent's reply has actually been sent, so a crash, a restart, or an agent that errors mid thought means the text comes back instead of disappearing. Source, threat model, and the full walkthrough: github.com/Kintupercy/agentcall-hermes-bridge.
Before you connect a powerful agent. A number in relay mode lets a text message attempt anything your agent can do. If it can run commands, spend money, or read your files, decide deliberately whether those tools belong on this channel, and put a narrower agent on SMS if not. allowedSenders helps, but caller ID is a claim rather than a credential.
Or build your own relay endpoint
POST https://agent.yourdomain.com/agentcall/sms
Content-Type: application/json
X-AgentCall-Signature: sha256=8f3a1c... (HMAC-SHA256 of the raw body)
X-AgentCall-Event: sms.relay
{
"message": {
"id": "msg_cmq48xk2m011a",
"from": "+13145550142",
"to": "+12702468123",
"body": "Ah thanks, I'll get it out Thursday.",
"receivedAt": "2026-08-09T14:38:27.000Z"
},
"conversation": {
"id": "smsconv_cmq47obtq004x",
"contactPhone": "+13145550142"
},
"context": { "channel": "sms", "numberId": "num_...", "agentId": "agent_..." },
"smsContext": {
"currentMessage": { "id": "msg_cmq48xk2m011a", "direction": "inbound", "body": "Ah thanks, I'll get it out Thursday.", "createdAt": "2026-08-09T14:38:27.000Z", "ageDays": 0, "isGreeting": false },
"recentMessages": [ { "id": "msg_...", "direction": "inbound", "body": "Hi Laura", "createdAt": "2026-08-08T18:30:00.000Z", "ageDays": 1, "isGreeting": true } ],
"recentSubstantiveMessages": [],
"olderMessages": [ { "id": "msg_...", "direction": "inbound", "body": "How much is it?", "createdAt": "2026-06-08T14:00:00.000Z", "ageDays": 62, "isGreeting": false } ],
"freshness": { "windowDays": 7, "referenceTime": "2026-08-09T14:38:27.000Z", "recentMessageCount": 1, "recentSubstantiveCount": 0, "olderMessageCount": 1 },
"sources": ["sms_recent", "sms_older"]
}
}Verify the signature by computing HMAC-SHA256 of the raw request body with your signing secret and comparing to the header. Delivery retries on non-2xx responses, so make your handler idempotent on message.id.
Use smsContext for anything about recency. It is the thread already sorted by age, so your agent never has to treat the last few texts as a recent conversation. On a quiet thread they can be months old, and an agent that conflates the two will describe a June exchange as something you just talked about. recentMessages covers the last 7 days, recentSubstantiveMessages is that list with plain greetings removed, and olderMessages is background only. When recentSubstantiveMessages is empty, the honest answer is that there has been no recent substantive text discussion. The field is additive, so an endpoint written against the older payload keeps working unchanged.
The systemPrompt in the config powers inbound voice AI on the same number, not relay texts, so give it a real prompt: one number can answer calls with AgentCall's voice AI and relay texts to your agent at the same time.
Personal agents: lock the number to yourself. Add allowedSenders: ["+1..."] to the config and texts from anyone else are dropped before they reach your webhook. For the full personal-agent story see Text Your Own AI Agent. Relay works on the free tier: every Free account gets 20 outbound texts a month, and your agent's replies come out of that same allowance. Pro is unlimited. Two-way AI SMS, where our model writes the reply, is Pro only.
There is a third mode, smsMode: "ai", where AgentCall's own managed model answers texts using your prompt. That is a different product shape (AgentCall as the brain); this guide is about your agent staying the brain.
3. Reply in the Same Thread
When your agent has thought about the reply (checked the ledger, updated the sheet), it answers on the conversation, not with a raw send. The conversation endpoint enforces STOP opt-outs for you and keeps the whole exchange threaded per contact, which is also your agent's memory of the relationship.
curl -X POST https://api.agentcall.co/v1/sms-conversations/smsconv_cmq47obtq004x/reply \
-H "Authorization: Bearer ac_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Perfect, noted for Thursday. The link stays live until then.",
"idempotencyKey": "inv-1042-reply-1"
}'GET /v1/sms-conversations/:id returns the last 50 messages of the thread, oldest first. Feeding that history to your agent before it composes is what makes the reply sound like a continuation instead of a cold open. Over MCP these are get_sms_conversation and reply_to_sms_conversation.
4. Let the Agent Act Mid-Conversation (Action Bridge)
If you use smsMode: "ai" (AgentCall answers texts with your prompt), you can hand the AI real tools. You declare up to 8 tool definitions and one actionWebhook URL; when the model decides to use a tool mid-conversation, AgentCall POSTs the call to your endpoint and relays your result back into the reply. Your server stays the hands even when AgentCall is the mouth.
POST https://agent.yourdomain.com/agentcall/action
X-AgentCall-Signature: sha256=...
X-AgentCall-Event: action.invoke
{
"tool": "mark_invoice_promised",
"arguments": { "invoiceId": "1042", "promisedDate": "2026-08-13" },
"context": {
"channel": "sms",
"contact": { "phone": "+13145550142", "name": null },
"threadId": "smsconv_cmq47obtq004x",
"callId": null
}
}
# You respond within the timeout (default 4s):
{ "result": "Invoice 1042 marked promised for Aug 13." }The same tools work on voice calls with channel: "voice", so one webhook serves both. Failures are soft: if your endpoint is down, the AI tells the customer it could not complete the action instead of claiming it did.
5. Who Your Agent Can Text on Day One
New accounts start with guardrails, because a brand-new account texting a list of strangers looks exactly like spam to carriers and to us. Here is what works immediately and how the limits lift:
The error codes are designed for agents: a 403 body names the exact endpoint that unblocks it, so an agent reading destination_not_verified or sms_starter_exhausted can tell its owner precisely what to do next. More detail in the FAQ.
Troubleshooting Agent SMS Delivery
Works with Any Agent Framework
Everything above is plain HTTPS, so the integration cost is whatever your framework charges for an HTTP tool, which is usually nothing:
- Claude, Cursor, Windsurf: connect the hosted MCP server and the agent gets
send_sms,get_inbox, and the conversation tools with no code at all. - OpenClaw: two environment variables and auto-discovery, see the OpenClaw guide.
- Hermes: this guide covers the texting half; for loading your agent's daily brief into inbound voice calls on the same number, see Hermes on the Phone. One number does both.
- Anything else: the
agentcallnpm SDK wraps every endpoint here, and/llms-full.txtis a complete plain-text API reference your agent can read directly.