SMS and Conversations

Every Saperly line can receive inbound SMS. You can reply and inspect conversation history through the API.

Outbound SMS requires either an inbound message from the recipient within the last 24 hours, OR an active explicit_outbound consent record on file for that recipient on this line (recorded via POST /v1/consent or a documented web-form opt-in). When neither holds, the API returns 403 with conversation_window_expired. This is a carrier compliance requirement.

Two paths to a successful outbound SMS:

  1. Reply within 24h — automatic; no extra setup required.
  2. Explicit consent — record an explicit_outbound consent for the recipient via POST /v1/consent (or a documented web-form opt-in) before sending. Once recorded, you can send to that recipient on that line outside the 24h reply window.

See Compliance and Consent for how to capture and record opt-ins.

What happens on inbound SMS

Your webhook receives an event like this:

1{
2 "event": "sms_received",
3 "line_id": "line_abc123",
4 "from_number": "+14155551234",
5 "to_number": "+14155550123",
6 "message": "Hello, I have a question about my order.",
7 "message_sid": "SM1234567890abcdef",
8 "timestamp": 1711900000000
9}

The timestamp is Unix milliseconds. The message_sid is the upstream carrier identifier.

SMS events arrive on the same webhook_url as call events. Distinguish them by the event field: sms_received for SMS, message for voice calls. This works for both webhook-mode and hosted-mode lines — voice mode does not gate SMS routing. A line without webhookUrl still records the consent record and logs the sms_received compliance event on inbound, but Saperly has nowhere to deliver the event, so your code cannot auto-reply with its own LLM.

Reply to a message

Send a reply with the line id, recipient number, and message text. The recipient must have texted you within the last 24 hours, OR you must have an active explicit_outbound consent record on file for that (line, recipient).

$curl -X POST https://saperly.com/api/v1/messages \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "line_id": "LINE_ID",
> "to": "+14155551234",
> "text": "Thanks for reaching out. We will get back to you shortly."
> }'

Response shape

A successful POST /api/v1/messages returns the queued message:

1{
2 "id": "SM1234567890",
3 "line_id": "line_abc123",
4 "to": "+14155551234",
5 "text": "Thanks for reaching out.",
6 "status": "queued",
7 "created_at": "2026-04-10T12:00:00Z"
8}

The status field transitions from queued to sent to delivered (or failed) as the carrier processes the message.

List conversation threads

$curl https://saperly.com/api/v1/conversations \
> -H "Authorization: Bearer sk_live_..."

Get one thread

$curl https://saperly.com/api/v1/conversations/LINE_ID/+14155551234 \
> -H "Authorization: Bearer sk_live_..."

Response shape

GET /api/v1/conversations/{lineId}/{phoneNumber} returns the full message history for the thread, ordered newest to oldest (most recent first):

1{
2 "messages": [
3 { "direction": "outbound", "text": "Hi!", "timestamp": "2026-04-10T12:00:30Z" },
4 { "direction": "inbound", "text": "Hello", "timestamp": "2026-04-10T12:00:00Z" }
5 ],
6 "has_more": false,
7 "next_cursor": null
8}

Each message has a direction (inbound or outbound), the text body, and a timestamp in ISO 8601 format.

Practical constraints

The simplest useful mental model is “reply to active conversations.”

If you are designing a workflow around outbound cold SMS from scratch, stop and check your carrier and registration requirements first.

Good uses

  • support follow-up
  • appointment confirmation
  • delivery coordination
  • threaded AI assistance after a user texts first