Webhooks
Webhooks may be configured on accounts either in the account’s integration settings or via the Integrations API endpoint.
Signature Verification
Webhook payloads include a cryptographic signature so you may verify that the message is legitimate. This verification is completely optional on your part. The signature will be contained in an X-TextUs-Signature header.
Procedure
- Extract the signature from the header.
- Prepare the signed payload. The signed body is the request body.
- Determine the expected signature value. Compute a HMAC with the SHA256 hash function, using your webhook’s signing secret as the key and the request body string as the message.
- Compare your computed signature with the value included in the X-TextUs-Signature field. If they match then you can be sure the request is authentic.
Example
secret_key = "textus-HOTh4kXxHIbYst0xutpkdw" # From the Webhook config
payload = request.body # The incoming webhook request
signature = request.headers["X-TextUs-Signature"]
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret_key, payload)
if digest == signature
# handle webhook
else
error "Webhook request did not come from TextUs!"
end
Events
Webhooks are delivered as JSON payloads and the exact structure depends on the type of action being delivered. The actions
key contains this type and will match the following payloads.
message.received
- An incoming Message was receivedmessage.delivered
- An outgoing Message was successfully deliveredmessage.failed
- There was a failure when delivering the Messagemessage.unknown
- There was an unknown error when delivering the Messagephone_call.completed
- A PhoneCall was completedcontact.opted_out
- A Contact opted-out of receiving Messagescontact.opted_in
- A Contact opted-incontact.created
- A Contact was created
Subscribing to events
By default TextUs sends the message.delivered
and message.received
events. To receive other events, actions must be added to the integration’s settings. Updating Integrations
Response
Message and Phone Call events
For message events (message.received, message.delivered, message.failed, message.unknown) and phone call events (phone_call.completed), the response body will be a WebhookDelivery.
Example
{
"@type": "WebhookDelivery",
"@context": "/contexts/WebhookDelivery.jsonld",
"id": "/integrations/h13Jc5/deliveries/xyz",
"timestamp": "2018-07-24T20:59:32.156Z",
"action": "message.received",
"webHook": "/integrations/h13Jc5",
"conversation": {
"@type": "Conversation",
"id": "/conversations/JYnJBY",
"@context": "/contexts/Conversation.jsonld",
"slug": "JYnJBY",
"currentState": "open",
"account": "/accounts/my_account",
"phoneNumber": "+13035551234",
"formattedPhoneNumber": "(303) 555-1234",
"accountPhoneNumber": "+13035551000",
"formattedAccountPhoneNumber": "(303) 555-1000",
"assignedContact": {
"id": "/contacts/Vled2N",
"@type": "Contact",
"@context": "/contexts/Contact.jsonld",
"name": "Chuck Norris",
"firstName": "Chuck",
"lastName": "Norris",
"phones": {
"id": "/contacts/Vled2N/phones",
"@type": "hydra:Collection",
"@context": "/contexts/hydra:Collection.jsonld",
"totalItems": 1,
"members": [
{
"@type": "ContactPhone",
"id": "/contact_phones/mxvbRw",
"@context": "/contexts/ContactPhone.jsonld",
"phoneNumber": "+13035551234",
"formattedPhoneNumber": "(303) 555-1234",
"extension": null,
"type": "Mobile",
"deliverabilityStatus": "unknown",
"contact": "/contacts/Vled2N"
}
]
},
"notes": "/contacts/DQeeO69/notes",
"conversations": "/contacts/DQeeO69/conversations",
"data": {},
"createdAt": "2020-01-06T21:49:58.312Z"
},
"associatedContacts": "/conversations/JYnJBY/associated_contacts",
"assignments": {
"@type": "hydra:Collection",
"@context": "/contexts/hydra:Collection.jsonld",
"id": "/conversations/JYnJBY/assignments",
"members": [
{
"@type": "ConversationAssignment",
"@context": "/contexts/ConversationAssignment.jsonld",
"id": "/assignments/Al4QQY",
"assignee": {
"@type": "User",
"@context": "/contexts/User.jsonld",
"id": "/users/JYnGLo",
"name": "TextUs User",
"firstName": "TextUs",
"lastName": "User",
"email": "somebody@textus.com",
"avatar": null,
"automatedActor": false
},
"assigner": "/users/JYnGLo"
}
],
"totalItems": 1
},
"participants": "/conversations/JYnJBY/participants",
"reopenConversation": null,
"closeConversation": "/conversations/JYnJBY/close",
"readConversation": "/conversations/JYnJBY/read",
"unreadConversation": "/conversations/JYnJBY/read",
"starConversation": "/conversations/JYnJBY/star",
"unstarConversation": "/conversations/JYnJBY/star",
"blockConversation": "/conversations/JYnJBY/block",
"unblockConversation": null,
"subscribeConversation": null,
"unsubscribeConversation": "/conversations/JYnJBY/subscribe",
"timeline": "/conversations/JYnJBY/timeline",
"unsubscribed": false,
"blocked": false
},
"message": {
"@type": "Message",
"id": "/messages/6Nvq9L",
"@context": "/contexts/Message.jsonld",
"direction": "outgoing",
"body": "Chuck Norris can access private methods.",
"formattedBody": "<div>Chuck Norris can access private methods.</div>",
"displayTimestamp": "2018-07-24T20:59:32.156Z",
"timelinePosition": "2018-07-24T20:59:32.156Z",
"status": "delivered",
"conversation": "/conversations/JYnJBY",
"sender": {
"@type": "User",
"@context": "/contexts/User.jsonld",
"id": "/users/JYnGLo",
"name": "TextUs User",
"firstName": "TextUs",
"lastName": "User",
"email": "somebody@textus.com",
"avatar": null,
"automatedActor": false
},
"attachments": {
"@context": "/contexts/hydra:Collection.jsonld",
"@type": "hydra:Collection",
"id": "/messages/6Nvq9L/attachments",
"members": [],
"totalItems": 0
},
"friendlyStateDescription": "Inbound Message Received"
}
}
OptOut Events
For OptOut events (contact.opted-out, contact.opted-in), the response body will be a OptOutWebhookDelivery.
Example
{
"@type": "OptOutWebhookDelivery",
"@context": "/contexts/OptOutWebhookDelivery.jsonld",
"id": "/integrations/KYxmBL/deliveries/f8db6d71-04bd-47cb-9983-d6fd2015ce3b",
"action": "contact.opted_out",
"timestamp": "2021-07-27T18:48:16.878086+00:00",
"webHook": "/integrations/KYxmBL",
"optOut": {
"@type": "OptOut",
"@context": "/contexts/OptOut.jsonld",
"id": "/textus/opt_outs/50711",
"type": "opt-out",
"phoneNumber": "+15551234567",
"formattedPhoneNumber": "(555) 123-4567",
"account": "/accounts/textus"
}
}
Contact Events
For Contact events (contact.created), the response body will be a ContactWebhookDelivery.
Example
{
"@type":"ContactWebhookDelivery",
"@context":"/contexts/ContactWebhookDelivery.jsonld",
"id":"/integrations/BYoaNE/deliveries/b3d91245-9658-4d3b-a855-a524151cd02d",
"action":"contact.created",
"timestamp":"2021-07-27T15:42:44.359180+00:00",
"webHook":"/integrations/BYoaNE",
"contact":{
"@type":"Contact",
"@context":"/contexts/Contact.jsonld",
"id":"/contacts/ZNZD6Y",
"name":"Chuck Norris",
"firstName":"Chuck",
"lastName":"Norris",
"phones":{
"@type":"hydra:Collection",
"@context":"/contexts/hydra:Collection.jsonld",
"id":"/contacts/ZNZD6Y/phones",
"members":[
{
"@type":"ContactPhone",
"@context":"/contexts/ContactPhone.jsonld",
"id":"/contact_phones/9Na06L",
"phoneNumber":"+15551234567",
"formattedPhoneNumber":"5551234567",
"extension":null,
"type":"Mobile",
"deliverabilityStatus":"unknown",
"contact":"/contacts/ZNZD6Y"
}
],
"totalItems":1
},
"notes":{
"@type":"hydra:Collection",
"@context":"/contexts/hydra:Collection.jsonld",
"id":"/contacts/ZNZD6Y/notes",
"members":[],
"totalItems":0
},
"conversations":"/contacts/ZNZD6Y/conversations",
"data":{
"tags":[],
"business":"Texas Ranger"
},
"createdAt":"2021-07-27T15:42:44.086511Z"
}
}
Error Handling
Webhooks strive for an “at least once” delivery of every Event. If the endpoint receiving the Webhook responds with anything other than a Success HTTP Status (in the 2xx range), we categorize those failures in two ways:
Transient Errors
If the endpoint responds with a 504 Gateway Timeout status code, we assume the server is busy, and we will periodically retry delivering the Event with an exponential backoff for up to 12 hours.
This means that if your endpoint received the Event and began processing it, but was slow in responding to the Webhook request, we may assume it failed and retry it, which can result in duplicated events being processes by your system. To help mitigate this for Message Events, we include an id
field that you may use to ensure the Message being received in the Event has not been processed before.
Fatal Errors
Any other error status (4xx or 5xx) will result the Webhook Integration being toggled into a “failed” state. We will stop attempting to send any events to the Webhook, and backlog them within our system instead. We will periodically retry, once per hour, to see if the endpoint responds with a success. If it does so, we will automatically unset the failed state on the Webhook Integration, and replay all the events that have been backlogged. If it continues to respond with an error status, we will continue backlogging Events and retrying once per hour until it does, or until User updates the Integration.