Texting and inbox

Read calls, voicemails, and texts in one history. Send from a business number, check delivery, and share reusable replies with your team.

The activity model

Messages use shared persistence (messages and message_send_attempts), rather than separate SMS and MMS tables. A message retains its identity across retries; each provider submission has its own potentially chargeable attempt. Currently only SMS is implemented. Sending uses /business_numbers/:id/messages, retrieval uses /messages/:id, and explicit retries use /messages/:message_id/retries. The unused proof-of-concept /business_numbers/:id/sms_messages sending route is not exposed. The optional send body format defaults to sms; MMS is rejected before any provider request. Message activity uses type: "message" with a separate format: "sms". Future formats share the same message activity type.

List grouped previews with GET /api/v1/inbox, load a number's timeline with GET /api/v1/inbox/:phone_number/activity, and mark observed items seen with POST /api/v1/inbox/:phone_number/seen. The existing /api/v1/activity/unseen endpoint includes people with unseen incoming SMS when messaging is enabled.

Messages create or update the external phone number's contact touchpoint, advancing last_contact_at like calls and voicemails. Web and API inbox aggregation includes calls and voicemails, plus messages when the company's messaging feature flag is enabled. The web timeline renders enabled message activity alongside calls and voicemails through a shared message view. With messaging disabled, messages are excluded from timelines, previews, unseen counts, and seen updates. Unknown activity types are skipped by the web renderer. The inbox API derives message ordering directly from message timestamps.

A conversation is a grouped view of activity for a company and external phone number. There is no conversation resource to create, no conversation ID, and no requirement to save a named contact before texting. Business-number filters narrow that view without changing the underlying records.

Use the existing contacts API for recipient details and the caller context API for integration panels. Every endpoint below accepts company API keys or OAuth tokens. Sending and retrying additionally require a current employee belonging to that company.

Properties

  • Name
    id
    Type
    string
    Description
    Stable public record ID. SMS IDs identify stored Phone.inc messages, not provider send attempts.
  • Name
    type
    Type
    string
    Description
    One of call, voicemail, or message.
  • Name
    format
    Type
    string
    Description
    Message format, currently sms. Present only on message items; future formats use the same message activity type.
  • Name
    created_at
    Type
    string
    Description
    ISO 8601 UTC timestamp with microseconds. For incoming SMS this is when Phone.inc stored the message; received_at retains the provider timestamp.
  • Name
    business_number_id
    Type
    string
    Description
    Business number public ID for this activity.
  • Name
    business_number_name
    Type
    string | null
    Description
    Business number label.
  • Name
    seen_at
    Type
    string | null
    Description
    Shared team seen timestamp. Incoming texts start unseen.
  • Name
    direction
    Type
    string
    Description
    Inbound or outbound.
  • Name
    from / to
    Type
    string
    Description
    Phone numbers in E.164. Voicemail items include from only.
  • Name
    text
    Type
    string
    Description
    SMS body, encrypted at rest. Present on SMS items, not on call or voicemail items.
  • Name
    author
    Type
    object
    Description
    SMS author: type is customer, employee, or automation. Employee authors include their public id and name when the employee still exists. Automation includes name: "Auto-reply".
  • Name
    status
    Type
    string
    Description
    SMS status: queued, accepted, delivered, failed, unknown, or received. Acceptance is not delivery. unknown includes provider timeouts and unknown delivery reports.
  • Name
    sms_count / encoding
    Type
    integer | null / string | null
    Description
    Provider-reported chargeable parts and gsm7 or ucs2 encoding. May be null before acceptance or when the response was lost.
  • Name
    received_at / delivered_at
    Type
    string | null
    Description
    Provider receive timestamp for incoming SMS, or confirmed delivery timestamp for outgoing SMS.
  • Name
    failure_code
    Type
    string | null
    Description
    Sanitized SMS failure code, such as provider_rejected, sending_unavailable, or inmobile_-1. Not a raw provider error message.
  • Name
    can_retry
    Type
    boolean
    Description
    True only for a definitively failed outgoing message whose sender still supports sending.
  • Name
    summary / duration / state / missed / employee / has_transcription
    Type
    call fields
    Description
    Call summary, duration in seconds, state, missed flag, nullable employee reference, and whether transcription is available. Fetch the existing call endpoint for full transcription.
  • Name
    transcription / recording_url / listened / call_id
    Type
    voicemail fields
    Description
    Voicemail transcript, playback URL, listened flag, and linked call public ID when present. Voicemails also include duration in seconds when known.

Inbox threads include phone_number, a nullable contact reference, last_activity_at, unseen_count, business_number_ids, reply_business_number_id, preview, and latest_item. The preview is limited to 160 characters. The latest-item metadata omits full text, summary, transcript, and recording URL; load activity for the full content. The suggested reply number is the latest item's number when it supports sending, otherwise null. Always select the sender explicitly for a send.

