Give your agent a phone number.
One REST and WebSocket API to send and receive on real iMessage and SMS lines. Your agent texts people, and they text back.
Send a text. Hear back in real time.
Outbound goes over REST. Inbound streams over one WebSocket. Reconnect when it drops and reconcile missed events with a quick REST read. The far end is a real person in Messages, not a chat widget.
A line, a request, a socket.
Three primitives carry the whole protocol. A provisioned line to answer on, a REST call to send, and a WebSocket to hear replies as they land.
A real line. Every agent answers on a provisioned number, iMessage with SMS fallback. List the pool with GET /v1/lines.
Send over REST. One POST sends a message through the provider. The first one creates the chat.
Receive over WebSocket. Mint a ticket, dial the socket, and read frames as they arrive. Reconnect and reconcile over REST when it drops.
Messaging that behaves like a person.
The affordances a real texter has, in the protocol, so your agent reads as one.
Real-time inbound. Messages arrive over one WebSocket carrying the same event envelope a webhook gets. On a dropped socket you reconnect and reconcile with GET /v1/chats, then GET /v1/chats/:id/messages.
Attachments, declared then sent. Declare the file, upload it to a signed URL, then send with attachment_uids. Images, video, and files, up to four in order.
Reads like a person. Voice memos, typing indicators, quoted replies, and paced sequences. The things that make an agent feel like it is really there.
Scheduled delivery. Cron and follow-up updates go back out through the same POST, so an agent can check in without being asked.
Start with curl. No SDK required.
The protocol is the surface. Activate a line, send with one request, and read replies off the socket. Reference scripts, not a mandatory library.
# The first message creates the chat curl $PLOW_API_BASE/v1/chats \ -H "Authorization: Bearer $PLOW_AGENT_TOKEN" \ -d '{ "line_uid": "ln_8f3a", "members": ["+16505551234"], "body": "Your table for 4 is set.", "idempotency_key": "dinner-fri" }' # -> { "uid": "cht_7f2a", "status": "active" }
Send a message
One POST goes out through the provider. An idempotency key keeps a retry from texting twice.
Open the API reference# Mint a one-shot ticket, then read the socket t = post("/v1/ws/ticket").json()["ticket"] ws = connect(f"{WS}/v1/ws?ticket={t}") for frame in ws: e = json.loads(frame) # first frame is {"type": "connected"}; use .get if e.get("event_type") == "message_received": msg = e["data"]["message"] if msg["direction"] == "inbound": reply(msg["chat_uid"], answer(msg))
Listen for replies
Read frames off the socket. Skip anything that is not inbound, since outbound frames are echoes of your own sends.
Chat plugin on GitHubScoped, verified,
and on your behalf.
How agents connect
Each agent gets a bearer scoped to one line. It never sees your account token, and a revoked token stops clean.
- One line per token
- Rotate and revoke
- 401 is terminal
A line is bound to a person when they activate it by texting a code. Chats on that line are created send-first, addressed to real handles.
- Activation code
- Binds a real identity
- Send-first chats
Chats carry an owner and a trust flag, so an agent can act on a person's behalf inside a boundary you set.
- Owner participant
- Trusted flag
- Per-chat scope
Lines are provider-abstracted. Reach people where they already read, on the number they already have.
- iMessage
- SMS fallback
- Group threads
The protocol is the surface.
There is no client library to adopt or outgrow. The whole API is HTTP and one WebSocket, described in the OpenAPI spec. Read it, curl it, and wire it into whatever your agent already runs on.
$curl https://api.plow.co/openapi.json
OpenAPI spec