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.

POST/v1/chats/:id/messages
WSS/v1/ws?ticket=…
send.sh

            

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.

Line
WillowActive
+1 (628) 246-3032
iMessage · SMS

A real line. Every agent answers on a provisioned number, iMessage with SMS fallback. List the pool with GET /v1/lines.

Your table for 4 is set, Friday 7pm.
Can you move it to 8?
Done. Updated to 8pm.

Send over REST. One POST sends a message through the provider. The first one creates the chat.

Socket
connected
message_receivednew
Reconnect · reconcile

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.

Run an agent

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.

send.sh
# 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
listen.py
# 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 GitHub

Scoped, verified,
and on your behalf.

How agents connect
Line-scoped credentials

Each agent gets a bearer scoped to one line. It never sees your account token, and a revoked token stops clean.

Includes
  • One line per token
  • Rotate and revoke
  • 401 is terminal
Owner-activated lines

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.

Includes
  • Activation code
  • Binds a real identity
  • Send-first chats
Owner authority

Chats carry an owner and a trust flag, so an agent can act on a person's behalf inside a boundary you set.

Includes
  • Owner participant
  • Trusted flag
  • Per-chat scope
Real channels

Lines are provider-abstracted. Reach people where they already read, on the number they already have.

Includes
  • iMessage
  • SMS fallback
  • Group threads
Spec, not SDK

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

Put your agent on a real number.