Thread unseen counts include only incoming calls, voicemails, and incoming texts with no seen timestamp. Outgoing activity never contributes to unread counts, even if its seen timestamp is cleared. A voicemail and its linked missed call count as one item. The existing unseen activity API counts distinct people per business number rather than summing message counts. The unread endpoint includes incoming SMS when messaging is enabled. The legacy /calls/seen endpoint remains calls-and-voicemails-only; use the inbox seen endpoint to acknowledge SMS. Clients should ignore unknown activity types and continue cursor pagination even when a page contains only unsupported items. A whole-thread seen acknowledgement applies to every activity type through its observation boundary, including items a client did not render.

Messaging capabilities

Read messaging from the business-number API:

{
  "messaging": {
    "sms": { "can_send": true, "can_receive": true },
    "mms": { "can_send": false, "can_receive": false }
  }
}

The company's messaging feature flag defaults to off and is managed by Phone.inc. Messaging endpoints (messages, retries, text snippets, and texting settings) return 404 while it is off. SMS is excluded from inbox previews, timelines, unseen counts, and seen updates. Sending capabilities also require this flag, including queued sends, retries, and automatic replies. Receive capability reflects the number's provider and inventory: signed carrier callbacks keep storing inbound messages and updating delivery state even with messaging off, so messages become visible when it is enabled. The number's existing provider handles voice and messaging. Currently only inMobile SMS is implemented; MMS is advertised as unsupported.

Unsupported or inactive senders cannot send, retry, or dispatch queued attempts. Existing history remains available when the company's messaging flag is enabled; delivery callbacks continue regardless of that flag. The message endpoints are format-neutral; only SMS is currently implemented. Automatic replies remain SMS-only.

Delivery, retries, and live updates

Send with the existing Send an SMS endpoint. Use a new idempotency key for each intentional send and reuse that key after a transport interruption. Keys are scoped to the sender business number. Reusing a key with changed recipient, body, or employee returns 409.

Messages are persisted before the provider request. A send error after persistence includes the stable message id. Check that record before taking further action. There are no automatic provider-request retries. A process interrupted after claiming an attempt leaves its state unknown, not safely retryable.

Incoming messages and delivery callbacks use signature verification. Incoming provider message IDs are deduplicated permanently while their SMS record exists, independently of receipt retention. Provider timestamps do not control the arrival/seen boundary. Texts and settings are not governed by call-data retention; they remain until their owning number or company is removed.

The authenticated CallsChannel sends sms_update notifications containing id and phone_number, plus unseen_update notifications. Treat these as invalidations: refresh the inbox, activity, or message endpoint. Refresh on reconnect as well. No message body is broadcast. Mobile background notification registration and delivery are not part of this API.

Incoming callbacks persist texts only for active inMobile business numbers. Callbacks for unknown or non-active numbers are rejected before reserving a receipt or enqueueing an auto-reply.

Canonical inMobile webhooks are /webhooks/inmobile/messages for incoming messages and /webhooks/inmobile/delivery_reports for outgoing delivery reports. The existing /webhooks/inmobile/sms and /webhooks/inmobile/sms_statuses URLs remain direct aliases, so existing provider configuration and preview callback URLs keep working.

For delivery callbacks, configure the server-owned INMOBILE_SMS_STATUS_CALLBACK_URL to your public /webhooks/inmobile/sms_statuses URL and configure InMobile signing keys through INMOBILE_WEBHOOK_PUBLIC_KEYS. The URL is attached to outgoing provider requests; clients cannot override it. Without a callback configuration, accepted messages cannot advance to confirmed delivery through this integration.


GET/v1/inbox

List the inbox

Returns one thread per external phone number, newest activity first. Optional business_number_id restricts history to that number. query matches contact name, company name, or phone number (up to 128 characters); it does not search message bodies. unseen=true returns only threads with unseen timeline items. limit defaults to 50 and is clamped to 1–100. Pass next_cursor as cursor to load more. A cursor is bound to the company, business-number filter, query, and unseen filter; malformed or mismatched cursors return 422.

Request

GET
/v1/inbox
curl --get https://app.phone.inc/api/v1/inbox \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  --data-urlencode 'limit=50'

Response

{
  "threads": [
    {
      "phone_number": "+4523909778",
      "contact": null,
      "last_activity_at": "2026-10-06T10:41:00.000000Z",
      "unseen_count": 1,
      "business_number_ids": [
        "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01"
      ],
      "reply_business_number_id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
      "preview": "Is the basket included?",
      "latest_item": {
        "id": "019daef1-085e-73f0-85f6-a71e2d8dd2cc",
        "type": "message",
        "format": "sms",
        "business_number_id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
        "direction": "inbound",
        "status": "received"
      }
    }
  ],
  "next_cursor": null
}

