Webhooks notify your application when something happens on your account, such as a reply arriving or a message being delivered. We send an HTTP request to a URL you choose each time the event occurs. You can have as many webhook subscriptions as you need.
Webhooks are the recommended way to receive replies and delivery receipts. Polling List inbound messages works, but is slower and uses up your rate limit.
Create a subscription
In touchSMS, open Settings in the sidebar, then Webhooks (or go to https://app.touchsms.com.au/settings/webhooks). Click Create webhook subscription, or edit an existing one.
For each subscription you choose:
- the event to subscribe to,
- the URL to call,
- the request method:
POSTorGET, - the encoding for
POST:jsonorform, - up to 10 custom headers, for example an API key your endpoint expects.
You can also manage subscriptions over the API:
| Task | Endpoint |
|---|---|
| List subscriptions | List webhook subscriptions |
| Create a subscription | Create webhook subscription |
| Get one subscription | Get webhook subscription |
| Update a subscription | Update webhook subscription |
| Delete a subscription | Delete webhook subscription |
{
"name": "Inbound to CRM",
"event": "message_received",
"url": "https://example.com/hooks/inbound",
"request_method": "POST",
"request_encoding_type": "json",
"headers": [
{ "key": "X-Api-Key", "value": "abc123" }
]
}A few things to know when using the API:
headersis an array of{ "key", "value" }objects, not a plain object. It is required: send[]if you have none. Header names must be 3 to 50 characters, and values 3 to 200.request_encoding_typeis required whenrequest_methodisPOST.- Updates replace the whole subscription, so send every field, not only the ones you’re changing.
- The URL can’t point at one of our own domains.
Subscription status
| Status | Meaning |
|---|---|
Active |
Enabled and receiving events. |
Disabled |
Disabled manually. |
Disabled Failing |
Disabled automatically after repeated delivery failures. |
A subscription in Disabled Failing gets no events until you re-enable it. If your endpoint has been down, check for this once it’s back up.
Events
| Event | Sent when |
|---|---|
message_received |
An inbound message (SMS, MMS or web chat) arrives on your account. |
message_dlr |
The delivery status of a message you sent changes. |
optout_created |
A number opts out. |
message_received_3cx |
An inbound message arrives, formatted for the 3CX phone system integration. Use message_received for your own integrations. |
message_received
Sent when an inbound message arrives, to a dedicated number or to the shared number pool.
{
"event": "message_received",
"date": "2022-01-13T04:23:57Z",
"id": 123456,
"to": "SHARED_NUMBER",
"from": "61412345678",
"content": "Hello Reply",
"media": [],
"is_opt_out": false,
"channel": null,
"in_reply_to_message": {
"id": 654321,
"content": "Hello World",
"metadata": {
"your_system_id": "c5969b2c-d33a-402d-9d7e-f82c98f85be7"
},
"reference": null,
"campaign_id": 123456
}
}| Field | Description |
|---|---|
event |
Always message_received. |
date |
When the message was received. |
id |
Inbound message ID. |
to |
The dedicated number that received the message, or SHARED_NUMBER for the shared pool. |
from |
The sender’s number. |
content |
Message text. An empty string for an MMS sent without text. |
media |
MMS media files. An empty array for SMS. See Inbound MMS media. |
is_opt_out |
true if the message opted the sender out (for example, a STOP reply). |
channel |
webchat if the message came from the web chat widget, null for SMS and MMS. |
in_reply_to_message |
The outbound message this is a reply to, including its metadata, reference and campaign_id. null if no outbound message matched. |
Inbound MMS media
For an MMS, media has one entry per file. Each entry has a key (the file’s unique, stable identifier) and a url (a direct link to the file). The payload doesn’t include the file contents.
"media": [
{ "key": "01gw171s65ecm8mhpx2086f7pg", "url": "https://mms.example.com/m/01gw171s65ecm8mhpx2086f7pg.jpeg" },
{ "key": "01gw171s65ecm8mhpx2086f7ph", "url": "https://mms.example.com/m/01gw171s65ecm8mhpx2086f7ph.png" }
]- Downloading needs no authentication. Fetch the
urlwith a plainGET, with no API key or login. - Treat URLs as secrets. Anyone with a URL can fetch the file. Don’t forward links to untrusted recipients; attach the downloaded file instead.
- Don’t assume a file type. Extensions reflect the type received from the carrier (commonly
.jpeg,.pngor.gif). Read theContent-Typeheader of the download. - Iterate the array. An MMS can contain several files, in the order the carrier sent them, so don’t just read
media[0]. - Download straight away and keep your own copy. Files are kept according to your account’s retention policy, and URLs aren’t guaranteed to resolve forever. If a download fails, retry promptly, and don’t return a success response until the media is stored.
List inbound messages returns the same media as media_urls, a flat array of URLs.
message_dlr
Sent each time the delivery status of a message you sent changes.
{
"event": "message_dlr",
"date": "2022-06-06T10:35:45Z",
"id": 123456,
"to": "61411802860",
"from": "61419046139",
"content": "Test",
"parts": 1,
"cost": 1,
"reference": null,
"metadata": {
"your_system_id": "c5969b2c-d33a-402d-9d7e-f82c98f85be7"
},
"campaign_id": 618,
"status": "delivered"
}| Field | Description |
|---|---|
event |
Always message_dlr. |
date |
When the status changed. |
id |
Outbound message ID. |
to |
Recipient number. |
from |
Sender ID the message was sent from, or SHARED_NUMBER for the shared pool. |
content |
Message text. |
parts |
Number of SMS parts. |
cost |
Cost of the message. |
reference, metadata |
The values you supplied when sending. |
campaign_id |
Campaign ID, used in reporting. |
status |
The new status. See below. |
Delivery statuses
Treat any status starting with failed as a delivery failure.
| Status | Meaning |
|---|---|
sent |
Handed to the carrier, awaiting a delivery receipt. |
delivered |
Delivered to the handset (or, in some cases, to the recipient’s carrier). |
failed |
Delivery failed. |
failed_rejected |
The carrier rejected the message. |
failed_expired |
The message couldn’t be delivered before it expired. |
failed_insufficient_balance |
Your account balance was too low to send the message. Not billed. |
cancelled |
The message was cancelled before sending. Not billed. |
opt_out |
The recipient had opted out. |
filtered |
Blocked by spam filtering, ours or upstream. |
filtered_duplicate |
Filtered as a duplicate. Not billed. |
optout_created
Sent each time a number opts out on your account: through a reply, an opt-out link, the web interface or the API. Imported opt-outs don’t trigger it.
{
"event": "optout_created",
"id": 123456,
"number": "61419046139",
"method": "reply",
"country": {
"id": 1,
"iso": "AU",
"name": "Australia"
},
"opt_out_list": {
"id": 1,
"name": "Default Opt-Out List",
"is_default": true
},
"created": "2022-06-06T10:35:45Z"
}| Field | Description |
|---|---|
id |
Opt-out ID. |
number |
The number that opted out. |
method |
How the opt-out was created. See below. |
country |
The number’s country: id, two-letter iso code and name. |
opt_out_list |
The opt-out list it was added to. |
created |
When the opt-out was created. |
method |
Created by |
|---|---|
reply |
An SMS reply, such as STOP. |
link |
An opt-out link. |
manual |
Someone in the web interface. |
api |
The API. |
chat |
The chat inbox. |
workflow |
An automated workflow. |
Responding and retries
Return a 2xx status as soon as you’ve safely stored the event. Any other response, or a timeout, counts as a failure.
Failed webhooks are retried up to 15 times, with exponentially increasing delays capped at 12 hours. Process events idempotently (for example, by event id and status), since the same event can arrive more than once.
Signature verification
Webhooks are signed with your account’s webhook secret, which you’ll find in your API settings in touchSMS. The signature is sent in the Signature header:
Signature: HMAC-SHA256(raw_json_request_body, webhook_secret)Compute the HMAC-SHA256 of the raw request body with your secret and compare it to the header in constant time. In PHP:
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, $request->header('Signature'))) {
abort(401);
}In Node.js:
import crypto from 'node:crypto';
const expected = crypto.createHmac('sha256', webhookSecret).update(rawBody).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));Use the raw body exactly as received. Parsing and re-serialising the JSON can change it and break the signature. For form-encoded and GET webhooks, the signature is still computed over the JSON encoding of the payload.
Legacy HTTP callbacks
Older accounts may use the legacy delivery receipt and incoming SMS callbacks configured in account settings. They still work, but new integrations should use webhook subscriptions.
Legacy callback parameters
Both callbacks are sent as POST requests with these body parameters.
Delivery receipt callback
| Parameter | Description |
|---|---|
reference |
The reference you supplied when sending. |
description |
DELIVERED or FAILED. |
time |
When the receipt was generated, as YYYY-MM-DD HH:MM:SS. |
timezone |
Timezone of time. Always your account’s timezone. |
Incoming SMS callback
| Parameter | Description |
|---|---|
reference |
The reference of the message being replied to. |
message |
The reply text. |
number |
The sender’s number, in international format. |
time |
When the reply was received, as YYYY-MM-DD HH:MM:SS. |
timezone |
Timezone of time. Always your account’s timezone. |