Link Search Menu Expand Document

Updated Fri Mar 5th 2021, 12:17 UTC

Webhooks

Webhooks may be configured on accounts either in the account’s integration settings or via the Integrations API endpoint.

  1. Webhooks
    1. Signature Verification
      1. Procedure
        1. Example
    2. Events
    3. Response
    4. Error Handling
      1. Transient Errors
      2. Fatal Errors

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

  1. Extract the signature from the header.
  2. Prepare the signed payload. The signed body is the request body.
  3. 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.
  4. 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.digest(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 action key contains this type and will match the following payloads.

  • message.received - An incoming Message was received
  • message.delivered - An outgoing Message was successfully delivered
  • message.failed - There was a failure when delivering the Message
  • message.unknown - There was an unknown error when delivering the Message
  • phone_call.completed - A PhoneCall was completed
  • contact.opted_out - A Contact opted-out of receiving Messages
  • contact.opted_in - A Contact opted-in

Response

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"
  }
}

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.


© 2021 TextUs