GET/v1/inbox/:phone_number/activity

List activity

The phone_number path parameter identifies the external person. National-format numbers are normalized using the company's country; E.164 is recommended; URL-encode the leading + as %2B. Optional business_number_id filters the history. limit defaults to 50 and is clamped to 1–100. Items are newest first; render them in reverse order for a chat timeline. Pass next_cursor as cursor for older items. Stable tie-breaking prevents duplicate items when timestamps match. A voicemail replaces its linked call's timeline item. An unknown number returns an empty list and does not create a contact. The response includes the normalized E.164 phone_number and nullable contact reference for the conversation header; use that normalized number when sending from a national-number draft. Reading does not change seen state. Save observed_at for the mark-seen request. Cursor pages preserve that original observation timestamp, so paging through older history does not mark newer arrivals seen. Pass observed_at unchanged as a string when marking seen; converting it through a client date type can lose microsecond precision.

Request

GET
/v1/inbox/:phone_number/activity
curl --get https://app.phone.inc/api/v1/inbox/%2B4523909778/activity \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  --data-urlencode 'limit=50'

Response

{
  "phone_number": "+4523909778",
  "contact": null,
  "items": [],
  "next_cursor": null,
  "observed_at": "2026-10-06T10:41:00.000000Z"
}

POST/v1/inbox/:phone_number/seen

Mark activity seen

Marks incoming calls, voicemails, and texts from this number seen through the observed_at timestamp returned by List activity. Optional business_number_id restricts the change to one business number; omitting it applies across the company. through is required, must be ISO 8601, and must not be in the future. Later arrivals remain unseen, including texts whose provider timestamp predates your observation. Seen state is shared by the team, not per employee. Voicemail listened state is unchanged. Returns 204 No Content.

Request

POST
/v1/inbox/:phone_number/seen
curl -X POST https://app.phone.inc/api/v1/inbox/%2B4523909778/seen \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"through":"2026-10-06T10:41:00.000000Z"}'

GET/v1/messages/{id}

Retrieve a text

Returns a persisted SMS by its public id, including its current delivery status. Unknown or foreign-company IDs return 404. Use this after a network interruption or a send response with an id; do not create another send just to check whether the first succeeded.

Request

GET
/v1/messages/{id}
curl https://app.phone.inc/api/v1/messages/019daef1-085e-73f0-85f6-a71e2d8dd2cc \
  -H "X-Api-Key: $PHONE_INC_API_KEY"

Response

{
  "id": "019daef1-085e-73f0-85f6-a71e2d8dd2cc",
  "type": "message",
  "format": "sms",
  "created_at": "2026-10-06T10:41:00.000000Z",
  "business_number_id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
  "business_number_name": "Sales",
  "seen_at": "2026-10-06T10:41:00.000000Z",
  "direction": "outbound",
  "from": "+4592450113",
  "to": "+4523909778",
  "text": "Your appointment is confirmed.",
  "author": {
    "type": "employee",
    "id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
    "name": "Lea"
  },
  "status": "accepted",
  "sms_count": 1,
  "encoding": "gsm7",
  "delivered_at": null,
  "received_at": null,
  "failure_code": null,
  "can_retry": false
}

POST/v1/messages/{id}/retries

Retry a failed text

Explicitly retries a definitively failed text with the same sender and body, keeping its message id and recording a new send attempt. This is a real, chargeable SMS. An employee belonging to the company is required. Supply a new idempotency_key (1–128 characters), or the Idempotency-Key header. Repeating the same retry key replays the attempt without sending again. unknown, accepted, delivered, and inbound messages cannot start a retry (409). The sender must still support sending. A different key represents a new retry, not a replay. A missing/invalid retry key returns 409. Send and retry attempts share a limit of 30 per business number in a rolling minute; exceeding it returns 429 with Retry-After: 60.

Request

POST
/v1/messages/{id}/retries
curl -X POST https://app.phone.inc/api/v1/messages/019daef1-085e-73f0-85f6-a71e2d8dd2cc/retries \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"idempotency_key":"retry-appointment-1"}'

Response

{
  "id": "019daef1-085e-73f0-85f6-a71e2d8dd2cc",
  "type": "message",
  "format": "sms",
  "status": "accepted",
  "can_retry": false
}

GET/v1/text_snippets

List snippets

Returns company-shared snippets ordered by position, then creation order. Selecting a snippet only inserts editable text into your local draft. It never sends an SMS.

Request

