Inbound Message Signatures

Messaging webhooks are signed with a cryptographic signature so you can validate they came from Telnyx. Here, we will describe the signing process and how to check the signature. For how to set up the webhook and get the signature in the first place, check out the [receiving messages] guide.

To validate the authenticity of webhooks sent by Telnyx, compute the signature and compare. If the provided and computed signatures differ, the webhook is not authentic. Here's how to check:

Checking the Signature

  1. Provide your HTTP server application with the messaging profile secret. We recommend this is configurable if you need to switch messaging profiles later, or rotate the secret if it is accidentally leaked.

Let us suppose the secret is the following. We will refer to the ASCII/UTF-8 encoded bytes below as secret

rq789onm321yxzkjihfEdcAm

  1. Obtain the raw byte string from the HTTP POST body. This is absolutely critical, because adding a single space, swapping two lines, or re-encoding the characters will cause the signature to dramatically change.

Here is an example payload. This sequence of bytes will be the body

Copy
Copied
{
  "sms_id":"834f3d53-8a3c-4aa0-a733-7f2d682a72df",
  "direction": "inbound",
  "from": "+13129450002",
  "to": "+13125550001",
  "body": "Hello!"
}

Note: After pasting the above content, Kindly check and remove any new line added

The precise string starts immediately before, and ends immediately after the braces, and includes 6 newline characters (there is no trailing newline). The SHA-256 hash of it begins db63cfb06... if you want to check.

  1. Parse the X-Telnyx-Signature header. It contains, separated by a comma:
    • t= : The timestamp when the signature was generated in Unix time . It is in whole seconds, as a decimal number. This ASCII/ byte string will be referred to below as time .
    • h= : The signature or hash, encoded as Base64 . Decode this into 32 bytes. The decoded byte string will be referred to below as the signature .

For our example:

Copy
Copied
X-Telnyx-Signature: t=1520983646,h=WlEXoEsHH2RMgy2x8eyvg10JlMBco0s51fdNpMORF00=

Note: After pasting the above content, Kindly check and remove any new line added

  • time is the byte string 1520983646
  • signature is the 32 bytes that WlEX... decoded into. It begins with the ASCII letters Z , Q , a non-printable \x17 , and ending with an M . This is what we'll compute independently.

Ensure that the time is within a reasonable amount of your system's time. We recommend ±30 seconds, and ensuring your system uses NTP to keep its clocks.

  1. Compute the signature and compare. Use HMAC with the following inputs:

The cryptographic hash function is SHA-256, i.e this is HMAC-SHA256.

The "message" is the concatenation, in order, of time, a period (. or \x2E), and the body. Following from our examples:

Copy
Copied
1520983646.{
  "sms_id":"834f3d53-8a3c-4aa0-a733-7f2d682a72df",
  "direction": "inbound",
  "from": "+13129450002",
  "to": "+13125550001",
  "body": "Hello!"
}

Note: After pasting the above content, Kindly check and remove any new line added

The secret key is the message profile's secret byte string.

Compute and compare!

Python Example

As an example in Python

Copy
Copied
import hashlib
import hmac

# 'time', 'body', 'secret', and 'signature' are bytes specified above

message = time + b"." + body

computed_sig = hmac.HMAC(secret, message, hashlib.sha256).digest()
# b'ZQ\x17\xa0K\x07...\xc3\x91\x17M'  (32 bytes)

# constant-time comparison is ideal for security; the output is the
# same as (signature == computed_signature)
assert hmac.compare_digest(signature, computed_signature)

Note: After pasting the above content, Kindly check and remove any new line added

MMS and More

When Telnyx receives an MMS intended for your phone number, the message will include an extra media field.

For instance, an MMS was sent to the same telephone number above. The payload could look like the following:

Copy
Copied
{
  "sms_id": "2c41e477-69b0-4c03-b91d-3d4a1e8f2c3b",
  "from": "+13129450002",
  "to": "+13125550001",
  "direction": "inbound",
  "body": "Hello!",
  "media": [
    {
      "url": "https://example.com/media/LONG_RANDOM_STRING.jpeg",
      "content_type": "image/jpeg",
      "hash_sha256": "sha256 hash",
      "size": 123456
    }
  ]
}

Note: After pasting the above content, Kindly check and remove any new line added

Validate the signature using the same process that is described above. The media will be available for 30 days. The URL does not require authentication; it is masked by the long unique ID. For sensitive media, take care to not inadvertently leak the URL.