SMS and Conversations

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

You can only reply to a number that texted you within the last 24 hours. After 24 hours, the API returns 403 with conversation_window_expired. This is a carrier compliance requirement, not a Saperly limitation.

Reply SMS works within 24 hours of receiving an inbound message. Proactive outbound SMS (cold outreach) is on the roadmap; use a compliant third-party SMS provider until Saperly ships it.

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.

$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