GET
/v1/text_snippets
curl https://app.phone.inc/api/v1/text_snippets \
  -H "X-Api-Key: $PHONE_INC_API_KEY"

Response

[
  {
    "id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
    "title": "On my way",
    "text": "I'm on my way, with you in about 15 minutes.",
    "position": 0,
    "created_at": "2026-10-06T10:41:00.000000Z",
    "updated_at": "2026-10-06T10:41:00.000000Z"
  }
]

POST/v1/text_snippets

Create a snippet

Required title contains 1–128 characters and text contains 1–10,000 characters; whitespace-only values are rejected. Optional position is a nonnegative integer, default 0. Returns the saved snippet with 201 Created.

Request

POST
/v1/text_snippets
curl -X POST https://app.phone.inc/api/v1/text_snippets \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"On my way","text":"I'"'"'m on my way, with you in about 15 minutes.","position":0}'

Response

{
  "id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
  "title": "On my way",
  "text": "I'm on my way, with you in about 15 minutes.",
  "position": 0,
  "created_at": "2026-10-06T10:41:00.000000Z",
  "updated_at": "2026-10-06T10:41:00.000000Z"
}

GET/v1/text_snippets/{id}

Retrieve a snippet

Returns a company-shared snippet by public id. Unknown and foreign-company IDs return 404.

Request

GET
/v1/text_snippets/{id}
curl https://app.phone.inc/api/v1/text_snippets/0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01 \
  -H "X-Api-Key: $PHONE_INC_API_KEY"

Response

{
  "id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
  "title": "On my way",
  "text": "I'm on my way, with you in about 15 minutes.",
  "position": 0,
  "created_at": "2026-10-06T10:41:00.000000Z",
  "updated_at": "2026-10-06T10:41:00.000000Z"
}

PATCH/v1/text_snippets/{id}

Update a snippet

Accepts any combination of title, text, and position, using the same validation as creation. Other fields are ignored. Changes are shared with the company.

Request

PATCH
/v1/text_snippets/{id}
curl -X PATCH https://app.phone.inc/api/v1/text_snippets/0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01 \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"I'"'"'ll be there in 10 minutes."}'

Response

{
  "id": "0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01",
  "title": "On my way",
  "text": "I'll be there in 10 minutes.",
  "position": 0,
  "created_at": "2026-10-06T10:41:00.000000Z",
  "updated_at": "2026-10-06T10:42:00.000000Z"
}

DELETE/v1/text_snippets/{id}

Delete a snippet

Deletes a company-shared snippet. Previously sent texts are unaffected. Returns 204 No Content.

Request

DELETE
/v1/text_snippets/{id}
curl -X DELETE https://app.phone.inc/api/v1/text_snippets/0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01 \
  -H "X-Api-Key: $PHONE_INC_API_KEY"

GET/v1/business_numbers/{business_number_id}/texting_settings

Retrieve texting settings

Returns the business number's outside-hours auto-reply settings. A number without saved settings returns these defaults without creating a settings record. Hours and timezone come from the business number's existing configuration.

Request

GET
/v1/business_numbers/{business_number_id}/texting_settings
curl https://app.phone.inc/api/v1/business_numbers/0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01/texting_settings \
  -H "X-Api-Key: $PHONE_INC_API_KEY"

Response

{
  "auto_reply_enabled": false,
  "auto_reply_text": null,
  "auto_reply_cooldown_minutes": 1440
}

PATCH/v1/business_numbers/{business_number_id}/texting_settings

Update texting settings

Requires a current employee belonging to the company. Accepts auto_reply_enabled (boolean), auto_reply_text (up to 10,000 characters; required and nonblank when enabled), and auto_reply_cooldown_minutes (integer 60–43,200, default 1,440). Enabling this may send real, chargeable texts without an app open, but only while the number supports SMS sending. On an incoming text outside the number's business hours, a background job sends the reply from that same number. The job skips sending if business hours have reopened. The cooldown applies per external number and business number, and also prevents loops on duplicate incoming callbacks. Automated messages use author.type=automation; they appear in the normal history. Failed or uncertain automatic sends are not blindly retried. Shared sending limits still apply.

Request

PATCH
/v1/business_numbers/{business_number_id}/texting_settings
curl -X PATCH https://app.phone.inc/api/v1/business_numbers/0193f4a9-7c34-7c00-9e3d-2b2b8a7f1c01/texting_settings \
  -H "X-Api-Key: $PHONE_INC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_reply_enabled":true,"auto_reply_text":"We'"'"'re closed now. We'"'"'ll reply when we reopen.","auto_reply_cooldown_minutes":1440}'

Response

{
  "auto_reply_enabled": true,
  "auto_reply_text": "We're closed now. We'll reply when we reopen.",
  "auto_reply_cooldown_minutes": 1440
}