AccountSMS
Fires when a text message is sent or received on any of the account's
numbers — including messages sent from the portal's message center and from
POST /sms/messages.
Payload
{
"id": 88214417,
"direction": "received",
"startTime": "2026-08-11T13:20:01Z",
"fromNumber": "13475550123",
"toNumber": "12125550188",
"message": "Can we move Tuesday to 9am?",
"media": "",
"webhookType": "AccountSMS"
}
| Field | Type | Description |
|---|---|---|
id | integer | The message id. Use it to deduplicate — there is no delivery id. |
direction | string | received (inbound to your number) or sent (outbound from it). |
startTime | string | RFC 3339 UTC. Named startTime, not time. |
fromNumber | string | Sender, normalized with a country code. |
toNumber | string | Recipient, normalized with a country code. |
message | string | The body. Attacker-controlled — escape before rendering. |
media | string | MMS attachments, as a JSON-encoded array of URLs inside a string — see below. Empty for a plain SMS. |
webhookType | string | Always AccountSMS. |
:::caution fromNumber and toNumber swap with direction
On a received message, toNumber is your line. On a sent message,
fromNumber is. Pick the field by direction — do not assume either position is
yours.
:::
Reacting to it
The two numbers are exactly what the conversation endpoints take — a conversation has no id, it is the pair — so you can act on the thread with no lookup first:
# your line = toNumber, other party = fromNumber, on a received message
curl -X POST ".../sms/conversations/12125550188/13475550123/resolve" \
-H "Authorization: Bearer $TB_KEY"
MMS media
media is a string containing a JSON array of URLs — not an array, not a
single URL:
"media": "[\"https://mms.example.net/9f2c/photo.jpg\"]"
Parse it, with a fallback — an older value can be a bare URL with no JSON around it:
function mediaUrls(media) {
if (!media) return [];
try {
const parsed = JSON.parse(media);
return Array.isArray(parsed) ? parsed : [String(parsed)];
} catch {
return [media]; // not JSON: treat the whole value as one URL
}
}
The URLs need no Authorization header, so anyone holding one can fetch the
attachment. They are also not permanent — download the file when the event
arrives rather than storing the link.
Delivery status
This fires when a message is recorded — for an outbound message, that the
gateway accepted it. There is no separate delivered or failed event.
Gotchas
- Your own API sends fire this event. An auto-responder that replies to every
AccountSMSwill reply to itself. Filter ondirection == "received". - Nothing fires on conversation state. Resolving a thread in the portal emits no event; this is about messages only.
- On an outbound group message,
toNumberis"0". A group thread is one row whose recipient column is0, and the participant list is not in this payload.